主播中心/邀请活动
This commit is contained in:
parent
7c1ecb804e
commit
4fc2ddebf1
@ -6,10 +6,12 @@ import (
|
|||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/lingxian"
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
|
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||||
"chatapp3-golang/internal/service/signinreward"
|
"chatapp3-golang/internal/service/signinreward"
|
||||||
"chatapp3-golang/internal/service/weekstar"
|
"chatapp3-golang/internal/service/weekstar"
|
||||||
"context"
|
"context"
|
||||||
@ -39,6 +41,8 @@ func main() {
|
|||||||
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
|
roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
hostCenterService := hostcenter.NewService(app.Config, &app.Gateways)
|
||||||
|
|
||||||
workerCtx, workerCancel := context.WithCancel(context.Background())
|
workerCtx, workerCancel := context.WithCancel(context.Background())
|
||||||
defer workerCancel()
|
defer workerCancel()
|
||||||
@ -51,15 +55,17 @@ func main() {
|
|||||||
lingxian.NewAppProvider(lingxianService),
|
lingxian.NewAppProvider(lingxianService),
|
||||||
)
|
)
|
||||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||||
Invite: inviteService,
|
Invite: inviteService,
|
||||||
Baishun: baishunService,
|
Baishun: baishunService,
|
||||||
Lingxian: lingxianService,
|
Lingxian: lingxianService,
|
||||||
GameOpen: gameOpenService,
|
GameOpen: gameOpenService,
|
||||||
GameProviders: gameProviders,
|
GameProviders: gameProviders,
|
||||||
LuckyGift: luckyGiftService,
|
LuckyGift: luckyGiftService,
|
||||||
RegisterReward: registerRewardService,
|
HostCenter: hostCenterService,
|
||||||
SignInReward: signInRewardService,
|
RegisterReward: registerRewardService,
|
||||||
WeekStar: weekStarService,
|
RoomTurnoverReward: roomTurnoverRewardService,
|
||||||
|
SignInReward: signInRewardService,
|
||||||
|
WeekStar: weekStarService,
|
||||||
})
|
})
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: app.Config.HTTP.ListenAddr,
|
Addr: app.Config.HTTP.ListenAddr,
|
||||||
|
|||||||
@ -10,16 +10,17 @@ import (
|
|||||||
|
|
||||||
// Config 是按运行层和业务模块分段后的应用配置。
|
// Config 是按运行层和业务模块分段后的应用配置。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HTTP HTTPConfig
|
HTTP HTTPConfig
|
||||||
Store StoreConfig
|
Store StoreConfig
|
||||||
Java JavaConfig
|
Java JavaConfig
|
||||||
Worker WorkerConfig
|
Worker WorkerConfig
|
||||||
Invite InviteConfig
|
Invite InviteConfig
|
||||||
WeekStar WeekStarConfig
|
WeekStar WeekStarConfig
|
||||||
Baishun BaishunConfig
|
RoomTurnoverReward RoomTurnoverRewardConfig
|
||||||
Lingxian LingxianConfig
|
Baishun BaishunConfig
|
||||||
GameOpen GameOpenConfig
|
Lingxian LingxianConfig
|
||||||
LuckyGift LuckyGiftConfig
|
GameOpen GameOpenConfig
|
||||||
|
LuckyGift LuckyGiftConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig 保存 HTTP 服务运行配置。
|
// HTTPConfig 保存 HTTP 服务运行配置。
|
||||||
@ -92,6 +93,13 @@ type WeekStarRocketMQConfig struct {
|
|||||||
Tag string
|
Tag string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardConfig 保存房间流水奖励模块配置。
|
||||||
|
type RoomTurnoverRewardConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
Timezone string
|
||||||
|
CycleDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// BaishunConfig 保存百顺模块配置。
|
// BaishunConfig 保存百顺模块配置。
|
||||||
type BaishunConfig struct {
|
type BaishunConfig struct {
|
||||||
PlatformBaseURL string
|
PlatformBaseURL string
|
||||||
@ -194,6 +202,20 @@ func Load() Config {
|
|||||||
Tag: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_TAG", "WEEK_STAR_ROCKETMQ_TAG"}, "give_gift_v3"),
|
Tag: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_TAG", "WEEK_STAR_ROCKETMQ_TAG"}, "give_gift_v3"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
RoomTurnoverReward: RoomTurnoverRewardConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_ROOM_TURNOVER_REWARD_DEFAULT_SYS_ORIGIN", "ROOM_TURNOVER_REWARD_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
Timezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_ROOM_TURNOVER_REWARD_TIMEZONE", "ROOM_TURNOVER_REWARD_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||||
|
"Asia/Riyadh",
|
||||||
|
),
|
||||||
|
CycleDuration: time.Duration(getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_ROOM_TURNOVER_REWARD_CYCLE_SECONDS", "ROOM_TURNOVER_REWARD_CYCLE_SECONDS"},
|
||||||
|
10,
|
||||||
|
)) * time.Second,
|
||||||
|
},
|
||||||
Baishun: BaishunConfig{
|
Baishun: BaishunConfig{
|
||||||
PlatformBaseURL: getEnvAny([]string{"CHATAPP_BAISHUN_PLATFORM_BASE_URL", "BAISHUN_PLATFORM_BASE_URL"}, ""),
|
PlatformBaseURL: getEnvAny([]string{"CHATAPP_BAISHUN_PLATFORM_BASE_URL", "BAISHUN_PLATFORM_BASE_URL"}, ""),
|
||||||
GameListURL: getEnvAny([]string{"CHATAPP_BAISHUN_GAME_LIST_URL", "BAISHUN_GAME_LIST_URL"}, ""),
|
GameListURL: getEnvAny([]string{"CHATAPP_BAISHUN_GAME_LIST_URL", "BAISHUN_GAME_LIST_URL"}, ""),
|
||||||
|
|||||||
@ -15,6 +15,8 @@ type Gateways struct {
|
|||||||
RewardDispatch *RewardDispatchGateway
|
RewardDispatch *RewardDispatchGateway
|
||||||
RewardQuery *RewardQueryGateway
|
RewardQuery *RewardQueryGateway
|
||||||
Profile *ProfileGateway
|
Profile *ProfileGateway
|
||||||
|
Room *RoomGateway
|
||||||
|
Team *TeamGateway
|
||||||
Wallet *WalletGateway
|
Wallet *WalletGateway
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,6 +31,8 @@ func NewGateways(cfg config.Config) Gateways {
|
|||||||
RewardDispatch: &RewardDispatchGateway{client: client},
|
RewardDispatch: &RewardDispatchGateway{client: client},
|
||||||
RewardQuery: &RewardQueryGateway{client: client},
|
RewardQuery: &RewardQueryGateway{client: client},
|
||||||
Profile: &ProfileGateway{client: client},
|
Profile: &ProfileGateway{client: client},
|
||||||
|
Room: &RoomGateway{client: client},
|
||||||
|
Team: &TeamGateway{client: client},
|
||||||
Wallet: &WalletGateway{client: client},
|
Wallet: &WalletGateway{client: client},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,6 +92,11 @@ func (g *Gateways) GetUserProfile(ctx context.Context, userID int64) (UserProfil
|
|||||||
return g.Profile.GetUserProfile(ctx, userID)
|
return g.Profile.GetUserProfile(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserRegion 透传到用户地区网关。
|
||||||
|
func (g *Gateways) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
|
||||||
|
return g.Profile.GetUserRegion(ctx, userID, authorization)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserProfileByAccount 透传到资料网关。
|
// GetUserProfileByAccount 透传到资料网关。
|
||||||
func (g *Gateways) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
func (g *Gateways) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
||||||
return g.Profile.GetUserProfileByAccount(ctx, sysOrigin, account)
|
return g.Profile.GetUserProfileByAccount(ctx, sysOrigin, account)
|
||||||
@ -103,6 +112,31 @@ func (g *Gateways) GetUserLanguage(ctx context.Context, userID int64) (string, e
|
|||||||
return g.Profile.GetUserLanguage(ctx, userID)
|
return g.Profile.GetUserLanguage(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListCurrentWeekRoomContribution 透传到房间流水网关。
|
||||||
|
func (g *Gateways) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
||||||
|
return g.Room.ListCurrentWeekRoomContribution(ctx, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentWeekRoomContribution 透传到房间流水网关。
|
||||||
|
func (g *Gateways) GetCurrentWeekRoomContribution(ctx context.Context, roomID int64) (RoomContributionActivityCount, error) {
|
||||||
|
return g.Room.GetCurrentWeekRoomContribution(ctx, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoomProfileByUserID 透传到房间资料网关。
|
||||||
|
func (g *Gateways) GetRoomProfileByUserID(ctx context.Context, userID int64) (RoomProfile, error) {
|
||||||
|
return g.Room.GetRoomProfileByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapRoomProfiles 透传到房间资料网关。
|
||||||
|
func (g *Gateways) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]RoomProfile, error) {
|
||||||
|
return g.Room.MapRoomProfiles(ctx, roomIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamPolicyRelease 透传到团队政策网关。
|
||||||
|
func (g *Gateways) GetTeamPolicyRelease(ctx context.Context, query TeamPolicyReleaseQuery) (TeamPolicyRelease, error) {
|
||||||
|
return g.Team.GetTeamPolicyRelease(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
// MapGoldBalance 透传到钱包网关。
|
// MapGoldBalance 透传到钱包网关。
|
||||||
func (g *Gateways) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
func (g *Gateways) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
||||||
return g.Wallet.MapGoldBalance(ctx, userIDs)
|
return g.Wallet.MapGoldBalance(ctx, userIDs)
|
||||||
@ -208,6 +242,11 @@ func (g *ProfileGateway) GetUserProfile(ctx context.Context, userID int64) (User
|
|||||||
return g.client.GetUserProfile(ctx, userID)
|
return g.client.GetUserProfile(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserRegion 查询用户当前地区。
|
||||||
|
func (g *ProfileGateway) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
|
||||||
|
return g.client.GetUserRegion(ctx, userID, authorization)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserProfileByAccount 根据账号查询用户资料。
|
// GetUserProfileByAccount 根据账号查询用户资料。
|
||||||
func (g *ProfileGateway) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
func (g *ProfileGateway) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
||||||
return g.client.GetUserProfileByAccount(ctx, sysOrigin, account)
|
return g.client.GetUserProfileByAccount(ctx, sysOrigin, account)
|
||||||
@ -223,6 +262,41 @@ func (g *ProfileGateway) GetUserLanguage(ctx context.Context, userID int64) (str
|
|||||||
return g.client.GetUserLanguage(ctx, userID)
|
return g.client.GetUserLanguage(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoomGateway 负责房间资料和房间流水相关接口。
|
||||||
|
type RoomGateway struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCurrentWeekRoomContribution 查询当前周房间流水排行榜。
|
||||||
|
func (g *RoomGateway) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
||||||
|
return g.client.ListCurrentWeekRoomContribution(ctx, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentWeekRoomContribution 查询指定房间当前周流水。
|
||||||
|
func (g *RoomGateway) GetCurrentWeekRoomContribution(ctx context.Context, roomID int64) (RoomContributionActivityCount, error) {
|
||||||
|
return g.client.GetCurrentWeekRoomContribution(ctx, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoomProfileByUserID 查询用户当前房间资料。
|
||||||
|
func (g *RoomGateway) GetRoomProfileByUserID(ctx context.Context, userID int64) (RoomProfile, error) {
|
||||||
|
return g.client.GetRoomProfileByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapRoomProfiles 批量查询房间资料。
|
||||||
|
func (g *RoomGateway) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]RoomProfile, error) {
|
||||||
|
return g.client.MapRoomProfiles(ctx, roomIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamGateway 负责团队政策相关接口。
|
||||||
|
type TeamGateway struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamPolicyRelease 查询已发布团队政策。
|
||||||
|
func (g *TeamGateway) GetTeamPolicyRelease(ctx context.Context, query TeamPolicyReleaseQuery) (TeamPolicyRelease, error) {
|
||||||
|
return g.client.GetTeamPolicyRelease(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
// WalletGateway 负责钱包相关接口。
|
// WalletGateway 负责钱包相关接口。
|
||||||
type WalletGateway struct {
|
type WalletGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
|
|||||||
@ -41,6 +41,36 @@ type UserProfile struct {
|
|||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
UserAvatar string `json:"userAvatar"`
|
UserAvatar string `json:"userAvatar"`
|
||||||
UserNickname string `json:"userNickname"`
|
UserNickname string `json:"userNickname"`
|
||||||
|
CountryID Int64Value `json:"countryId"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
CountryName string `json:"countryName"`
|
||||||
|
RegionCode string `json:"regionCode"`
|
||||||
|
OriginSys string `json:"originSys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRegion struct {
|
||||||
|
RegionID string `json:"regionId"`
|
||||||
|
RegionCode string `json:"regionCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomContributionActivityCount struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
RoomID Int64Value `json:"roomId"`
|
||||||
|
ContributionValue AmountValue `json:"contributionValue"`
|
||||||
|
DateNumber int `json:"dateNumber"`
|
||||||
|
Received bool `json:"received"`
|
||||||
|
RewardCoinsSent bool `json:"rewardCoinsSent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomProfile struct {
|
||||||
|
ID Int64Value `json:"id"`
|
||||||
|
RoomAccount string `json:"roomAccount"`
|
||||||
|
UserID Int64Value `json:"userId"`
|
||||||
|
RoomCover string `json:"roomCover"`
|
||||||
|
RoomName string `json:"roomName"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
CountryName string `json:"countryName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GoldReceiptCommand struct {
|
type GoldReceiptCommand struct {
|
||||||
@ -104,6 +134,37 @@ type SendActivityRewardRequest struct {
|
|||||||
AcceptUserID int64 `json:"acceptUserId"`
|
AcceptUserID int64 `json:"acceptUserId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamPolicyReleaseQuery struct {
|
||||||
|
SysOrigin string
|
||||||
|
Region string
|
||||||
|
PolicyType string
|
||||||
|
CountryCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamPolicyRelease struct {
|
||||||
|
ID Int64Value `json:"id"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
HistoryRelease bool `json:"historyRelease"`
|
||||||
|
Release bool `json:"release"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Policy []TeamPolicyItem `json:"policy"`
|
||||||
|
PolicyType string `json:"policyType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamPolicyItem struct {
|
||||||
|
Level int `json:"level"`
|
||||||
|
OnlineTime Int64Value `json:"onlineTime"`
|
||||||
|
EffectiveDay int `json:"effectiveDay"`
|
||||||
|
Target Int64Value `json:"target"`
|
||||||
|
MemberSalary DecimalString `json:"memberSalary"`
|
||||||
|
OwnSalary DecimalString `json:"ownSalary"`
|
||||||
|
TotalSalary DecimalString `json:"totalSalary"`
|
||||||
|
PropsRewards any `json:"propsRewards"`
|
||||||
|
}
|
||||||
|
|
||||||
// OfficialNoticeCustomizeRequest 表示发送自定义官方通知的请求体。
|
// OfficialNoticeCustomizeRequest 表示发送自定义官方通知的请求体。
|
||||||
type OfficialNoticeCustomizeRequest struct {
|
type OfficialNoticeCustomizeRequest struct {
|
||||||
ToAccounts []int64 `json:"toAccounts"`
|
ToAccounts []int64 `json:"toAccounts"`
|
||||||
@ -157,6 +218,57 @@ func (v *Int64Value) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AmountValue int64
|
||||||
|
|
||||||
|
func (v *AmountValue) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := strings.TrimSpace(string(data))
|
||||||
|
if raw == "" || raw == "null" {
|
||||||
|
*v = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
|
||||||
|
raw = strings.Trim(raw, "\"")
|
||||||
|
}
|
||||||
|
if raw == "" {
|
||||||
|
*v = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
floatRaw := raw
|
||||||
|
if dot := strings.Index(raw, "."); dot >= 0 {
|
||||||
|
raw = raw[:dot]
|
||||||
|
}
|
||||||
|
if parsed, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||||
|
*v = AmountValue(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseFloat(floatRaw, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*v = AmountValue(int64(parsed))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecimalString string
|
||||||
|
|
||||||
|
func (v *DecimalString) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := strings.TrimSpace(string(data))
|
||||||
|
if raw == "" || raw == "null" {
|
||||||
|
*v = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
|
||||||
|
var parsed string
|
||||||
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*v = DecimalString(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*v = DecimalString(raw)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func New(cfg config.Config) *Client {
|
func New(cfg config.Config) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@ -383,6 +495,22 @@ func (c *Client) GetUserProfile(ctx context.Context, userID int64) (UserProfile,
|
|||||||
return resp.Body, nil
|
return resp.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
|
||||||
|
if userID <= 0 {
|
||||||
|
return UserRegion{}, fmt.Errorf("userId is required")
|
||||||
|
}
|
||||||
|
endpoint := c.cfg.Java.AppBaseURL + "/user/user-region?userId=" + url.QueryEscape(strconv.FormatInt(userID, 10))
|
||||||
|
headers := javaAppHeadersFromAuthorization(authorization)
|
||||||
|
if strings.TrimSpace(authorization) != "" {
|
||||||
|
headers.Set("Authorization", authorization)
|
||||||
|
}
|
||||||
|
var resp resultResponse[UserRegion]
|
||||||
|
if err := c.getJSON(ctx, endpoint, headers, &resp); err != nil {
|
||||||
|
return UserRegion{}, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
func (c *Client) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
||||||
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccount?sysOrigin=" +
|
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccount?sysOrigin=" +
|
||||||
url.QueryEscape(strings.TrimSpace(sysOrigin)) + "&account=" + url.QueryEscape(strings.TrimSpace(account))
|
url.QueryEscape(strings.TrimSpace(sysOrigin)) + "&account=" + url.QueryEscape(strings.TrimSpace(account))
|
||||||
@ -409,6 +537,82 @@ func (c *Client) GetUserProfileByAccountOne(ctx context.Context, account string)
|
|||||||
return resp.Body, nil
|
return resp.Body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTeamPolicyRelease(ctx context.Context, query TeamPolicyReleaseQuery) (TeamPolicyRelease, error) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("sysOrigin", strings.TrimSpace(query.SysOrigin))
|
||||||
|
values.Set("region", strings.TrimSpace(query.Region))
|
||||||
|
values.Set("policyType", strings.TrimSpace(query.PolicyType))
|
||||||
|
values.Set("countryCode", strings.TrimSpace(query.CountryCode))
|
||||||
|
endpoint := c.cfg.Java.OtherBaseURL + "/team-policy/client/getReleaseByRegionAndTypeAndPolicyType?" + values.Encode()
|
||||||
|
var resp resultResponse[TeamPolicyRelease]
|
||||||
|
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
||||||
|
return TeamPolicyRelease{}, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
endpoint := c.cfg.Java.OtherBaseURL + "/room-contribution-activity-count/client/listCurrentWeek?limit=" +
|
||||||
|
url.QueryEscape(strconv.Itoa(limit))
|
||||||
|
var resp resultResponse[[]RoomContributionActivityCount]
|
||||||
|
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetCurrentWeekRoomContribution(ctx context.Context, roomID int64) (RoomContributionActivityCount, error) {
|
||||||
|
if roomID <= 0 {
|
||||||
|
return RoomContributionActivityCount{}, nil
|
||||||
|
}
|
||||||
|
endpoint := c.cfg.Java.OtherBaseURL + "/room-contribution-activity-count/client/currentWeek?roomId=" +
|
||||||
|
url.QueryEscape(strconv.FormatInt(roomID, 10))
|
||||||
|
var resp resultResponse[RoomContributionActivityCount]
|
||||||
|
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
||||||
|
return RoomContributionActivityCount{}, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetRoomProfileByUserID(ctx context.Context, userID int64) (RoomProfile, error) {
|
||||||
|
if userID <= 0 {
|
||||||
|
return RoomProfile{}, nil
|
||||||
|
}
|
||||||
|
endpoint := c.cfg.Java.OtherBaseURL + "/room-manager/client/getProfileByUserId?userId=" +
|
||||||
|
url.QueryEscape(strconv.FormatInt(userID, 10))
|
||||||
|
var resp resultResponse[RoomProfile]
|
||||||
|
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
||||||
|
return RoomProfile{}, err
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]RoomProfile, error) {
|
||||||
|
if len(roomIDs) == 0 {
|
||||||
|
return map[int64]RoomProfile{}, nil
|
||||||
|
}
|
||||||
|
endpoint := c.cfg.Java.OtherBaseURL + "/room-manager/client/mapProfileByRoomIds"
|
||||||
|
var resp resultResponse[map[string]RoomProfile]
|
||||||
|
if err := c.postJSON(ctx, endpoint, roomIDs, nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make(map[int64]RoomProfile, len(resp.Body))
|
||||||
|
for key, value := range resp.Body {
|
||||||
|
roomID, err := strconv.ParseInt(strings.TrimSpace(key), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
roomID = int64(value.ID)
|
||||||
|
}
|
||||||
|
if roomID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[roomID] = value
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) rewardBridgeBaseURL() string {
|
func (c *Client) rewardBridgeBaseURL() string {
|
||||||
if baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/"); baseURL != "" {
|
if baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/"); baseURL != "" {
|
||||||
return baseURL
|
return baseURL
|
||||||
|
|||||||
59
internal/model/room_turnover_reward_models.go
Normal file
59
internal/model/room_turnover_reward_models.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RoomTurnoverRewardConfig) TableName() string { return "room_turnover_reward_config" }
|
||||||
|
|
||||||
|
// RoomTurnoverRewardLevel 保存达到某个流水门槛后发放的金币奖励。
|
||||||
|
type RoomTurnoverRewardLevel struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;uniqueIndex:uk_room_turnover_reward_level,priority:1;uniqueIndex:uk_room_turnover_reward_threshold,priority:1;index:idx_room_turnover_reward_level_config"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_room_turnover_reward_level,priority:2"`
|
||||||
|
TurnoverThreshold int64 `gorm:"column:turnover_threshold;uniqueIndex:uk_room_turnover_reward_threshold,priority:2"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RoomTurnoverRewardLevel) TableName() string { return "room_turnover_reward_level" }
|
||||||
|
|
||||||
|
// RoomTurnoverRewardRecord 记录每周给房主发放的最高档房间流水奖励。
|
||||||
|
type RoomTurnoverRewardRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_room_turnover_reward_record_config"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_room_turnover_reward_record_room,priority:1;index:idx_room_turnover_reward_record_cycle,priority:1"`
|
||||||
|
CycleKey string `gorm:"column:cycle_key;size:32;uniqueIndex:uk_room_turnover_reward_record_room,priority:2;index:idx_room_turnover_reward_record_cycle,priority:2"`
|
||||||
|
PeriodStartAt time.Time `gorm:"column:period_start_at"`
|
||||||
|
PeriodEndAt time.Time `gorm:"column:period_end_at"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;uniqueIndex:uk_room_turnover_reward_record_room,priority:3;index:idx_room_turnover_reward_record_room"`
|
||||||
|
RoomAccount string `gorm:"column:room_account;size:64"`
|
||||||
|
RoomName string `gorm:"column:room_name;size:255"`
|
||||||
|
RoomCover string `gorm:"column:room_cover;size:1024"`
|
||||||
|
OwnerUserID int64 `gorm:"column:owner_user_id;index:idx_room_turnover_reward_record_owner"`
|
||||||
|
CountryCode string `gorm:"column:country_code;size:32"`
|
||||||
|
CountryName string `gorm:"column:country_name;size:128"`
|
||||||
|
TurnoverAmount int64 `gorm:"column:turnover_amount"`
|
||||||
|
Level int `gorm:"column:level"`
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
SentAt *time.Time `gorm:"column:sent_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RoomTurnoverRewardRecord) TableName() string { return "room_turnover_reward_record" }
|
||||||
55
internal/router/host_center_routes.go
Normal file
55
internal/router/host_center_routes.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerHostCenterRoutes(engine *gin.Engine, service *hostcenter.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
group := engine.Group("/app/h5/host-center")
|
||||||
|
group.GET("/policy", func(c *gin.Context) {
|
||||||
|
token := strings.TrimSpace(queryValuePreservePlus(c.Request.URL.RawQuery, "token"))
|
||||||
|
if token == "" {
|
||||||
|
writeError(c, hostcenter.NewAppError(http.StatusUnauthorized, "missing_token", "token is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.GetAnchorPolicy(c.Request.Context(), hostcenter.AnchorPolicyRequest{
|
||||||
|
Token: token,
|
||||||
|
PolicyType: c.Query("policyType"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryValuePreservePlus(rawQuery, name string) string {
|
||||||
|
for _, part := range strings.Split(rawQuery, "&") {
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, value, _ := strings.Cut(part, "=")
|
||||||
|
decodedKey, err := url.QueryUnescape(key)
|
||||||
|
if err != nil || decodedKey != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decodedValue, err := url.QueryUnescape(strings.ReplaceAll(value, "+", "%2B"))
|
||||||
|
if err != nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return decodedValue
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
10
internal/router/host_center_routes_test.go
Normal file
10
internal/router/host_center_routes_test.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestQueryValuePreservePlusKeepsTokenBase64Plus(t *testing.T) {
|
||||||
|
got := queryValuePreservePlus("lang=en&token=abc+def%2Bghi", "token")
|
||||||
|
if got != "abc+def+ghi" {
|
||||||
|
t.Fatalf("token = %q, want abc+def+ghi", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
92
internal/router/room_turnover_reward_routes.go
Normal file
92
internal/router/room_turnover_reward_routes.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerRoomTurnoverRewardRoutes 注册房间流水奖励后台配置和发奖记录路由。
|
||||||
|
func registerRoomTurnoverRewardRoutes(engine *gin.Engine, javaClient authGateway, service *roomturnoverreward.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.GET("/public/h5/room-turnover-reward/page", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetPublicPage(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/h5/room-turnover-reward")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/home", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group := engine.Group("/resident-activity/room-turnover-reward")
|
||||||
|
group.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
|
||||||
|
group.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req roomturnoverreward.SaveRoomTurnoverRewardConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, roomturnoverreward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.GET("/reward-record/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageRewardRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("cycleKey"),
|
||||||
|
c.Query("status"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.GET("/current-rank", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.CurrentRoomRanking(c.Request.Context(), c.Query("sysOrigin"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -9,10 +9,12 @@ import (
|
|||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/lingxian"
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
|
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||||
"chatapp3-golang/internal/service/signinreward"
|
"chatapp3-golang/internal/service/signinreward"
|
||||||
"chatapp3-golang/internal/service/weekstar"
|
"chatapp3-golang/internal/service/weekstar"
|
||||||
|
|
||||||
@ -21,15 +23,17 @@ import (
|
|||||||
|
|
||||||
// Services 聚合所有业务模块服务,供路由层统一装配。
|
// Services 聚合所有业务模块服务,供路由层统一装配。
|
||||||
type Services struct {
|
type Services struct {
|
||||||
Invite *invite.InviteService
|
Invite *invite.InviteService
|
||||||
Baishun *baishun.BaishunService
|
Baishun *baishun.BaishunService
|
||||||
Lingxian *lingxian.Service
|
Lingxian *lingxian.Service
|
||||||
GameOpen *gameopen.GameOpenService
|
GameOpen *gameopen.GameOpenService
|
||||||
GameProviders *gameprovider.Registry
|
GameProviders *gameprovider.Registry
|
||||||
LuckyGift *luckygift.LuckyGiftService
|
LuckyGift *luckygift.LuckyGiftService
|
||||||
RegisterReward *registerreward.RegisterRewardService
|
HostCenter *hostcenter.Service
|
||||||
SignInReward *signinreward.SignInRewardService
|
RegisterReward *registerreward.RegisterRewardService
|
||||||
WeekStar *weekstar.WeekStarService
|
RoomTurnoverReward *roomturnoverreward.Service
|
||||||
|
SignInReward *signinreward.SignInRewardService
|
||||||
|
WeekStar *weekstar.WeekStarService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter 创建 Gin 路由,并按模块注册所有接口。
|
// NewRouter 创建 Gin 路由,并按模块注册所有接口。
|
||||||
@ -40,13 +44,15 @@ func NewRouter(
|
|||||||
services Services,
|
services Services,
|
||||||
) *gin.Engine {
|
) *gin.Engine {
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(gin.Logger(), gin.Recovery())
|
engine.Use(gin.Logger(), gin.Recovery(), corsMiddleware())
|
||||||
|
|
||||||
registerHealthRoutes(engine, cfg, repository)
|
registerHealthRoutes(engine, cfg, repository)
|
||||||
registerInviteRoutes(engine, javaClient, services.Invite)
|
registerInviteRoutes(engine, javaClient, services.Invite)
|
||||||
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
||||||
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
||||||
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
||||||
|
registerRoomTurnoverRewardRoutes(engine, javaClient, services.RoomTurnoverReward)
|
||||||
|
registerHostCenterRoutes(engine, services.HostCenter)
|
||||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
||||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
||||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||||
@ -56,6 +62,28 @@ func NewRouter(
|
|||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func corsMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
origin := c.GetHeader("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
origin = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Authorization,Content-Type,Origin,Accept,req-lang,req-client,req-imei,req-app-intel,req-sys-origin,req-zone,X-Requested-With")
|
||||||
|
c.Header("Access-Control-Expose-Headers", "Authorization")
|
||||||
|
c.Header("Access-Control-Max-Age", "86400")
|
||||||
|
|
||||||
|
if c.Request.Method == http.MethodOptions {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// registerHealthRoutes 注册健康检查接口。
|
// registerHealthRoutes 注册健康检查接口。
|
||||||
func registerHealthRoutes(engine *gin.Engine, cfg config.Config, repository *repo.Repository) {
|
func registerHealthRoutes(engine *gin.Engine, cfg config.Config, repository *repo.Repository) {
|
||||||
engine.GET("/health", func(c *gin.Context) {
|
engine.GET("/health", func(c *gin.Context) {
|
||||||
|
|||||||
7
internal/service/hostcenter/aliases.go
Normal file
7
internal/service/hostcenter/aliases.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package hostcenter
|
||||||
|
|
||||||
|
import "chatapp3-golang/internal/common"
|
||||||
|
|
||||||
|
type AppError = common.AppError
|
||||||
|
|
||||||
|
var NewAppError = common.NewAppError
|
||||||
127
internal/service/hostcenter/policy.go
Normal file
127
internal/service/hostcenter/policy.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package hostcenter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) GetAnchorPolicy(ctx context.Context, req AnchorPolicyRequest) (*AnchorPolicyResponse, error) {
|
||||||
|
if s.java == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "java_gateway_unavailable", "java gateway is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := trimBearer(req.Token)
|
||||||
|
if token == "" {
|
||||||
|
return nil, NewAppError(http.StatusUnauthorized, "missing_token", "token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
credential, err := s.java.AuthenticateToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewAppError(http.StatusUnauthorized, "invalid_token", err.Error())
|
||||||
|
}
|
||||||
|
userID := int64(credential.UserID)
|
||||||
|
if userID <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusUnauthorized, "invalid_token", "token user is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
authorization := "Bearer " + token
|
||||||
|
profile, _ := s.java.GetUserProfile(ctx, userID)
|
||||||
|
userRegion, err := s.java.GetUserRegion(ctx, userID, authorization)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
region := strings.TrimSpace(userRegion.RegionID)
|
||||||
|
if region == "" {
|
||||||
|
region = strings.TrimSpace(userRegion.RegionCode)
|
||||||
|
}
|
||||||
|
if region == "" {
|
||||||
|
region = strings.TrimSpace(profile.RegionCode)
|
||||||
|
}
|
||||||
|
if region == "" {
|
||||||
|
return nil, NewAppError(http.StatusNotFound, "user_region_not_found", "user region is not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
sysOrigin := normalizeSysOrigin(credential.SysOrigin, profile.OriginSys)
|
||||||
|
policyType := normalizePolicyType(req.PolicyType)
|
||||||
|
countryCode := strings.TrimSpace(profile.CountryCode)
|
||||||
|
|
||||||
|
release, err := s.loadReleasePolicy(ctx, sysOrigin, region, policyType, countryCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &AnchorPolicyResponse{
|
||||||
|
UserID: userID,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Region: region,
|
||||||
|
RegionCode: strings.TrimSpace(userRegion.RegionCode),
|
||||||
|
CountryCode: countryCode,
|
||||||
|
PolicyType: policyType,
|
||||||
|
Title: strings.TrimSpace(release.Title),
|
||||||
|
Release: release.Release,
|
||||||
|
Policy: release.Policy,
|
||||||
|
}
|
||||||
|
if resp.Title == "" {
|
||||||
|
resp.Title = "主播政策"
|
||||||
|
}
|
||||||
|
resp.Empty = int64(release.ID) == 0 && len(release.Policy) == 0
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadReleasePolicy(ctx context.Context, sysOrigin, region, policyType, countryCode string) (integration.TeamPolicyRelease, error) {
|
||||||
|
query := integration.TeamPolicyReleaseQuery{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Region: region,
|
||||||
|
PolicyType: javaPolicyType(policyType),
|
||||||
|
CountryCode: countryCode,
|
||||||
|
}
|
||||||
|
release, err := s.java.GetTeamPolicyRelease(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return integration.TeamPolicyRelease{}, err
|
||||||
|
}
|
||||||
|
if !isEmptyRelease(release) || strings.TrimSpace(countryCode) == "" {
|
||||||
|
return release, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query.CountryCode = ""
|
||||||
|
return s.java.GetTeamPolicyRelease(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSysOrigin(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if trimmed := strings.ToUpper(strings.TrimSpace(value)); trimmed != "" {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultSysOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePolicyType(policyType string) string {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(policyType), diamondPolicyType) {
|
||||||
|
return diamondPolicyType
|
||||||
|
}
|
||||||
|
return defaultPolicyType
|
||||||
|
}
|
||||||
|
|
||||||
|
func javaPolicyType(policyType string) string {
|
||||||
|
if policyType == diamondPolicyType {
|
||||||
|
return diamondPolicyType
|
||||||
|
}
|
||||||
|
return defaultPolicyType
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmptyRelease(release integration.TeamPolicyRelease) bool {
|
||||||
|
return int64(release.ID) == 0 && strings.TrimSpace(release.Title) == "" && len(release.Policy) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimBearer(token string) string {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if len(token) >= 7 && strings.EqualFold(token[:7], "Bearer ") {
|
||||||
|
return strings.TrimSpace(token[7:])
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
123
internal/service/hostcenter/policy_test.go
Normal file
123
internal/service/hostcenter/policy_test.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package hostcenter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubHostCenterJavaGateway struct {
|
||||||
|
credential integration.UserCredential
|
||||||
|
profile integration.UserProfile
|
||||||
|
region integration.UserRegion
|
||||||
|
releases []integration.TeamPolicyRelease
|
||||||
|
queries []integration.TeamPolicyReleaseQuery
|
||||||
|
authErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubHostCenterJavaGateway) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||||
|
if s.authErr != nil {
|
||||||
|
return integration.UserCredential{}, s.authErr
|
||||||
|
}
|
||||||
|
return s.credential, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubHostCenterJavaGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) {
|
||||||
|
return s.profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubHostCenterJavaGateway) GetUserRegion(context.Context, int64, string) (integration.UserRegion, error) {
|
||||||
|
return s.region, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubHostCenterJavaGateway) GetTeamPolicyRelease(_ context.Context, query integration.TeamPolicyReleaseQuery) (integration.TeamPolicyRelease, error) {
|
||||||
|
s.queries = append(s.queries, query)
|
||||||
|
if len(s.releases) == 0 {
|
||||||
|
return integration.TeamPolicyRelease{}, nil
|
||||||
|
}
|
||||||
|
release := s.releases[0]
|
||||||
|
s.releases = s.releases[1:]
|
||||||
|
return release, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAnchorPolicyUsesTokenRegionAndCountryPolicy(t *testing.T) {
|
||||||
|
java := &stubHostCenterJavaGateway{
|
||||||
|
credential: integration.UserCredential{UserID: integration.Int64Value(1001), SysOrigin: "LIKEI"},
|
||||||
|
profile: integration.UserProfile{CountryCode: "AE"},
|
||||||
|
region: integration.UserRegion{RegionID: "MENA", RegionCode: "MENA"},
|
||||||
|
releases: []integration.TeamPolicyRelease{{
|
||||||
|
ID: integration.Int64Value(99),
|
||||||
|
Release: true,
|
||||||
|
Title: "MENA Policy",
|
||||||
|
Policy: []integration.TeamPolicyItem{{
|
||||||
|
Level: 1,
|
||||||
|
OnlineTime: integration.Int64Value(2),
|
||||||
|
EffectiveDay: 5,
|
||||||
|
Target: integration.Int64Value(10000),
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
service := NewService(config.Config{}, java)
|
||||||
|
|
||||||
|
resp, err := service.GetAnchorPolicy(context.Background(), AnchorPolicyRequest{Token: "token-value"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetAnchorPolicy() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.UserID != 1001 || resp.Region != "MENA" || resp.CountryCode != "AE" || resp.PolicyType != defaultPolicyType {
|
||||||
|
t.Fatalf("unexpected response metadata: %+v", resp)
|
||||||
|
}
|
||||||
|
if len(resp.Policy) != 1 || resp.Policy[0].Level != 1 {
|
||||||
|
t.Fatalf("unexpected policy rows: %+v", resp.Policy)
|
||||||
|
}
|
||||||
|
if len(java.queries) != 1 {
|
||||||
|
t.Fatalf("query count = %d, want 1", len(java.queries))
|
||||||
|
}
|
||||||
|
if got := java.queries[0]; got.SysOrigin != "LIKEI" || got.Region != "MENA" || got.CountryCode != "AE" || got.PolicyType != defaultPolicyType {
|
||||||
|
t.Fatalf("unexpected query: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAnchorPolicyFallsBackWhenCountryPolicyIsEmpty(t *testing.T) {
|
||||||
|
java := &stubHostCenterJavaGateway{
|
||||||
|
credential: integration.UserCredential{UserID: integration.Int64Value(1001), SysOrigin: "LIKEI"},
|
||||||
|
profile: integration.UserProfile{CountryCode: "AE"},
|
||||||
|
region: integration.UserRegion{RegionID: "MENA"},
|
||||||
|
releases: []integration.TeamPolicyRelease{
|
||||||
|
{},
|
||||||
|
{ID: integration.Int64Value(100), Title: "Default Policy"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
service := NewService(config.Config{}, java)
|
||||||
|
|
||||||
|
resp, err := service.GetAnchorPolicy(context.Background(), AnchorPolicyRequest{Token: "token-value"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetAnchorPolicy() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.Title != "Default Policy" {
|
||||||
|
t.Fatalf("fallback title = %q", resp.Title)
|
||||||
|
}
|
||||||
|
if len(java.queries) != 2 {
|
||||||
|
t.Fatalf("query count = %d, want 2", len(java.queries))
|
||||||
|
}
|
||||||
|
if java.queries[0].CountryCode != "AE" || java.queries[1].CountryCode != "" {
|
||||||
|
t.Fatalf("fallback queries = %+v", java.queries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAnchorPolicyRejectsInvalidToken(t *testing.T) {
|
||||||
|
java := &stubHostCenterJavaGateway{authErr: errors.New("bad token")}
|
||||||
|
service := NewService(config.Config{}, java)
|
||||||
|
|
||||||
|
_, err := service.GetAnchorPolicy(context.Background(), AnchorPolicyRequest{Token: "bad"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
appErr, ok := err.(*AppError)
|
||||||
|
if !ok || appErr.Status != http.StatusUnauthorized || appErr.Code != "invalid_token" {
|
||||||
|
t.Fatalf("unexpected error = %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
internal/service/hostcenter/types.go
Normal file
51
internal/service/hostcenter/types.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package hostcenter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPolicyType = "MONEY"
|
||||||
|
diamondPolicyType = "SALARY_DIAMOND"
|
||||||
|
defaultSysOrigin = "LIKEI"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnchorPolicyRequest struct {
|
||||||
|
Token string
|
||||||
|
PolicyType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnchorPolicyResponse struct {
|
||||||
|
UserID int64 `json:"userId,string"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
RegionCode string `json:"regionCode"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
PolicyType string `json:"policyType"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Release bool `json:"release"`
|
||||||
|
Empty bool `json:"empty"`
|
||||||
|
Policy []integration.TeamPolicyItem `json:"policy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type javaGateway interface {
|
||||||
|
AuthenticateToken(ctx context.Context, token string) (integration.UserCredential, error)
|
||||||
|
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
||||||
|
GetUserRegion(ctx context.Context, userID int64, authorization string) (integration.UserRegion, error)
|
||||||
|
GetTeamPolicyRelease(ctx context.Context, query integration.TeamPolicyReleaseQuery) (integration.TeamPolicyRelease, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
cfg config.Config
|
||||||
|
java javaGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(cfg config.Config, javaGateway javaGateway) *Service {
|
||||||
|
return &Service{
|
||||||
|
cfg: cfg,
|
||||||
|
java: javaGateway,
|
||||||
|
}
|
||||||
|
}
|
||||||
7
internal/service/roomturnoverreward/aliases.go
Normal file
7
internal/service/roomturnoverreward/aliases.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import "chatapp3-golang/internal/common"
|
||||||
|
|
||||||
|
type AppError = common.AppError
|
||||||
|
|
||||||
|
var NewAppError = common.NewAppError
|
||||||
211
internal/service/roomturnoverreward/config.go
Normal file
211
internal/service/roomturnoverreward/config.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/utils"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetConfig 返回系统当前房间流水奖励配置;未配置时返回默认档位草稿。
|
||||||
|
func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*RoomTurnoverRewardConfigResponse, error) {
|
||||||
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
||||||
|
|
||||||
|
var configRow model.RoomTurnoverRewardConfig
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &RoomTurnoverRewardConfigResponse{
|
||||||
|
Configured: false,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Enabled: false,
|
||||||
|
Timezone: s.defaultTimezone(),
|
||||||
|
LevelConfigs: defaultLevelPayloads(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
levels, err := s.loadLevels(ctx, configRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
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),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveConfig 保存系统级房间流水奖励配置;同一系统只保留一份当前配置。
|
||||||
|
func (s *Service) SaveConfig(ctx context.Context, req SaveRoomTurnoverRewardConfigRequest) (*RoomTurnoverRewardConfigResponse, error) {
|
||||||
|
requestID := req.ID.Int64()
|
||||||
|
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
|
||||||
|
timezone := strings.TrimSpace(req.Timezone)
|
||||||
|
if timezone == "" {
|
||||||
|
timezone = s.defaultTimezone()
|
||||||
|
}
|
||||||
|
if _, err := time.LoadLocation(timezone); err != nil {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
levels, err := s.normalizeLevelInputs(req.LevelConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.Enabled {
|
||||||
|
hasEnabledLevel := false
|
||||||
|
for _, level := range levels {
|
||||||
|
if level.Enabled {
|
||||||
|
hasEnabledLevel = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasEnabledLevel {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "enabled config must contain at least one enabled level")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedConfigID int64
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
now := time.Now()
|
||||||
|
var configRow model.RoomTurnoverRewardConfig
|
||||||
|
if requestID > 0 {
|
||||||
|
err := tx.Where("id = ?", requestID).First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return NewAppError(http.StatusNotFound, "room_turnover_reward_config_not_found", "config not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.SysOrigin) == "" {
|
||||||
|
sysOrigin = configRow.SysOrigin
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := tx.Where("sys_origin = ?", sysOrigin).First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
configID, idErr := utils.NextID()
|
||||||
|
if idErr != nil {
|
||||||
|
return idErr
|
||||||
|
}
|
||||||
|
configRow = model.RoomTurnoverRewardConfig{
|
||||||
|
ID: configID,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
CreateTime: now,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configRow.SysOrigin = sysOrigin
|
||||||
|
configRow.Enabled = req.Enabled
|
||||||
|
configRow.Timezone = timezone
|
||||||
|
configRow.UpdateTime = now
|
||||||
|
if configRow.CreateTime.IsZero() {
|
||||||
|
configRow.CreateTime = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Save(&configRow).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.RoomTurnoverRewardLevel{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for index := range levels {
|
||||||
|
levels[index].ConfigID = configRow.ID
|
||||||
|
levels[index].CreateTime = now
|
||||||
|
levels[index].UpdateTime = now
|
||||||
|
}
|
||||||
|
if err := tx.Create(&levels).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
savedConfigID = configRow.ID
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetConfig(ctx, sysOriginBySavedID(ctx, s.repo.DB, savedConfigID, sysOrigin))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) normalizeLevelInputs(inputs []RoomTurnoverRewardLevelInput) ([]model.RoomTurnoverRewardLevel, error) {
|
||||||
|
if len(inputs) == 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "levelConfigs is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
seenLevels := make(map[int]struct{}, len(inputs))
|
||||||
|
seenThresholds := make(map[int64]struct{}, len(inputs))
|
||||||
|
rows := make([]model.RoomTurnoverRewardLevel, 0, len(inputs))
|
||||||
|
for _, item := range inputs {
|
||||||
|
if item.Level <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_level", "level must be greater than 0")
|
||||||
|
}
|
||||||
|
if item.TurnoverThreshold <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_turnover_threshold", "turnoverThreshold must be greater than 0")
|
||||||
|
}
|
||||||
|
if item.RewardGold <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_gold", "rewardGold must be greater than 0")
|
||||||
|
}
|
||||||
|
if _, exists := seenLevels[item.Level]; exists {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "duplicate_level", "levelConfigs must not contain duplicate levels")
|
||||||
|
}
|
||||||
|
if _, exists := seenThresholds[item.TurnoverThreshold]; exists {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "duplicate_turnover_threshold", "levelConfigs must not contain duplicate thresholds")
|
||||||
|
}
|
||||||
|
seenLevels[item.Level] = struct{}{}
|
||||||
|
seenThresholds[item.TurnoverThreshold] = struct{}{}
|
||||||
|
id, err := utils.NextID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows = append(rows, model.RoomTurnoverRewardLevel{
|
||||||
|
ID: id,
|
||||||
|
Level: item.Level,
|
||||||
|
TurnoverThreshold: item.TurnoverThreshold,
|
||||||
|
RewardGold: item.RewardGold,
|
||||||
|
Enabled: item.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
|
return rows[i].TurnoverThreshold < rows[j].TurnoverThreshold
|
||||||
|
})
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadLevels(ctx context.Context, configID int64) ([]model.RoomTurnoverRewardLevel, error) {
|
||||||
|
var levels []model.RoomTurnoverRewardLevel
|
||||||
|
if configID <= 0 {
|
||||||
|
return levels, nil
|
||||||
|
}
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("config_id = ?", configID).
|
||||||
|
Order("level asc").
|
||||||
|
Find(&levels).Error
|
||||||
|
return levels, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sysOriginBySavedID(ctx context.Context, db roomTurnoverRewardDB, configID int64, fallback string) string {
|
||||||
|
if configID <= 0 {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
var row model.RoomTurnoverRewardConfig
|
||||||
|
if err := db.WithContext(ctx).Select("sys_origin").Where("id = ?", configID).First(&row).Error; err != nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return row.SysOrigin
|
||||||
|
}
|
||||||
106
internal/service/roomturnoverreward/config_test.go
Normal file
106
internal/service/roomturnoverreward/config_test.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestService(t *testing.T) (*Service, *gorm.DB) {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(
|
||||||
|
&model.RoomTurnoverRewardConfig{},
|
||||||
|
&model.RoomTurnoverRewardLevel{},
|
||||||
|
&model.RoomTurnoverRewardRecord{},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
service := NewService(config.Config{
|
||||||
|
RoomTurnoverReward: config.RoomTurnoverRewardConfig{
|
||||||
|
DefaultSysOrigin: "LIKEI",
|
||||||
|
Timezone: "Asia/Riyadh",
|
||||||
|
},
|
||||||
|
}, db)
|
||||||
|
return service, db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConfigReturnsDefaultLevelsWhenMissing(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
|
||||||
|
resp, err := service.GetConfig(context.Background(), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.Configured || resp.Enabled {
|
||||||
|
t.Fatalf("GetConfig() configured/enabled = %v/%v, want false/false", resp.Configured, resp.Enabled)
|
||||||
|
}
|
||||||
|
if resp.SysOrigin != "LIKEI" || resp.Timezone != "Asia/Riyadh" {
|
||||||
|
t.Fatalf("GetConfig() sys/timezone = %q/%q", resp.SysOrigin, resp.Timezone)
|
||||||
|
}
|
||||||
|
if len(resp.LevelConfigs) != 4 {
|
||||||
|
t.Fatalf("default level count = %d, want 4", len(resp.LevelConfigs))
|
||||||
|
}
|
||||||
|
if got := resp.LevelConfigs[3]; got.TurnoverThreshold != 5000000 || got.RewardGold != 250000 {
|
||||||
|
t.Fatalf("highest default level = %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveConfigRejectsDuplicateThreshold(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
|
||||||
|
_, err := service.SaveConfig(context.Background(), SaveRoomTurnoverRewardConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "Asia/Riyadh",
|
||||||
|
LevelConfigs: []RoomTurnoverRewardLevelInput{
|
||||||
|
{Level: 1, TurnoverThreshold: 100000, RewardGold: 2000, Enabled: true},
|
||||||
|
{Level: 2, TurnoverThreshold: 100000, RewardGold: 3000, Enabled: true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("SaveConfig() error = nil, want duplicate threshold error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveConfigUpsertsBySysOrigin(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
first, 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("first SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
second, err := service.SaveConfig(ctx, SaveRoomTurnoverRewardConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "Asia/Riyadh",
|
||||||
|
LevelConfigs: []RoomTurnoverRewardLevelInput{
|
||||||
|
{Level: 1, TurnoverThreshold: 200000, RewardGold: 5000, Enabled: true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if first.ID != second.ID {
|
||||||
|
t.Fatalf("config ID changed on upsert: first=%d second=%d", first.ID, second.ID)
|
||||||
|
}
|
||||||
|
if got := second.LevelConfigs[0]; got.TurnoverThreshold != 200000 || got.RewardGold != 5000 {
|
||||||
|
t.Fatalf("upserted level = %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
201
internal/service/roomturnoverreward/current_rank.go
Normal file
201
internal/service/roomturnoverreward/current_rank.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type roomTurnoverRewardRankingLevel struct {
|
||||||
|
Level int
|
||||||
|
TurnoverThreshold int64
|
||||||
|
RewardGold int64
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentRoomRanking 返回当前周期的房间流水排行榜,并根据当前配置计算预计命中的最高奖励档。
|
||||||
|
func (s *Service) CurrentRoomRanking(ctx context.Context, sysOrigin string, limit int) (*RoomTurnoverRewardCurrentRankResponse, error) {
|
||||||
|
if s.repo.Java == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "java_gateway_unavailable", "java gateway is unavailable")
|
||||||
|
}
|
||||||
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
levels, err := s.loadRankingLevels(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().In(s.location)
|
||||||
|
periodStart, periodEnd := s.currentCycleBounds(now)
|
||||||
|
dateNumber, err := s.currentDateNumber(now)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceLimit := limit * 5
|
||||||
|
if sourceLimit < 100 {
|
||||||
|
sourceLimit = 100
|
||||||
|
}
|
||||||
|
if sourceLimit > 500 {
|
||||||
|
sourceLimit = 500
|
||||||
|
}
|
||||||
|
contributions, err := s.repo.Java.ListCurrentWeekRoomContribution(ctx, sourceLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
roomIDs := uniqueRoomIDs(contributions)
|
||||||
|
roomProfiles, err := s.repo.Java.MapRoomProfiles(ctx, roomIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]RoomTurnoverRewardCurrentRankItem, 0, limit)
|
||||||
|
for _, contribution := range contributions {
|
||||||
|
roomID := int64(contribution.RoomID)
|
||||||
|
if roomID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
profile, ok := roomProfiles[roomID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if profileSysOrigin := strings.ToUpper(strings.TrimSpace(profile.SysOrigin)); profileSysOrigin != "" && profileSysOrigin != sysOrigin {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
turnoverAmount := int64(contribution.ContributionValue)
|
||||||
|
item := RoomTurnoverRewardCurrentRankItem{
|
||||||
|
RoomID: roomID,
|
||||||
|
RoomAccount: profile.RoomAccount,
|
||||||
|
RoomName: profile.RoomName,
|
||||||
|
RoomCover: profile.RoomCover,
|
||||||
|
OwnerUserID: int64(profile.UserID),
|
||||||
|
CountryCode: profile.CountryCode,
|
||||||
|
CountryName: profile.CountryName,
|
||||||
|
TurnoverAmount: turnoverAmount,
|
||||||
|
DateNumber: contribution.DateNumber,
|
||||||
|
}
|
||||||
|
if item.DateNumber == 0 {
|
||||||
|
item.DateNumber = dateNumber
|
||||||
|
}
|
||||||
|
if level, ok := pickHighestRankingLevel(levels, turnoverAmount); ok {
|
||||||
|
item.MatchedLevel = level.Level
|
||||||
|
item.TurnoverThreshold = level.TurnoverThreshold
|
||||||
|
item.ExpectedRewardGold = level.RewardGold
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
if items[i].TurnoverAmount == items[j].TurnoverAmount {
|
||||||
|
return items[i].RoomID < items[j].RoomID
|
||||||
|
}
|
||||||
|
return items[i].TurnoverAmount > items[j].TurnoverAmount
|
||||||
|
})
|
||||||
|
if len(items) > limit {
|
||||||
|
items = items[:limit]
|
||||||
|
}
|
||||||
|
for index := range items {
|
||||||
|
items[index].Rank = index + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RoomTurnoverRewardCurrentRankResponse{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Timezone: s.defaultTimezone(),
|
||||||
|
CycleKey: s.cycleKey(periodStart),
|
||||||
|
PeriodStartAt: formatDateTime(periodStart),
|
||||||
|
PeriodEndAt: formatDateTime(periodEnd),
|
||||||
|
DateNumber: dateNumber,
|
||||||
|
Records: items,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadRankingLevels(ctx context.Context, sysOrigin string) ([]roomTurnoverRewardRankingLevel, error) {
|
||||||
|
var configRow model.RoomTurnoverRewardConfig
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return defaultRankingLevels(), nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
levels, err := s.loadLevels(ctx, configRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]roomTurnoverRewardRankingLevel, 0, len(levels))
|
||||||
|
for _, level := range levels {
|
||||||
|
result = append(result, roomTurnoverRewardRankingLevel{
|
||||||
|
Level: level.Level,
|
||||||
|
TurnoverThreshold: level.TurnoverThreshold,
|
||||||
|
RewardGold: level.RewardGold,
|
||||||
|
Enabled: level.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultRankingLevels() []roomTurnoverRewardRankingLevel {
|
||||||
|
defaults := defaultLevelPayloads()
|
||||||
|
result := make([]roomTurnoverRewardRankingLevel, 0, len(defaults))
|
||||||
|
for _, level := range defaults {
|
||||||
|
result = append(result, roomTurnoverRewardRankingLevel{
|
||||||
|
Level: level.Level,
|
||||||
|
TurnoverThreshold: level.TurnoverThreshold,
|
||||||
|
RewardGold: level.RewardGold,
|
||||||
|
Enabled: level.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickHighestRankingLevel(levels []roomTurnoverRewardRankingLevel, turnoverAmount int64) (roomTurnoverRewardRankingLevel, bool) {
|
||||||
|
if turnoverAmount <= 0 || len(levels) == 0 {
|
||||||
|
return roomTurnoverRewardRankingLevel{}, false
|
||||||
|
}
|
||||||
|
sort.Slice(levels, func(i, j int) bool {
|
||||||
|
return levels[i].TurnoverThreshold > levels[j].TurnoverThreshold
|
||||||
|
})
|
||||||
|
for _, level := range levels {
|
||||||
|
if !level.Enabled || level.TurnoverThreshold <= 0 || level.RewardGold <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if turnoverAmount >= level.TurnoverThreshold {
|
||||||
|
return level, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roomTurnoverRewardRankingLevel{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueRoomIDs(contributions []integration.RoomContributionActivityCount) []int64 {
|
||||||
|
result := make([]int64, 0, len(contributions))
|
||||||
|
seen := make(map[int64]struct{}, len(contributions))
|
||||||
|
for _, item := range contributions {
|
||||||
|
roomID := int64(item.RoomID)
|
||||||
|
if roomID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[roomID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[roomID] = struct{}{}
|
||||||
|
result = append(result, roomID)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
82
internal/service/roomturnoverreward/current_rank_test.go
Normal file
82
internal/service/roomturnoverreward/current_rank_test.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubRoomTurnoverRewardJavaGateway struct {
|
||||||
|
contributions []integration.RoomContributionActivityCount
|
||||||
|
current integration.RoomContributionActivityCount
|
||||||
|
userProfile integration.UserProfile
|
||||||
|
userRoom integration.RoomProfile
|
||||||
|
profiles map[int64]integration.RoomProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubRoomTurnoverRewardJavaGateway) GetUserProfile(_ context.Context, _ int64) (integration.UserProfile, error) {
|
||||||
|
return s.userProfile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubRoomTurnoverRewardJavaGateway) GetRoomProfileByUserID(_ context.Context, _ int64) (integration.RoomProfile, error) {
|
||||||
|
return s.userRoom, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubRoomTurnoverRewardJavaGateway) GetCurrentWeekRoomContribution(_ context.Context, _ int64) (integration.RoomContributionActivityCount, error) {
|
||||||
|
return s.current, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubRoomTurnoverRewardJavaGateway) ListCurrentWeekRoomContribution(_ context.Context, _ int) ([]integration.RoomContributionActivityCount, error) {
|
||||||
|
return s.contributions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s stubRoomTurnoverRewardJavaGateway) MapRoomProfiles(_ context.Context, _ []int64) (map[int64]integration.RoomProfile, error) {
|
||||||
|
return s.profiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCurrentRoomRankingSortsAndMatchesHighestLevel(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
|
||||||
|
contributions: []integration.RoomContributionActivityCount{
|
||||||
|
{RoomID: integration.Int64Value(101), ContributionValue: integration.AmountValue(120000)},
|
||||||
|
{RoomID: integration.Int64Value(102), ContributionValue: integration.AmountValue(800000)},
|
||||||
|
{RoomID: integration.Int64Value(103), ContributionValue: integration.AmountValue(900000)},
|
||||||
|
},
|
||||||
|
profiles: map[int64]integration.RoomProfile{
|
||||||
|
101: {ID: integration.Int64Value(101), RoomName: "A", SysOrigin: "LIKEI", UserID: integration.Int64Value(1001)},
|
||||||
|
102: {ID: integration.Int64Value(102), RoomName: "B", SysOrigin: "LIKEI", UserID: integration.Int64Value(1002)},
|
||||||
|
103: {ID: integration.Int64Value(103), RoomName: "C", SysOrigin: "TARAB", UserID: integration.Int64Value(1003)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := service.SaveConfig(context.Background(), SaveRoomTurnoverRewardConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "Asia/Riyadh",
|
||||||
|
LevelConfigs: []RoomTurnoverRewardLevelInput{
|
||||||
|
{Level: 1, TurnoverThreshold: 100000, RewardGold: 2000, Enabled: true},
|
||||||
|
{Level: 2, TurnoverThreshold: 500000, RewardGold: 15000, Enabled: true},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.CurrentRoomRanking(context.Background(), "likei", 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CurrentRoomRanking() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.Records) != 2 {
|
||||||
|
t.Fatalf("record count = %d, want 2", len(resp.Records))
|
||||||
|
}
|
||||||
|
if got := resp.Records[0]; got.RoomID != 102 || got.Rank != 1 || got.MatchedLevel != 2 || got.ExpectedRewardGold != 15000 {
|
||||||
|
t.Fatalf("top record = %+v", got)
|
||||||
|
}
|
||||||
|
if got := resp.Records[1]; got.RoomID != 101 || got.Rank != 2 || got.MatchedLevel != 1 || got.ExpectedRewardGold != 2000 {
|
||||||
|
t.Fatalf("second record = %+v", got)
|
||||||
|
}
|
||||||
|
if resp.CycleKey == "" || resp.PeriodStartAt == "" || resp.PeriodEndAt == "" {
|
||||||
|
t.Fatalf("expected cycle metadata, got %+v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
113
internal/service/roomturnoverreward/home.go
Normal file
113
internal/service/roomturnoverreward/home.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHome 返回当前登录用户房间的流水奖励 H5 数据。
|
||||||
|
func (s *Service) GetHome(ctx context.Context, user common.AuthUser) (*RoomTurnoverRewardHomeResponse, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := s.GetConfig(ctx, user.SysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
now := time.Now().In(s.location)
|
||||||
|
periodStart, periodEnd := s.currentCycleBounds(now)
|
||||||
|
levels := enabledPageLevels(config.LevelConfigs)
|
||||||
|
|
||||||
|
resp := &RoomTurnoverRewardHomeResponse{
|
||||||
|
ActivityStatus: roomTurnoverRewardActivityStatus(config.Enabled),
|
||||||
|
Configured: config.Configured,
|
||||||
|
SysOrigin: config.SysOrigin,
|
||||||
|
Timezone: config.Timezone,
|
||||||
|
CycleKey: s.cycleKey(periodStart),
|
||||||
|
PeriodStartAt: formatDateTime(periodStart),
|
||||||
|
PeriodEndAt: formatDateTime(periodEnd),
|
||||||
|
CurrentPeriodText: formatRoomTurnoverRewardPeriod(periodStart, periodEnd),
|
||||||
|
CountdownMillis: maxInt64(0, periodEnd.UnixMilli()-now.UnixMilli()),
|
||||||
|
UserID: user.UserID,
|
||||||
|
HighlightRewardGold: 0,
|
||||||
|
LevelConfigs: levels,
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile, err := s.repo.Java.GetUserProfile(ctx, user.UserID); err == nil {
|
||||||
|
resp.UserAccount = profile.Account
|
||||||
|
resp.UserNickname = profile.UserNickname
|
||||||
|
resp.UserAvatar = profile.UserAvatar
|
||||||
|
} else if !isEmptyJavaProfileError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
roomProfile, err := s.repo.Java.GetRoomProfileByUserID(ctx, user.UserID)
|
||||||
|
if err != nil {
|
||||||
|
if isEmptyJavaProfileError(err) {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if int64(roomProfile.ID) <= 0 {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.HasRoom = true
|
||||||
|
resp.RoomID = int64(roomProfile.ID)
|
||||||
|
resp.RoomAccount = roomProfile.RoomAccount
|
||||||
|
resp.RoomName = roomProfile.RoomName
|
||||||
|
resp.RoomCover = roomProfile.RoomCover
|
||||||
|
if resp.UserID == 0 {
|
||||||
|
resp.UserID = int64(roomProfile.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
contribution, err := s.repo.Java.GetCurrentWeekRoomContribution(ctx, resp.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp.TurnoverAmount = int64(contribution.ContributionValue)
|
||||||
|
|
||||||
|
if level, ok := pickHighestRankingLevel(rankingLevelsFromPayloads(levels), resp.TurnoverAmount); ok {
|
||||||
|
resp.MatchedLevel = level.Level
|
||||||
|
resp.TurnoverThreshold = level.TurnoverThreshold
|
||||||
|
resp.ExpectedRewardGold = level.RewardGold
|
||||||
|
resp.HighlightRewardGold = level.RewardGold
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomTurnoverRewardActivityStatus(enabled bool) string {
|
||||||
|
if enabled {
|
||||||
|
return roomTurnoverRewardActivityStatusOngoing
|
||||||
|
}
|
||||||
|
return roomTurnoverRewardActivityStatusDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func rankingLevelsFromPayloads(levels []RoomTurnoverRewardLevelPayload) []roomTurnoverRewardRankingLevel {
|
||||||
|
result := make([]roomTurnoverRewardRankingLevel, 0, len(levels))
|
||||||
|
for _, level := range levels {
|
||||||
|
result = append(result, roomTurnoverRewardRankingLevel{
|
||||||
|
Level: level.Level,
|
||||||
|
TurnoverThreshold: level.TurnoverThreshold,
|
||||||
|
RewardGold: level.RewardGold,
|
||||||
|
Enabled: level.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmptyJavaProfileError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(strings.ToLower(err.Error()), "empty")
|
||||||
|
}
|
||||||
103
internal/service/roomturnoverreward/home_test.go
Normal file
103
internal/service/roomturnoverreward/home_test.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHomeReturnsUserRoomTurnoverAndExpectedReward(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
|
||||||
|
userProfile: integration.UserProfile{
|
||||||
|
ID: integration.Int64Value(9001),
|
||||||
|
Account: "123456789",
|
||||||
|
UserNickname: "Owner",
|
||||||
|
UserAvatar: "https://example.com/avatar.png",
|
||||||
|
},
|
||||||
|
userRoom: integration.RoomProfile{
|
||||||
|
ID: integration.Int64Value(101),
|
||||||
|
RoomAccount: "R101",
|
||||||
|
RoomName: "Owner room",
|
||||||
|
RoomCover: "https://example.com/room.png",
|
||||||
|
UserID: integration.Int64Value(9001),
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
},
|
||||||
|
current: integration.RoomContributionActivityCount{
|
||||||
|
RoomID: integration.Int64Value(101),
|
||||||
|
ContributionValue: integration.AmountValue(7200000),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
if _, err := service.SaveConfig(ctx, SaveRoomTurnoverRewardConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "Asia/Riyadh",
|
||||||
|
LevelConfigs: []RoomTurnoverRewardLevelInput{
|
||||||
|
{Level: 1, TurnoverThreshold: 1000000, RewardGold: 1000, Enabled: true},
|
||||||
|
{Level: 2, TurnoverThreshold: 7000000, RewardGold: 350000, Enabled: true},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.GetHome(ctx, common.AuthUser{UserID: 9001, SysOrigin: "LIKEI"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHome() error = %v", err)
|
||||||
|
}
|
||||||
|
if !resp.HasRoom || resp.RoomID != 101 || resp.UserNickname != "Owner" {
|
||||||
|
t.Fatalf("profile fields = %+v", resp)
|
||||||
|
}
|
||||||
|
if resp.TurnoverAmount != 7200000 || resp.MatchedLevel != 2 || resp.ExpectedRewardGold != 350000 {
|
||||||
|
t.Fatalf("reward fields = %+v", resp)
|
||||||
|
}
|
||||||
|
if len(resp.LevelConfigs) != 2 || resp.HighlightRewardGold != 350000 {
|
||||||
|
t.Fatalf("level/highlight fields = %+v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHomeReturnsZeroRewardWhenNoRoom(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
|
||||||
|
userProfile: integration.UserProfile{
|
||||||
|
ID: integration.Int64Value(9001),
|
||||||
|
UserNickname: "Owner",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.GetHome(context.Background(), common.AuthUser{UserID: 9001, SysOrigin: "LIKEI"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHome() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.HasRoom || resp.TurnoverAmount != 0 || resp.ExpectedRewardGold != 0 {
|
||||||
|
t.Fatalf("no room response = %+v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHomeMatchesDefaultLevelsWhenConfigDisabled(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
service.repo.Java = stubRoomTurnoverRewardJavaGateway{
|
||||||
|
userRoom: integration.RoomProfile{
|
||||||
|
ID: integration.Int64Value(101),
|
||||||
|
UserID: integration.Int64Value(9001),
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
},
|
||||||
|
current: integration.RoomContributionActivityCount{
|
||||||
|
RoomID: integration.Int64Value(101),
|
||||||
|
ContributionValue: integration.AmountValue(7200000),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.GetHome(context.Background(), common.AuthUser{UserID: 9001, SysOrigin: "LIKEI"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHome() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.Configured || resp.ActivityStatus != roomTurnoverRewardActivityStatusDisabled {
|
||||||
|
t.Fatalf("config fields = %+v", resp)
|
||||||
|
}
|
||||||
|
if resp.MatchedLevel != 4 || resp.ExpectedRewardGold != 250000 || resp.HighlightRewardGold != 250000 {
|
||||||
|
t.Fatalf("reward fields = %+v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
121
internal/service/roomturnoverreward/mapper.go
Normal file
121
internal/service/roomturnoverreward/mapper.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) normalizeSysOrigin(sysOrigin string) string {
|
||||||
|
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
||||||
|
if sysOrigin != "" {
|
||||||
|
return sysOrigin
|
||||||
|
}
|
||||||
|
if fallback := strings.ToUpper(strings.TrimSpace(s.cfg.RoomTurnoverReward.DefaultSysOrigin)); fallback != "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
if fallback := strings.ToUpper(strings.TrimSpace(s.cfg.WeekStar.DefaultSysOrigin)); fallback != "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return "LIKEI"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) defaultTimezone() string {
|
||||||
|
if timezone := strings.TrimSpace(s.cfg.RoomTurnoverReward.Timezone); timezone != "" {
|
||||||
|
return timezone
|
||||||
|
}
|
||||||
|
if timezone := strings.TrimSpace(s.cfg.WeekStar.Timezone); timezone != "" {
|
||||||
|
return timezone
|
||||||
|
}
|
||||||
|
return defaultRoomTurnoverRewardTimezone
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultLevelPayloads() []RoomTurnoverRewardLevelPayload {
|
||||||
|
return []RoomTurnoverRewardLevelPayload{
|
||||||
|
{Level: 1, TurnoverThreshold: 100000, RewardGold: 2000, Enabled: true},
|
||||||
|
{Level: 2, TurnoverThreshold: 500000, RewardGold: 15000, Enabled: true},
|
||||||
|
{Level: 3, TurnoverThreshold: 2000000, RewardGold: 80000, Enabled: true},
|
||||||
|
{Level: 4, TurnoverThreshold: 5000000, RewardGold: 250000, Enabled: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLevelPayloads(rows []model.RoomTurnoverRewardLevel) []RoomTurnoverRewardLevelPayload {
|
||||||
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
|
return rows[i].Level < rows[j].Level
|
||||||
|
})
|
||||||
|
result := make([]RoomTurnoverRewardLevelPayload, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
result = append(result, RoomTurnoverRewardLevelPayload{
|
||||||
|
ID: row.ID,
|
||||||
|
Level: row.Level,
|
||||||
|
TurnoverThreshold: row.TurnoverThreshold,
|
||||||
|
RewardGold: row.RewardGold,
|
||||||
|
Enabled: row.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRecordView(row model.RoomTurnoverRewardRecord) RoomTurnoverRewardRecordView {
|
||||||
|
return RoomTurnoverRewardRecordView{
|
||||||
|
ID: row.ID,
|
||||||
|
ConfigID: row.ConfigID,
|
||||||
|
SysOrigin: row.SysOrigin,
|
||||||
|
CycleKey: row.CycleKey,
|
||||||
|
PeriodStartAt: formatDateTime(row.PeriodStartAt),
|
||||||
|
PeriodEndAt: formatDateTime(row.PeriodEndAt),
|
||||||
|
RoomID: row.RoomID,
|
||||||
|
RoomAccount: row.RoomAccount,
|
||||||
|
RoomName: row.RoomName,
|
||||||
|
RoomCover: row.RoomCover,
|
||||||
|
OwnerUserID: row.OwnerUserID,
|
||||||
|
CountryCode: row.CountryCode,
|
||||||
|
CountryName: row.CountryName,
|
||||||
|
TurnoverAmount: row.TurnoverAmount,
|
||||||
|
Level: row.Level,
|
||||||
|
TurnoverThreshold: row.TurnoverThreshold,
|
||||||
|
RewardGold: row.RewardGold,
|
||||||
|
TrackID: row.TrackID,
|
||||||
|
Status: row.Status,
|
||||||
|
RetryCount: row.RetryCount,
|
||||||
|
LastError: row.LastError,
|
||||||
|
SentAt: formatDateTime(pointerTimeValue(row.SentAt)),
|
||||||
|
UpdateTime: formatDateTime(row.UpdateTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePage(cursor, limit int) (int, int) {
|
||||||
|
if cursor <= 0 {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
return cursor, limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDateTime(t time.Time) string {
|
||||||
|
if t.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func pointerTimeValue(value *time.Time) time.Time {
|
||||||
|
if value == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateString(value string, limit int) string {
|
||||||
|
if limit <= 0 || len(value) <= limit {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:limit]
|
||||||
|
}
|
||||||
83
internal/service/roomturnoverreward/page.go
Normal file
83
internal/service/roomturnoverreward/page.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
roomTurnoverRewardActivityStatusDisabled = "DISABLED"
|
||||||
|
roomTurnoverRewardActivityStatusOngoing = "ONGOING"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPublicPage 返回房间流水奖励 H5 页面所需的公开配置数据。
|
||||||
|
func (s *Service) GetPublicPage(ctx context.Context, sysOrigin string) (*RoomTurnoverRewardPageResponse, error) {
|
||||||
|
config, err := s.GetConfig(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().In(s.location)
|
||||||
|
periodStart, periodEnd := s.currentCycleBounds(now)
|
||||||
|
levels := enabledPageLevels(config.LevelConfigs)
|
||||||
|
activityStatus := roomTurnoverRewardActivityStatusDisabled
|
||||||
|
if config.Enabled {
|
||||||
|
activityStatus = roomTurnoverRewardActivityStatusOngoing
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RoomTurnoverRewardPageResponse{
|
||||||
|
ActivityStatus: activityStatus,
|
||||||
|
Configured: config.Configured,
|
||||||
|
SysOrigin: config.SysOrigin,
|
||||||
|
Timezone: config.Timezone,
|
||||||
|
CycleKey: s.cycleKey(periodStart),
|
||||||
|
PeriodStartAt: formatDateTime(periodStart),
|
||||||
|
PeriodEndAt: formatDateTime(periodEnd),
|
||||||
|
CurrentPeriodText: formatRoomTurnoverRewardPeriod(periodStart, periodEnd),
|
||||||
|
CountdownMillis: maxInt64(0, periodEnd.UnixMilli()-now.UnixMilli()),
|
||||||
|
HighlightRewardGold: highestRewardGold(levels),
|
||||||
|
LevelConfigs: levels,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enabledPageLevels(levels []RoomTurnoverRewardLevelPayload) []RoomTurnoverRewardLevelPayload {
|
||||||
|
result := make([]RoomTurnoverRewardLevelPayload, 0, len(levels))
|
||||||
|
for _, level := range levels {
|
||||||
|
if !level.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, level)
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
result = append(result, levels...)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
return result[i].TurnoverThreshold < result[j].TurnoverThreshold
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func highestRewardGold(levels []RoomTurnoverRewardLevelPayload) int64 {
|
||||||
|
var reward int64
|
||||||
|
for _, level := range levels {
|
||||||
|
if level.RewardGold > reward {
|
||||||
|
reward = level.RewardGold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reward
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRoomTurnoverRewardPeriod(start, end time.Time) string {
|
||||||
|
if start.IsZero() || end.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return start.Format("Jan 02 15:04") + " - " + end.Format("Jan 02 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxInt64(a, b int64) int64 {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
64
internal/service/roomturnoverreward/page_test.go
Normal file
64
internal/service/roomturnoverreward/page_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetPublicPageReturnsEnabledLevelsAndPeriod(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if _, err := service.SaveConfig(ctx, SaveRoomTurnoverRewardConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "Asia/Riyadh",
|
||||||
|
LevelConfigs: []RoomTurnoverRewardLevelInput{
|
||||||
|
{Level: 2, TurnoverThreshold: 7000000, RewardGold: 350000, Enabled: true},
|
||||||
|
{Level: 1, TurnoverThreshold: 1000000, RewardGold: 50000, Enabled: false},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.GetPublicPage(ctx, "likei")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetPublicPage() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.ActivityStatus != roomTurnoverRewardActivityStatusOngoing {
|
||||||
|
t.Fatalf("activityStatus = %q, want %q", resp.ActivityStatus, roomTurnoverRewardActivityStatusOngoing)
|
||||||
|
}
|
||||||
|
if resp.CycleKey == "" || resp.PeriodStartAt == "" || resp.PeriodEndAt == "" || resp.CurrentPeriodText == "" {
|
||||||
|
t.Fatalf("period fields are incomplete: %+v", resp)
|
||||||
|
}
|
||||||
|
if resp.CountdownMillis <= 0 {
|
||||||
|
t.Fatalf("countdownMillis = %d, want > 0", resp.CountdownMillis)
|
||||||
|
}
|
||||||
|
if len(resp.LevelConfigs) != 1 {
|
||||||
|
t.Fatalf("level count = %d, want 1", len(resp.LevelConfigs))
|
||||||
|
}
|
||||||
|
if got := resp.LevelConfigs[0]; got.TurnoverThreshold != 7000000 || got.RewardGold != 350000 {
|
||||||
|
t.Fatalf("level = %+v, want 7M/350k", got)
|
||||||
|
}
|
||||||
|
if resp.HighlightRewardGold != 350000 {
|
||||||
|
t.Fatalf("highlightRewardGold = %d, want 350000", resp.HighlightRewardGold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPublicPageFallsBackToDefaultLevelsWhenMissing(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
|
||||||
|
resp, err := service.GetPublicPage(context.Background(), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetPublicPage() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.ActivityStatus != roomTurnoverRewardActivityStatusDisabled {
|
||||||
|
t.Fatalf("activityStatus = %q, want disabled", resp.ActivityStatus)
|
||||||
|
}
|
||||||
|
if len(resp.LevelConfigs) != 4 {
|
||||||
|
t.Fatalf("level count = %d, want 4", len(resp.LevelConfigs))
|
||||||
|
}
|
||||||
|
if resp.HighlightRewardGold != 250000 {
|
||||||
|
t.Fatalf("highlightRewardGold = %d, want 250000", resp.HighlightRewardGold)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
internal/service/roomturnoverreward/records.go
Normal file
51
internal/service/roomturnoverreward/records.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PageRewardRecords 分页查询每周房间流水奖励发放记录。
|
||||||
|
func (s *Service) PageRewardRecords(ctx context.Context, sysOrigin, cycleKey, status string, cursor, limit int) (map[string]any, error) {
|
||||||
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
||||||
|
page, size := normalizePage(cursor, limit)
|
||||||
|
|
||||||
|
query := s.repo.DB.WithContext(ctx).
|
||||||
|
Model(&model.RoomTurnoverRewardRecord{}).
|
||||||
|
Where("sys_origin = ?", sysOrigin)
|
||||||
|
if cycleKey = strings.TrimSpace(cycleKey); cycleKey != "" {
|
||||||
|
query = query.Where("cycle_key = ?", cycleKey)
|
||||||
|
}
|
||||||
|
if status = strings.ToUpper(strings.TrimSpace(status)); status != "" {
|
||||||
|
query = query.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []model.RoomTurnoverRewardRecord
|
||||||
|
if total > 0 {
|
||||||
|
if err := query.
|
||||||
|
Order("cycle_key desc").
|
||||||
|
Order("turnover_amount desc").
|
||||||
|
Order("id desc").
|
||||||
|
Offset((page - 1) * size).
|
||||||
|
Limit(size).
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make([]RoomTurnoverRewardRecordView, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
records = append(records, toRecordView(row))
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"records": records,
|
||||||
|
"total": total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
291
internal/service/roomturnoverreward/types.go
Normal file
291
internal/service/roomturnoverreward/types.go
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
package roomturnoverreward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
roomTurnoverRewardStatusPending = "PENDING"
|
||||||
|
roomTurnoverRewardStatusSuccess = "SUCCESS"
|
||||||
|
roomTurnoverRewardStatusFailed = "FAILED"
|
||||||
|
|
||||||
|
defaultRoomTurnoverRewardTimezone = "Asia/Riyadh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// flexibleInt64 兼容后台以字符串传递 Snowflake ID 的情况。
|
||||||
|
type flexibleInt64 int64
|
||||||
|
|
||||||
|
func (v *flexibleInt64) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := strings.TrimSpace(string(data))
|
||||||
|
if raw == "" || raw == "null" {
|
||||||
|
*v = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
|
||||||
|
raw = strings.Trim(raw, "\"")
|
||||||
|
}
|
||||||
|
if raw == "" {
|
||||||
|
*v = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if parsed, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||||
|
*v = flexibleInt64(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseFloat(raw, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*v = flexibleInt64(int64(math.Round(parsed)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v flexibleInt64) Int64() int64 {
|
||||||
|
return int64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v flexibleInt64) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(strconv.FormatInt(int64(v), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardLevelPayload 是后台展示的奖励档位。
|
||||||
|
type RoomTurnoverRewardLevelPayload struct {
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
TurnoverThreshold int64 `json:"turnoverThreshold"`
|
||||||
|
RewardGold int64 `json:"rewardGold"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardLevelInput 是后台保存奖励档位的入参。
|
||||||
|
type RoomTurnoverRewardLevelInput struct {
|
||||||
|
ID flexibleInt64 `json:"id"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
TurnoverThreshold int64 `json:"turnoverThreshold"`
|
||||||
|
RewardGold int64 `json:"rewardGold"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveRoomTurnoverRewardConfigRequest 是房间流水奖励配置保存入参。
|
||||||
|
type SaveRoomTurnoverRewardConfigRequest struct {
|
||||||
|
ID flexibleInt64 `json:"id"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
LevelConfigs []RoomTurnoverRewardLevelInput `json:"levelConfigs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardRecordView 是后台发奖记录列表项。
|
||||||
|
type RoomTurnoverRewardRecordView struct {
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
ConfigID int64 `json:"configId,string"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
CycleKey string `json:"cycleKey"`
|
||||||
|
PeriodStartAt string `json:"periodStartAt"`
|
||||||
|
PeriodEndAt string `json:"periodEndAt"`
|
||||||
|
RoomID int64 `json:"roomId,string"`
|
||||||
|
RoomAccount string `json:"roomAccount"`
|
||||||
|
RoomName string `json:"roomName"`
|
||||||
|
RoomCover string `json:"roomCover"`
|
||||||
|
OwnerUserID int64 `json:"ownerUserId,string"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
CountryName string `json:"countryName"`
|
||||||
|
TurnoverAmount int64 `json:"turnoverAmount"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
TurnoverThreshold int64 `json:"turnoverThreshold"`
|
||||||
|
RewardGold int64 `json:"rewardGold"`
|
||||||
|
TrackID string `json:"trackId"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RetryCount int `json:"retryCount"`
|
||||||
|
LastError string `json:"lastError"`
|
||||||
|
SentAt string `json:"sentAt"`
|
||||||
|
UpdateTime string `json:"updateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardCurrentRankItem 是当前房间流水排行榜列表项。
|
||||||
|
type RoomTurnoverRewardCurrentRankItem struct {
|
||||||
|
Rank int `json:"rank"`
|
||||||
|
RoomID int64 `json:"roomId,string"`
|
||||||
|
RoomAccount string `json:"roomAccount"`
|
||||||
|
RoomName string `json:"roomName"`
|
||||||
|
RoomCover string `json:"roomCover"`
|
||||||
|
OwnerUserID int64 `json:"ownerUserId,string"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
CountryName string `json:"countryName"`
|
||||||
|
TurnoverAmount int64 `json:"turnoverAmount"`
|
||||||
|
DateNumber int `json:"dateNumber"`
|
||||||
|
MatchedLevel int `json:"matchedLevel"`
|
||||||
|
TurnoverThreshold int64 `json:"turnoverThreshold"`
|
||||||
|
ExpectedRewardGold int64 `json:"expectedRewardGold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardCurrentRankResponse 是当前房间流水排行榜读模型。
|
||||||
|
type RoomTurnoverRewardCurrentRankResponse struct {
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
CycleKey string `json:"cycleKey"`
|
||||||
|
PeriodStartAt string `json:"periodStartAt"`
|
||||||
|
PeriodEndAt string `json:"periodEndAt"`
|
||||||
|
DateNumber int `json:"dateNumber"`
|
||||||
|
Records []RoomTurnoverRewardCurrentRankItem `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardPageResponse 是房间流水奖励 H5 页面读模型。
|
||||||
|
type RoomTurnoverRewardPageResponse struct {
|
||||||
|
ActivityStatus string `json:"activityStatus"`
|
||||||
|
Configured bool `json:"configured"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
CycleKey string `json:"cycleKey"`
|
||||||
|
PeriodStartAt string `json:"periodStartAt"`
|
||||||
|
PeriodEndAt string `json:"periodEndAt"`
|
||||||
|
CurrentPeriodText string `json:"currentPeriodText"`
|
||||||
|
CountdownMillis int64 `json:"countdownMillis"`
|
||||||
|
HighlightRewardGold int64 `json:"highlightRewardGold"`
|
||||||
|
LevelConfigs []RoomTurnoverRewardLevelPayload `json:"levelConfigs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardHomeResponse 是房间流水奖励 H5 用户态首页读模型。
|
||||||
|
type RoomTurnoverRewardHomeResponse struct {
|
||||||
|
ActivityStatus string `json:"activityStatus"`
|
||||||
|
Configured bool `json:"configured"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
CycleKey string `json:"cycleKey"`
|
||||||
|
PeriodStartAt string `json:"periodStartAt"`
|
||||||
|
PeriodEndAt string `json:"periodEndAt"`
|
||||||
|
CurrentPeriodText string `json:"currentPeriodText"`
|
||||||
|
CountdownMillis int64 `json:"countdownMillis"`
|
||||||
|
UserID int64 `json:"userId,string"`
|
||||||
|
UserAccount string `json:"userAccount"`
|
||||||
|
UserNickname string `json:"userNickname"`
|
||||||
|
UserAvatar string `json:"userAvatar"`
|
||||||
|
HasRoom bool `json:"hasRoom"`
|
||||||
|
RoomID int64 `json:"roomId,string"`
|
||||||
|
RoomAccount string `json:"roomAccount"`
|
||||||
|
RoomName string `json:"roomName"`
|
||||||
|
RoomCover string `json:"roomCover"`
|
||||||
|
TurnoverAmount int64 `json:"turnoverAmount"`
|
||||||
|
MatchedLevel int `json:"matchedLevel"`
|
||||||
|
TurnoverThreshold int64 `json:"turnoverThreshold"`
|
||||||
|
ExpectedRewardGold int64 `json:"expectedRewardGold"`
|
||||||
|
HighlightRewardGold int64 `json:"highlightRewardGold"`
|
||||||
|
LevelConfigs []RoomTurnoverRewardLevelPayload `json:"levelConfigs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type roomTurnoverRewardDB interface {
|
||||||
|
WithContext(ctx context.Context) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type roomTurnoverRewardJavaGateway interface {
|
||||||
|
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
||||||
|
GetRoomProfileByUserID(ctx context.Context, userID int64) (integration.RoomProfile, error)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type roomTurnoverRewardPorts struct {
|
||||||
|
DB roomTurnoverRewardDB
|
||||||
|
Java roomTurnoverRewardJavaGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 负责房间流水奖励配置与后台记录查询。
|
||||||
|
type Service struct {
|
||||||
|
cfg config.Config
|
||||||
|
repo roomTurnoverRewardPorts
|
||||||
|
location *time.Location
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(cfg config.Config, db roomTurnoverRewardDB, javaGateway ...roomTurnoverRewardJavaGateway) *Service {
|
||||||
|
timezone := strings.TrimSpace(cfg.RoomTurnoverReward.Timezone)
|
||||||
|
if timezone == "" {
|
||||||
|
timezone = strings.TrimSpace(cfg.WeekStar.Timezone)
|
||||||
|
}
|
||||||
|
if timezone == "" {
|
||||||
|
timezone = defaultRoomTurnoverRewardTimezone
|
||||||
|
}
|
||||||
|
location, err := time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
location = time.UTC
|
||||||
|
}
|
||||||
|
var java roomTurnoverRewardJavaGateway
|
||||||
|
if len(javaGateway) > 0 {
|
||||||
|
java = javaGateway[0]
|
||||||
|
}
|
||||||
|
return &Service{
|
||||||
|
cfg: cfg,
|
||||||
|
repo: roomTurnoverRewardPorts{DB: db, Java: java},
|
||||||
|
location: location,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) cycleDuration() time.Duration {
|
||||||
|
duration := s.cfg.RoomTurnoverReward.CycleDuration
|
||||||
|
if duration <= 0 {
|
||||||
|
duration = 10 * time.Second
|
||||||
|
}
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) currentCycleBounds(now time.Time) (time.Time, time.Time) {
|
||||||
|
duration := s.cycleDuration()
|
||||||
|
local := now.In(s.location)
|
||||||
|
if duration >= 7*24*time.Hour {
|
||||||
|
start := s.weekCycleStart(local)
|
||||||
|
return start, start.AddDate(0, 0, 7)
|
||||||
|
}
|
||||||
|
anchor := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, s.location)
|
||||||
|
elapsed := local.Sub(anchor)
|
||||||
|
cycles := int64(elapsed / duration)
|
||||||
|
start := anchor.Add(time.Duration(cycles) * duration)
|
||||||
|
return start, start.Add(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) weekCycleStart(t time.Time) time.Time {
|
||||||
|
local := t.In(s.location)
|
||||||
|
weekday := int(local.Weekday())
|
||||||
|
if weekday == 0 {
|
||||||
|
weekday = 7
|
||||||
|
}
|
||||||
|
startDay := local.AddDate(0, 0, -(weekday - 1))
|
||||||
|
return time.Date(startDay.Year(), startDay.Month(), startDay.Day(), 0, 0, 0, 0, s.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) cycleKey(start time.Time) string {
|
||||||
|
if s.cycleDuration() >= 7*24*time.Hour {
|
||||||
|
return start.In(s.location).Format("20060102")
|
||||||
|
}
|
||||||
|
return start.In(s.location).Format("20060102150405")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) currentDateNumber(now time.Time) (int, error) {
|
||||||
|
raw := s.weekCycleStart(now).Format("20060102")
|
||||||
|
dateNumber, err := strconv.Atoi(raw)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parse current date number: %w", err)
|
||||||
|
}
|
||||||
|
return dateNumber, nil
|
||||||
|
}
|
||||||
@ -176,23 +176,20 @@ func (s *WeekStarService) SaveConfig(ctx context.Context, req SaveWeekStarConfig
|
|||||||
if _, exists := seenRanks[item.Rank]; exists {
|
if _, exists := seenRanks[item.Rank]; exists {
|
||||||
return nil, NewAppError(http.StatusBadRequest, "duplicate_reward_rank", "rewardConfigs must not contain duplicate ranks")
|
return nil, NewAppError(http.StatusBadRequest, "duplicate_reward_rank", "rewardConfigs must not contain duplicate ranks")
|
||||||
}
|
}
|
||||||
rewardItems, rewardGroupName, err := s.loadRewardPreviewItems(ctx, rewardGroupID)
|
rewardGroupName := strings.TrimSpace(item.RewardGroupName)
|
||||||
if err != nil || len(rewardItems) == 0 {
|
if _, loadedRewardGroupName, err := s.loadRewardPreviewItems(ctx, rewardGroupID); err == nil && strings.TrimSpace(loadedRewardGroupName) != "" {
|
||||||
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_group_id", "reward group detail is empty or unavailable")
|
rewardGroupName = loadedRewardGroupName
|
||||||
}
|
}
|
||||||
seenRanks[item.Rank] = struct{}{}
|
seenRanks[item.Rank] = struct{}{}
|
||||||
id, idErr := utils.NextID()
|
id, idErr := utils.NextID()
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return nil, idErr
|
return nil, idErr
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(rewardGroupName) == "" {
|
|
||||||
rewardGroupName = item.RewardGroupName
|
|
||||||
}
|
|
||||||
normalizedRewards = append(normalizedRewards, model.WeekStarActivityRewardConfig{
|
normalizedRewards = append(normalizedRewards, model.WeekStarActivityRewardConfig{
|
||||||
ID: id,
|
ID: id,
|
||||||
Rank: item.Rank,
|
Rank: item.Rank,
|
||||||
RewardGroupID: rewardGroupID,
|
RewardGroupID: rewardGroupID,
|
||||||
RewardGroupName: strings.TrimSpace(rewardGroupName),
|
RewardGroupName: rewardGroupName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package weekstar
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -41,7 +42,19 @@ func (g weekStarTestGateway) GetUserProfile(context.Context, int64) (integration
|
|||||||
return integration.UserProfile{}, nil
|
return integration.UserProfile{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type unavailableRewardDetailGateway struct {
|
||||||
|
weekStarTestGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g unavailableRewardDetailGateway) GetRewardGroupDetail(context.Context, int64) (integration.RewardGroupDetail, error) {
|
||||||
|
return integration.RewardGroupDetail{}, errors.New("java reward detail unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
func newTestWeekStarService(t *testing.T) (*WeekStarService, *gorm.DB) {
|
func newTestWeekStarService(t *testing.T) (*WeekStarService, *gorm.DB) {
|
||||||
|
return newTestWeekStarServiceWithGateway(t, weekStarTestGateway{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestWeekStarServiceWithGateway(t *testing.T, gateway weekStarGateway) (*WeekStarService, *gorm.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
||||||
@ -62,7 +75,7 @@ func newTestWeekStarService(t *testing.T) (*WeekStarService, *gorm.DB) {
|
|||||||
DefaultSysOrigin: "LIKEI",
|
DefaultSysOrigin: "LIKEI",
|
||||||
Timezone: "Asia/Riyadh",
|
Timezone: "Asia/Riyadh",
|
||||||
},
|
},
|
||||||
}, db, nil, weekStarTestGateway{})
|
}, db, nil, gateway)
|
||||||
return service, db
|
return service, db
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +166,42 @@ func TestSaveConfigAllowsCurrentWeekWhenNoActiveConfigExists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSaveConfigAllowsRewardGroupWhenDetailUnavailable(t *testing.T) {
|
||||||
|
service, _ := newTestWeekStarServiceWithGateway(t, unavailableRewardDetailGateway{})
|
||||||
|
now := time.Now().In(service.location)
|
||||||
|
cycleStart, cycleEnd := service.cycleBounds(now)
|
||||||
|
|
||||||
|
resp, err := service.SaveConfig(context.Background(), SaveWeekStarConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
StartAt: formatDateTime(cycleStart),
|
||||||
|
EndAt: formatDateTime(cycleEnd),
|
||||||
|
Timezone: "Asia/Riyadh",
|
||||||
|
GiftConfigs: []WeekStarGiftConfigInput{
|
||||||
|
{GiftID: flexibleInt64(101), GiftName: "Gift 1", Sort: 1},
|
||||||
|
{GiftID: flexibleInt64(102), GiftName: "Gift 2", Sort: 2},
|
||||||
|
{GiftID: flexibleInt64(103), GiftName: "Gift 3", Sort: 3},
|
||||||
|
},
|
||||||
|
RewardConfigs: []WeekStarRewardConfigInput{
|
||||||
|
{Rank: 1, RewardGroupID: flexibleInt64(2004471533988167125), RewardGroupName: "Top1 Fallback"},
|
||||||
|
{Rank: 2, RewardGroupID: flexibleInt64(2004471533988167126), RewardGroupName: "Top2 Fallback"},
|
||||||
|
{Rank: 3, RewardGroupID: flexibleInt64(2004471533988167127), RewardGroupName: "Top3 Fallback"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil || len(resp.RewardConfigs) != 3 {
|
||||||
|
t.Fatalf("SaveConfig() response = %#v, want 3 reward configs", resp)
|
||||||
|
}
|
||||||
|
if got := resp.RewardConfigs[0].RewardGroupName; got != "Top1 Fallback" {
|
||||||
|
t.Fatalf("rewardGroupName = %q, want fallback name", got)
|
||||||
|
}
|
||||||
|
if len(resp.RewardConfigs[0].Items) != 0 {
|
||||||
|
t.Fatalf("reward items length = %d, want 0 when detail is unavailable", len(resp.RewardConfigs[0].Items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSaveConfigRejectsCurrentWeekWhenActiveConfigExists(t *testing.T) {
|
func TestSaveConfigRejectsCurrentWeekWhenActiveConfigExists(t *testing.T) {
|
||||||
service, db := newTestWeekStarService(t)
|
service, db := newTestWeekStarService(t)
|
||||||
now := time.Now().In(service.location)
|
now := time.Now().In(service.location)
|
||||||
|
|||||||
61
migrations/017_room_turnover_reward.sql
Normal file
61
migrations/017_room_turnover_reward.sql
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `room_turnover_reward_config` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键ID',
|
||||||
|
`sys_origin` varchar(32) NOT NULL COMMENT '来源系统',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用',
|
||||||
|
`timezone` varchar(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '结算时区',
|
||||||
|
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_room_turnover_reward_sys_origin` (`sys_origin`),
|
||||||
|
KEY `idx_room_turnover_reward_enabled` (`enabled`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间流水奖励主配置';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_turnover_reward_level` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键ID',
|
||||||
|
`config_id` bigint NOT NULL COMMENT '主配置ID',
|
||||||
|
`level` int NOT NULL COMMENT '奖励等级',
|
||||||
|
`turnover_threshold` bigint NOT NULL COMMENT '流水门槛',
|
||||||
|
`reward_gold` bigint NOT NULL COMMENT '奖励房主金币',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
|
||||||
|
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_room_turnover_reward_level` (`config_id`, `level`),
|
||||||
|
UNIQUE KEY `uk_room_turnover_reward_threshold` (`config_id`, `turnover_threshold`),
|
||||||
|
KEY `idx_room_turnover_reward_level_config` (`config_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间流水奖励等级配置';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `room_turnover_reward_record` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键ID',
|
||||||
|
`config_id` bigint NOT NULL COMMENT '主配置ID',
|
||||||
|
`sys_origin` varchar(32) NOT NULL COMMENT '来源系统',
|
||||||
|
`cycle_key` varchar(32) NOT NULL COMMENT '周期标识,取周一 yyyyMMdd',
|
||||||
|
`period_start_at` datetime(3) NOT NULL COMMENT '周期开始时间',
|
||||||
|
`period_end_at` datetime(3) NOT NULL COMMENT '周期结束时间',
|
||||||
|
`room_id` bigint NOT NULL COMMENT '房间ID',
|
||||||
|
`room_account` varchar(64) DEFAULT NULL COMMENT '房间账号快照',
|
||||||
|
`room_name` varchar(255) DEFAULT NULL COMMENT '房间名称快照',
|
||||||
|
`room_cover` varchar(1024) DEFAULT NULL COMMENT '房间封面快照',
|
||||||
|
`owner_user_id` bigint NOT NULL COMMENT '房主用户ID',
|
||||||
|
`country_code` varchar(32) DEFAULT NULL COMMENT '国家编码',
|
||||||
|
`country_name` varchar(128) DEFAULT NULL COMMENT '国家名称',
|
||||||
|
`turnover_amount` bigint NOT NULL DEFAULT '0' COMMENT '本周期房间流水',
|
||||||
|
`level` int NOT NULL COMMENT '命中的最高奖励等级',
|
||||||
|
`turnover_threshold` bigint NOT NULL COMMENT '命中的流水门槛',
|
||||||
|
`reward_gold` bigint NOT NULL DEFAULT '0' COMMENT '奖励金币',
|
||||||
|
`track_id` varchar(128) NOT NULL COMMENT '钱包发奖幂等ID',
|
||||||
|
`status` varchar(32) NOT NULL DEFAULT 'PENDING' COMMENT '发奖状态',
|
||||||
|
`retry_count` int NOT NULL DEFAULT '0' COMMENT '重试次数',
|
||||||
|
`last_error` varchar(1024) DEFAULT NULL COMMENT '最近失败原因',
|
||||||
|
`sent_at` datetime(3) DEFAULT NULL COMMENT '发放时间',
|
||||||
|
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_room_turnover_reward_record_room` (`sys_origin`, `cycle_key`, `room_id`),
|
||||||
|
UNIQUE KEY `uk_room_turnover_reward_track` (`track_id`),
|
||||||
|
KEY `idx_room_turnover_reward_record_cycle` (`sys_origin`, `cycle_key`),
|
||||||
|
KEY `idx_room_turnover_reward_record_config` (`config_id`),
|
||||||
|
KEY `idx_room_turnover_reward_record_room` (`room_id`),
|
||||||
|
KEY `idx_room_turnover_reward_record_owner` (`owner_user_id`),
|
||||||
|
KEY `idx_room_turnover_reward_record_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间流水奖励发放记录';
|
||||||
Loading…
x
Reference in New Issue
Block a user