主播中心/邀请活动
This commit is contained in:
parent
7c1ecb804e
commit
4fc2ddebf1
@ -6,10 +6,12 @@ import (
|
||||
"chatapp3-golang/internal/service/baishun"
|
||||
"chatapp3-golang/internal/service/gameopen"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"chatapp3-golang/internal/service/hostcenter"
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/lingxian"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||
"chatapp3-golang/internal/service/signinreward"
|
||||
"chatapp3-golang/internal/service/weekstar"
|
||||
"context"
|
||||
@ -39,6 +41,8 @@ func main() {
|
||||
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)
|
||||
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())
|
||||
defer workerCancel()
|
||||
@ -51,15 +55,17 @@ func main() {
|
||||
lingxian.NewAppProvider(lingxianService),
|
||||
)
|
||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||
Invite: inviteService,
|
||||
Baishun: baishunService,
|
||||
Lingxian: lingxianService,
|
||||
GameOpen: gameOpenService,
|
||||
GameProviders: gameProviders,
|
||||
LuckyGift: luckyGiftService,
|
||||
RegisterReward: registerRewardService,
|
||||
SignInReward: signInRewardService,
|
||||
WeekStar: weekStarService,
|
||||
Invite: inviteService,
|
||||
Baishun: baishunService,
|
||||
Lingxian: lingxianService,
|
||||
GameOpen: gameOpenService,
|
||||
GameProviders: gameProviders,
|
||||
LuckyGift: luckyGiftService,
|
||||
HostCenter: hostCenterService,
|
||||
RegisterReward: registerRewardService,
|
||||
RoomTurnoverReward: roomTurnoverRewardService,
|
||||
SignInReward: signInRewardService,
|
||||
WeekStar: weekStarService,
|
||||
})
|
||||
server := &http.Server{
|
||||
Addr: app.Config.HTTP.ListenAddr,
|
||||
|
||||
@ -10,16 +10,17 @@ import (
|
||||
|
||||
// Config 是按运行层和业务模块分段后的应用配置。
|
||||
type Config struct {
|
||||
HTTP HTTPConfig
|
||||
Store StoreConfig
|
||||
Java JavaConfig
|
||||
Worker WorkerConfig
|
||||
Invite InviteConfig
|
||||
WeekStar WeekStarConfig
|
||||
Baishun BaishunConfig
|
||||
Lingxian LingxianConfig
|
||||
GameOpen GameOpenConfig
|
||||
LuckyGift LuckyGiftConfig
|
||||
HTTP HTTPConfig
|
||||
Store StoreConfig
|
||||
Java JavaConfig
|
||||
Worker WorkerConfig
|
||||
Invite InviteConfig
|
||||
WeekStar WeekStarConfig
|
||||
RoomTurnoverReward RoomTurnoverRewardConfig
|
||||
Baishun BaishunConfig
|
||||
Lingxian LingxianConfig
|
||||
GameOpen GameOpenConfig
|
||||
LuckyGift LuckyGiftConfig
|
||||
}
|
||||
|
||||
// HTTPConfig 保存 HTTP 服务运行配置。
|
||||
@ -92,6 +93,13 @@ type WeekStarRocketMQConfig struct {
|
||||
Tag string
|
||||
}
|
||||
|
||||
// RoomTurnoverRewardConfig 保存房间流水奖励模块配置。
|
||||
type RoomTurnoverRewardConfig struct {
|
||||
DefaultSysOrigin string
|
||||
Timezone string
|
||||
CycleDuration time.Duration
|
||||
}
|
||||
|
||||
// BaishunConfig 保存百顺模块配置。
|
||||
type BaishunConfig struct {
|
||||
PlatformBaseURL string
|
||||
@ -194,6 +202,20 @@ func Load() Config {
|
||||
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{
|
||||
PlatformBaseURL: getEnvAny([]string{"CHATAPP_BAISHUN_PLATFORM_BASE_URL", "BAISHUN_PLATFORM_BASE_URL"}, ""),
|
||||
GameListURL: getEnvAny([]string{"CHATAPP_BAISHUN_GAME_LIST_URL", "BAISHUN_GAME_LIST_URL"}, ""),
|
||||
|
||||
@ -15,6 +15,8 @@ type Gateways struct {
|
||||
RewardDispatch *RewardDispatchGateway
|
||||
RewardQuery *RewardQueryGateway
|
||||
Profile *ProfileGateway
|
||||
Room *RoomGateway
|
||||
Team *TeamGateway
|
||||
Wallet *WalletGateway
|
||||
}
|
||||
|
||||
@ -29,6 +31,8 @@ func NewGateways(cfg config.Config) Gateways {
|
||||
RewardDispatch: &RewardDispatchGateway{client: client},
|
||||
RewardQuery: &RewardQueryGateway{client: client},
|
||||
Profile: &ProfileGateway{client: client},
|
||||
Room: &RoomGateway{client: client},
|
||||
Team: &TeamGateway{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)
|
||||
}
|
||||
|
||||
// GetUserRegion 透传到用户地区网关。
|
||||
func (g *Gateways) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
|
||||
return g.Profile.GetUserRegion(ctx, userID, authorization)
|
||||
}
|
||||
|
||||
// GetUserProfileByAccount 透传到资料网关。
|
||||
func (g *Gateways) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 透传到钱包网关。
|
||||
func (g *Gateways) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// GetUserRegion 查询用户当前地区。
|
||||
func (g *ProfileGateway) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
|
||||
return g.client.GetUserRegion(ctx, userID, authorization)
|
||||
}
|
||||
|
||||
// GetUserProfileByAccount 根据账号查询用户资料。
|
||||
func (g *ProfileGateway) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 负责钱包相关接口。
|
||||
type WalletGateway struct {
|
||||
client *Client
|
||||
|
||||
@ -41,6 +41,36 @@ type UserProfile struct {
|
||||
Account string `json:"account"`
|
||||
UserAvatar string `json:"userAvatar"`
|
||||
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 {
|
||||
@ -104,6 +134,37 @@ type SendActivityRewardRequest struct {
|
||||
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 表示发送自定义官方通知的请求体。
|
||||
type OfficialNoticeCustomizeRequest struct {
|
||||
ToAccounts []int64 `json:"toAccounts"`
|
||||
@ -157,6 +218,57 @@ func (v *Int64Value) UnmarshalJSON(data []byte) error {
|
||||
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 {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
@ -383,6 +495,22 @@ func (c *Client) GetUserProfile(ctx context.Context, userID int64) (UserProfile,
|
||||
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) {
|
||||
endpoint := c.cfg.Java.OtherBaseURL + "/user-profile/client/getByAccount?sysOrigin=" +
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.Java.ConsoleBaseURL), "/"); 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/gameopen"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
"chatapp3-golang/internal/service/hostcenter"
|
||||
"chatapp3-golang/internal/service/invite"
|
||||
"chatapp3-golang/internal/service/lingxian"
|
||||
"chatapp3-golang/internal/service/luckygift"
|
||||
"chatapp3-golang/internal/service/registerreward"
|
||||
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||
"chatapp3-golang/internal/service/signinreward"
|
||||
"chatapp3-golang/internal/service/weekstar"
|
||||
|
||||
@ -21,15 +23,17 @@ import (
|
||||
|
||||
// Services 聚合所有业务模块服务,供路由层统一装配。
|
||||
type Services struct {
|
||||
Invite *invite.InviteService
|
||||
Baishun *baishun.BaishunService
|
||||
Lingxian *lingxian.Service
|
||||
GameOpen *gameopen.GameOpenService
|
||||
GameProviders *gameprovider.Registry
|
||||
LuckyGift *luckygift.LuckyGiftService
|
||||
RegisterReward *registerreward.RegisterRewardService
|
||||
SignInReward *signinreward.SignInRewardService
|
||||
WeekStar *weekstar.WeekStarService
|
||||
Invite *invite.InviteService
|
||||
Baishun *baishun.BaishunService
|
||||
Lingxian *lingxian.Service
|
||||
GameOpen *gameopen.GameOpenService
|
||||
GameProviders *gameprovider.Registry
|
||||
LuckyGift *luckygift.LuckyGiftService
|
||||
HostCenter *hostcenter.Service
|
||||
RegisterReward *registerreward.RegisterRewardService
|
||||
RoomTurnoverReward *roomturnoverreward.Service
|
||||
SignInReward *signinreward.SignInRewardService
|
||||
WeekStar *weekstar.WeekStarService
|
||||
}
|
||||
|
||||
// NewRouter 创建 Gin 路由,并按模块注册所有接口。
|
||||
@ -40,13 +44,15 @@ func NewRouter(
|
||||
services Services,
|
||||
) *gin.Engine {
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Logger(), gin.Recovery())
|
||||
engine.Use(gin.Logger(), gin.Recovery(), corsMiddleware())
|
||||
|
||||
registerHealthRoutes(engine, cfg, repository)
|
||||
registerInviteRoutes(engine, javaClient, services.Invite)
|
||||
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
||||
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
||||
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
||||
registerRoomTurnoverRewardRoutes(engine, javaClient, services.RoomTurnoverReward)
|
||||
registerHostCenterRoutes(engine, services.HostCenter)
|
||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||
@ -56,6 +62,28 @@ func NewRouter(
|
||||
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 注册健康检查接口。
|
||||
func registerHealthRoutes(engine *gin.Engine, cfg config.Config, repository *repo.Repository) {
|
||||
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 {
|
||||
return nil, NewAppError(http.StatusBadRequest, "duplicate_reward_rank", "rewardConfigs must not contain duplicate ranks")
|
||||
}
|
||||
rewardItems, rewardGroupName, err := s.loadRewardPreviewItems(ctx, rewardGroupID)
|
||||
if err != nil || len(rewardItems) == 0 {
|
||||
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_group_id", "reward group detail is empty or unavailable")
|
||||
rewardGroupName := strings.TrimSpace(item.RewardGroupName)
|
||||
if _, loadedRewardGroupName, err := s.loadRewardPreviewItems(ctx, rewardGroupID); err == nil && strings.TrimSpace(loadedRewardGroupName) != "" {
|
||||
rewardGroupName = loadedRewardGroupName
|
||||
}
|
||||
seenRanks[item.Rank] = struct{}{}
|
||||
id, idErr := utils.NextID()
|
||||
if idErr != nil {
|
||||
return nil, idErr
|
||||
}
|
||||
if strings.TrimSpace(rewardGroupName) == "" {
|
||||
rewardGroupName = item.RewardGroupName
|
||||
}
|
||||
normalizedRewards = append(normalizedRewards, model.WeekStarActivityRewardConfig{
|
||||
ID: id,
|
||||
Rank: item.Rank,
|
||||
RewardGroupID: rewardGroupID,
|
||||
RewardGroupName: strings.TrimSpace(rewardGroupName),
|
||||
RewardGroupName: rewardGroupName,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package weekstar
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -41,7 +42,19 @@ func (g weekStarTestGateway) GetUserProfile(context.Context, int64) (integration
|
||||
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) {
|
||||
return newTestWeekStarServiceWithGateway(t, weekStarTestGateway{})
|
||||
}
|
||||
|
||||
func newTestWeekStarServiceWithGateway(t *testing.T, gateway weekStarGateway) (*WeekStarService, *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
||||
@ -62,7 +75,7 @@ func newTestWeekStarService(t *testing.T) (*WeekStarService, *gorm.DB) {
|
||||
DefaultSysOrigin: "LIKEI",
|
||||
Timezone: "Asia/Riyadh",
|
||||
},
|
||||
}, db, nil, weekStarTestGateway{})
|
||||
}, db, nil, gateway)
|
||||
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) {
|
||||
service, db := newTestWeekStarService(t)
|
||||
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