Compare commits
No commits in common. "39744caf2ecdb3ba75d89d3cef1e6cd8a0e706ed" and "fc214f99914c53e79a5f949fbd9fdbdede32a005" have entirely different histories.
39744caf2e
...
fc214f9991
@ -15,7 +15,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/apppopup"
|
"chatapp3-golang/internal/service/apppopup"
|
||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/binancerecharge"
|
"chatapp3-golang/internal/service/binancerecharge"
|
||||||
"chatapp3-golang/internal/service/errorlog"
|
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
"chatapp3-golang/internal/service/hostcenter"
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
@ -31,7 +30,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/signinreward"
|
"chatapp3-golang/internal/service/signinreward"
|
||||||
"chatapp3-golang/internal/service/smashegg"
|
"chatapp3-golang/internal/service/smashegg"
|
||||||
"chatapp3-golang/internal/service/taskcenter"
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
"chatapp3-golang/internal/service/userbadge"
|
|
||||||
"chatapp3-golang/internal/service/vip"
|
"chatapp3-golang/internal/service/vip"
|
||||||
"chatapp3-golang/internal/service/voiceroomredpacket"
|
"chatapp3-golang/internal/service/voiceroomredpacket"
|
||||||
"chatapp3-golang/internal/service/voiceroomrocket"
|
"chatapp3-golang/internal/service/voiceroomrocket"
|
||||||
@ -49,7 +47,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appPopupService := apppopup.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
appPopupService := apppopup.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
||||||
errorLogService := errorlog.NewService(app.Repository.DB)
|
|
||||||
inviteService := invite.NewInviteService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
inviteService := invite.NewInviteService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
baishunService := baishun.NewBaishunService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
baishunService := baishun.NewBaishunService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
binanceRechargeService := binancerecharge.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
binanceRechargeService := binancerecharge.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
@ -61,7 +58,6 @@ func main() {
|
|||||||
rechargeRewardService := rechargereward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
rechargeRewardService := rechargereward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
signInRewardService := signinreward.NewSignInRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
taskCenterService := taskcenter.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
taskCenterService := taskcenter.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
userBadgeService := userbadge.NewService(app.Repository.DB)
|
|
||||||
vipService := vip.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
vipService := vip.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
wheelService := wheel.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
wheelService := wheel.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
smashEggService := smashegg.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
smashEggService := smashegg.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
@ -104,7 +100,6 @@ func main() {
|
|||||||
)
|
)
|
||||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||||
AppPopup: appPopupService,
|
AppPopup: appPopupService,
|
||||||
ErrorLog: errorLogService,
|
|
||||||
Invite: inviteService,
|
Invite: inviteService,
|
||||||
Baishun: baishunService,
|
Baishun: baishunService,
|
||||||
BinanceRecharge: binanceRechargeService,
|
BinanceRecharge: binanceRechargeService,
|
||||||
@ -122,7 +117,6 @@ func main() {
|
|||||||
SignInReward: signInRewardService,
|
SignInReward: signInRewardService,
|
||||||
SmashEgg: smashEggService,
|
SmashEgg: smashEggService,
|
||||||
TaskCenter: taskCenterService,
|
TaskCenter: taskCenterService,
|
||||||
UserBadge: userBadgeService,
|
|
||||||
VIP: vipService,
|
VIP: vipService,
|
||||||
Wheel: wheelService,
|
Wheel: wheelService,
|
||||||
VoiceRoomRedPacket: voiceRoomRedPacketService,
|
VoiceRoomRedPacket: voiceRoomRedPacketService,
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// AgoraErrorLog stores client-reported Agora room connection errors.
|
|
||||||
type AgoraErrorLog struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键ID
|
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32"` // 系统来源
|
|
||||||
UserID int64 `gorm:"column:user_id"` // 上报用户ID
|
|
||||||
RoomID string `gorm:"column:room_id;size:64"` // 房间ID
|
|
||||||
AgoraUID string `gorm:"column:agora_uid;size:64"` // Agora UID
|
|
||||||
ErrorCodeType string `gorm:"column:error_code_type;size:128"` // Agora ErrorCodeType
|
|
||||||
ConnectionChangedReasonType string `gorm:"column:connection_changed_reason_type;size:128"` // Agora ConnectionChangedReasonType
|
|
||||||
BannedByServer bool `gorm:"column:banned_by_server"` // 是否 BannedByServer/kicking-rule
|
|
||||||
IsTimeout bool `gorm:"column:is_timeout"` // 是否超时
|
|
||||||
TokenRequestSuccess *bool `gorm:"column:token_request_success"` // token 请求是否成功
|
|
||||||
NetworkType string `gorm:"column:network_type;size:64"` // 网络类型
|
|
||||||
ReqClient string `gorm:"column:req_client;size:32"` // 请求客户端
|
|
||||||
ReqAppIntel string `gorm:"column:req_app_intel;size:255"` // App 版本信息请求头
|
|
||||||
ClientIP string `gorm:"column:client_ip;size:64"` // 客户端IP
|
|
||||||
UserAgent string `gorm:"column:user_agent;size:255"` // User-Agent
|
|
||||||
RawPayload string `gorm:"column:raw_payload;type:mediumtext"` // 原始上报内容
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName returns the Agora error log table name.
|
|
||||||
func (AgoraErrorLog) TableName() string { return "agora_error_log" }
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// UserBadgeBackpack stores user-owned badge records from the Java badge system.
|
|
||||||
type UserBadgeBackpack struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
UserID int64 `gorm:"column:user_id;index:idx_user_badge_backpack_user"`
|
|
||||||
BadgeID int64 `gorm:"column:badge_id;index:idx_user_badge_backpack_badge"`
|
|
||||||
ExpireType string `gorm:"column:expire_type;size:32"`
|
|
||||||
ExpireTime time.Time `gorm:"column:expire_time"`
|
|
||||||
UseProps bool `gorm:"column:is_use_props"`
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName returns the user badge backpack table name.
|
|
||||||
func (UserBadgeBackpack) TableName() string { return "user_badge_backpack" }
|
|
||||||
|
|
||||||
// SysBadgeConfig stores badge metadata from the Java badge system.
|
|
||||||
type SysBadgeConfig struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
Type string `gorm:"column:type;size:64"`
|
|
||||||
BadgeLevel int `gorm:"column:badge_level"`
|
|
||||||
Milestone int64 `gorm:"column:milestone"`
|
|
||||||
BadgeName string `gorm:"column:badge_name;size:128"`
|
|
||||||
BadgeKey string `gorm:"column:badge_key;size:128"`
|
|
||||||
Del bool `gorm:"column:is_del"`
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName returns the badge config table name.
|
|
||||||
func (SysBadgeConfig) TableName() string { return "sys_badge_config" }
|
|
||||||
|
|
||||||
// SysBadgePictureConfig stores per-platform badge images.
|
|
||||||
type SysBadgePictureConfig struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
BadgeConfigID int64 `gorm:"column:badge_config_id;index:idx_badge_picture_config_badge"`
|
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_badge_picture_config_sys"`
|
|
||||||
SelectURL string `gorm:"column:select_url;size:512"`
|
|
||||||
NotSelectURL string `gorm:"column:not_select_url;size:512"`
|
|
||||||
AnimationURL string `gorm:"column:animation_url;size:512"`
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName returns the badge picture config table name.
|
|
||||||
func (SysBadgePictureConfig) TableName() string { return "sys_badge_picture_config" }
|
|
||||||
@ -28,7 +28,6 @@ type VoiceRoomRocketLevelConfig struct {
|
|||||||
Level int `gorm:"column:level;uniqueIndex:uk_voice_room_rocket_level,priority:2"`
|
Level int `gorm:"column:level;uniqueIndex:uk_voice_room_rocket_level,priority:2"`
|
||||||
NeedEnergy int64 `gorm:"column:need_energy"`
|
NeedEnergy int64 `gorm:"column:need_energy"`
|
||||||
ShakeThresholdPercent string `gorm:"column:shake_threshold_percent;type:decimal(8,4)"`
|
ShakeThresholdPercent string `gorm:"column:shake_threshold_percent;type:decimal(8,4)"`
|
||||||
PreviewRocketURL string `gorm:"column:preview_rocket_url;size:512"`
|
|
||||||
RocketIconURL string `gorm:"column:rocket_icon_url;size:512"`
|
RocketIconURL string `gorm:"column:rocket_icon_url;size:512"`
|
||||||
RocketAnimationURL string `gorm:"column:rocket_animation_url;size:512"`
|
RocketAnimationURL string `gorm:"column:rocket_animation_url;size:512"`
|
||||||
ProgressBarURL string `gorm:"column:progress_bar_url;size:512"`
|
ProgressBarURL string `gorm:"column:progress_bar_url;size:512"`
|
||||||
@ -52,7 +51,6 @@ type VoiceRoomRocketRewardConfig struct {
|
|||||||
RewardCover string `gorm:"column:reward_cover;size:512"`
|
RewardCover string `gorm:"column:reward_cover;size:512"`
|
||||||
RewardAmount int64 `gorm:"column:reward_amount"`
|
RewardAmount int64 `gorm:"column:reward_amount"`
|
||||||
ExpireDays *int `gorm:"column:expire_days"`
|
ExpireDays *int `gorm:"column:expire_days"`
|
||||||
RewardCopies int `gorm:"column:reward_copies;default:1"`
|
|
||||||
Weight int `gorm:"column:weight"`
|
Weight int `gorm:"column:weight"`
|
||||||
Sort int `gorm:"column:sort"`
|
Sort int `gorm:"column:sort"`
|
||||||
Enabled bool `gorm:"column:enabled;index:idx_voice_room_rocket_reward_scene,priority:4"`
|
Enabled bool `gorm:"column:enabled;index:idx_voice_room_rocket_reward_scene,priority:4"`
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/service/errorlog"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registerAgoraErrorLogRoutes registers client report and admin page APIs for Agora errors.
|
|
||||||
func registerAgoraErrorLogRoutes(engine *gin.Engine, javaClient authGateway, service *errorlog.Service) {
|
|
||||||
if service == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appGroup := engine.Group("/app/agora")
|
|
||||||
appGroup.Use(authMiddleware(javaClient))
|
|
||||||
appGroup.POST("/error-log", func(c *gin.Context) {
|
|
||||||
raw, err := io.ReadAll(c.Request.Body)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, errorlog.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var req errorlog.AgoraErrorReportRequest
|
|
||||||
if len(strings.TrimSpace(string(raw))) > 0 {
|
|
||||||
if err := json.Unmarshal(raw, &req); err != nil {
|
|
||||||
writeError(c, errorlog.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp, err := service.ReportAgoraError(
|
|
||||||
c.Request.Context(),
|
|
||||||
mustAuthUser(c),
|
|
||||||
req,
|
|
||||||
errorlog.AgoraErrorReportContext{
|
|
||||||
ReqClient: c.GetHeader("Req-Client"),
|
|
||||||
ReqAppIntel: c.GetHeader("Req-App-Intel"),
|
|
||||||
ClientIP: resolveClientIP(c),
|
|
||||||
UserAgent: c.GetHeader("User-Agent"),
|
|
||||||
RawPayload: string(raw),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
adminGroup := engine.Group("/app-system/error-logs/agora")
|
|
||||||
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
|
||||||
adminGroup.GET("/page", func(c *gin.Context) {
|
|
||||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
|
||||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
|
||||||
bannedOnly, _ := strconv.ParseBool(strings.TrimSpace(c.DefaultQuery("bannedOnly", "false")))
|
|
||||||
resp, err := service.PageAgoraErrorLogs(c.Request.Context(), errorlog.AgoraErrorLogQuery{
|
|
||||||
SysOrigin: c.Query("sysOrigin"),
|
|
||||||
UserID: parseInt64(c.Query("userId")),
|
|
||||||
RoomID: c.Query("roomId"),
|
|
||||||
AgoraUID: c.Query("agoraUid"),
|
|
||||||
ErrorCodeType: c.Query("errorCodeType"),
|
|
||||||
ReasonType: c.Query("reasonType"),
|
|
||||||
BannedOnly: bannedOnly,
|
|
||||||
NetworkType: c.Query("networkType"),
|
|
||||||
StartTime: c.Query("startTime"),
|
|
||||||
EndTime: c.Query("endTime"),
|
|
||||||
Cursor: cursor,
|
|
||||||
Limit: limit,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/apppopup"
|
"chatapp3-golang/internal/service/apppopup"
|
||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/binancerecharge"
|
"chatapp3-golang/internal/service/binancerecharge"
|
||||||
"chatapp3-golang/internal/service/errorlog"
|
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
"chatapp3-golang/internal/service/hostcenter"
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
@ -25,7 +24,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/signinreward"
|
"chatapp3-golang/internal/service/signinreward"
|
||||||
"chatapp3-golang/internal/service/smashegg"
|
"chatapp3-golang/internal/service/smashegg"
|
||||||
"chatapp3-golang/internal/service/taskcenter"
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
"chatapp3-golang/internal/service/userbadge"
|
|
||||||
"chatapp3-golang/internal/service/vip"
|
"chatapp3-golang/internal/service/vip"
|
||||||
"chatapp3-golang/internal/service/voiceroomredpacket"
|
"chatapp3-golang/internal/service/voiceroomredpacket"
|
||||||
"chatapp3-golang/internal/service/voiceroomrocket"
|
"chatapp3-golang/internal/service/voiceroomrocket"
|
||||||
@ -38,7 +36,6 @@ import (
|
|||||||
// Services 聚合所有业务模块服务,供路由层统一装配。
|
// Services 聚合所有业务模块服务,供路由层统一装配。
|
||||||
type Services struct {
|
type Services struct {
|
||||||
AppPopup *apppopup.Service
|
AppPopup *apppopup.Service
|
||||||
ErrorLog *errorlog.Service
|
|
||||||
Invite *invite.InviteService
|
Invite *invite.InviteService
|
||||||
Baishun *baishun.BaishunService
|
Baishun *baishun.BaishunService
|
||||||
BinanceRecharge *binancerecharge.Service
|
BinanceRecharge *binancerecharge.Service
|
||||||
@ -56,7 +53,6 @@ type Services struct {
|
|||||||
SignInReward *signinreward.SignInRewardService
|
SignInReward *signinreward.SignInRewardService
|
||||||
SmashEgg *smashegg.Service
|
SmashEgg *smashegg.Service
|
||||||
TaskCenter *taskcenter.Service
|
TaskCenter *taskcenter.Service
|
||||||
UserBadge *userbadge.Service
|
|
||||||
VIP *vip.Service
|
VIP *vip.Service
|
||||||
Wheel *wheel.Service
|
Wheel *wheel.Service
|
||||||
VoiceRoomRedPacket *voiceroomredpacket.Service
|
VoiceRoomRedPacket *voiceroomredpacket.Service
|
||||||
@ -76,7 +72,6 @@ func NewRouter(
|
|||||||
|
|
||||||
registerHealthRoutes(engine, cfg, repository)
|
registerHealthRoutes(engine, cfg, repository)
|
||||||
registerAppPopupRoutes(engine, javaClient, services.AppPopup)
|
registerAppPopupRoutes(engine, javaClient, services.AppPopup)
|
||||||
registerAgoraErrorLogRoutes(engine, javaClient, services.ErrorLog)
|
|
||||||
registerInviteRoutes(engine, javaClient, services.Invite)
|
registerInviteRoutes(engine, javaClient, services.Invite)
|
||||||
registerRechargeAgencyRoutes(engine, javaClient, services.RechargeAgency)
|
registerRechargeAgencyRoutes(engine, javaClient, services.RechargeAgency)
|
||||||
registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward)
|
registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward)
|
||||||
@ -84,7 +79,6 @@ func NewRouter(
|
|||||||
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
registerRegisterRewardRoutes(engine, javaClient, services.RegisterReward)
|
||||||
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
registerSignInRewardRoutes(engine, javaClient, services.SignInReward)
|
||||||
registerTaskCenterRoutes(engine, cfg, javaClient, services.TaskCenter)
|
registerTaskCenterRoutes(engine, cfg, javaClient, services.TaskCenter)
|
||||||
registerUserBadgeRoutes(engine, javaClient, services.UserBadge)
|
|
||||||
registerVipRoutes(engine, javaClient, services.VIP)
|
registerVipRoutes(engine, javaClient, services.VIP)
|
||||||
registerWheelRoutes(engine, javaClient, services.Wheel)
|
registerWheelRoutes(engine, javaClient, services.Wheel)
|
||||||
registerSmashEggRoutes(engine, javaClient, services.SmashEgg)
|
registerSmashEggRoutes(engine, javaClient, services.SmashEgg)
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatapp3-golang/internal/service/userbadge"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registerUserBadgeRoutes registers App user badge APIs.
|
|
||||||
func registerUserBadgeRoutes(engine *gin.Engine, javaClient authGateway, service *userbadge.Service) {
|
|
||||||
if service == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appGroup := engine.Group("/app/user/badge")
|
|
||||||
appGroup.Use(authMiddleware(javaClient))
|
|
||||||
appGroup.GET("/current", func(c *gin.Context) {
|
|
||||||
resp, err := service.Current(c.Request.Context(), mustAuthUser(c))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
appGroup.GET("/other", func(c *gin.Context) {
|
|
||||||
targetUserID := userbadge.ParseUserID(c.Query("userId"))
|
|
||||||
if targetUserID <= 0 {
|
|
||||||
targetUserID = userbadge.ParseUserID(c.Query("targetUserId"))
|
|
||||||
}
|
|
||||||
resp, err := service.Other(c.Request.Context(), mustAuthUser(c), targetUserID)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,199 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/repo"
|
|
||||||
"chatapp3-golang/internal/service/userbadge"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type userBadgeAuthStub struct{}
|
|
||||||
|
|
||||||
func (userBadgeAuthStub) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
|
||||||
return integration.UserCredential{UserID: 123, SysOrigin: "LIKEI"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (userBadgeAuthStub) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
|
||||||
return integration.ConsoleAccount{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserBadgeCurrentRoute(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := newRouterUserBadgeDB(t)
|
|
||||||
now := time.Now().Add(time.Hour)
|
|
||||||
if err := db.Create(&model.UserBadgeBackpack{
|
|
||||||
ID: 1,
|
|
||||||
UserID: 123,
|
|
||||||
BadgeID: 501,
|
|
||||||
ExpireType: "TEMPORARY",
|
|
||||||
ExpireTime: now,
|
|
||||||
UseProps: true,
|
|
||||||
UpdateTime: time.Now(),
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create backpack: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.Create(&model.SysBadgeConfig{
|
|
||||||
ID: 501,
|
|
||||||
Type: "ACTIVITY",
|
|
||||||
BadgeName: "Activity Badge",
|
|
||||||
BadgeKey: "ACTIVITY_BADGE",
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create badge config: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.Create(&model.SysBadgePictureConfig{
|
|
||||||
ID: 601,
|
|
||||||
BadgeConfigID: 501,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
SelectURL: "https://cdn.example.com/activity.png",
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create badge picture: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := userbadge.NewService(db)
|
|
||||||
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/current", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload map[string]any
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("decode response: %v", err)
|
|
||||||
}
|
|
||||||
data, ok := payload["data"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("data missing: %#v", payload)
|
|
||||||
}
|
|
||||||
shortBadges, ok := data["shortBadges"].([]any)
|
|
||||||
if !ok || len(shortBadges) != 1 {
|
|
||||||
t.Fatalf("shortBadges = %#v", data["shortBadges"])
|
|
||||||
}
|
|
||||||
item, ok := shortBadges[0].(map[string]any)
|
|
||||||
if !ok || item["type"] != "SHORT" || item["badgeId"] != "501" {
|
|
||||||
t.Fatalf("short badge item = %#v", shortBadges[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserBadgeCurrentRouteRequiresAuth(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
service := userbadge.NewService(newRouterUserBadgeDB(t))
|
|
||||||
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/current", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusUnauthorized {
|
|
||||||
t.Fatalf("status = %d body = %s, want 401", rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserBadgeOtherRoute(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := newRouterUserBadgeDB(t)
|
|
||||||
targetUserID := int64(456)
|
|
||||||
if err := db.Create(&model.UserBadgeBackpack{
|
|
||||||
ID: 2,
|
|
||||||
UserID: targetUserID,
|
|
||||||
BadgeID: 502,
|
|
||||||
ExpireType: "TEMPORARY",
|
|
||||||
ExpireTime: time.Now().Add(time.Hour),
|
|
||||||
UseProps: true,
|
|
||||||
UpdateTime: time.Now(),
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create backpack: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.Create(&model.SysBadgeConfig{
|
|
||||||
ID: 502,
|
|
||||||
Type: "ACHIEVEMENT",
|
|
||||||
BadgeName: "Achievement Badge",
|
|
||||||
BadgeKey: "ACHIEVEMENT_BADGE",
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create badge config: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.Create(&model.SysBadgePictureConfig{
|
|
||||||
ID: 602,
|
|
||||||
BadgeConfigID: 502,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
SelectURL: "https://cdn.example.com/achievement.png",
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create badge picture: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := userbadge.NewService(db)
|
|
||||||
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/other?userId=456", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
var payload map[string]any
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
|
||||||
t.Fatalf("decode response: %v", err)
|
|
||||||
}
|
|
||||||
data, ok := payload["data"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("data missing: %#v", payload)
|
|
||||||
}
|
|
||||||
if data["userId"] != float64(targetUserID) {
|
|
||||||
t.Fatalf("userId = %#v, want target user", data["userId"])
|
|
||||||
}
|
|
||||||
shortBadges, ok := data["shortBadges"].([]any)
|
|
||||||
if !ok || len(shortBadges) != 1 {
|
|
||||||
t.Fatalf("shortBadges = %#v", data["shortBadges"])
|
|
||||||
}
|
|
||||||
item, ok := shortBadges[0].(map[string]any)
|
|
||||||
if !ok || item["sourceType"] != "ACHIEVEMENT" || item["badgeId"] != "502" {
|
|
||||||
t.Fatalf("short badge item = %#v", shortBadges[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserBadgeOtherRouteRequiresTargetUser(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
service := userbadge.NewService(newRouterUserBadgeDB(t))
|
|
||||||
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/other", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer token")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
engine.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("status = %d body = %s, want 400", rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRouterUserBadgeDB(t *testing.T) *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.UserVipState{},
|
|
||||||
&model.UserBadgeBackpack{},
|
|
||||||
&model.SysBadgeConfig{},
|
|
||||||
&model.SysBadgePictureConfig{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("migrate sqlite: %v", err)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
package errorlog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReportAgoraError persists one client-reported Agora error log.
|
|
||||||
func (s *Service) ReportAgoraError(ctx context.Context, user AuthUser, req AgoraErrorReportRequest, meta AgoraErrorReportContext) (*AgoraErrorReportResponse, error) {
|
|
||||||
if s == nil || s.db == nil {
|
|
||||||
return nil, NewAppError(http.StatusServiceUnavailable, "error_log_unavailable", "error log service is unavailable")
|
|
||||||
}
|
|
||||||
roomID := req.RoomID.String()
|
|
||||||
if roomID == "" {
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "room_id_required", "roomId is required")
|
|
||||||
}
|
|
||||||
reasonType := firstNonBlank(req.ConnectionChangedReasonType, req.ReasonType, req.Reason)
|
|
||||||
bannedByServer := isBannedByServer(reasonType)
|
|
||||||
|
|
||||||
id, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
row := model.AgoraErrorLog{
|
|
||||||
ID: id,
|
|
||||||
SysOrigin: normalizeSysOrigin(user.SysOrigin),
|
|
||||||
UserID: user.UserID,
|
|
||||||
RoomID: trimMax(roomID, 64),
|
|
||||||
AgoraUID: trimMax(req.AgoraUID.String(), 64),
|
|
||||||
ErrorCodeType: trimMax(req.ErrorCodeType, 128),
|
|
||||||
ConnectionChangedReasonType: trimMax(reasonType, 128),
|
|
||||||
BannedByServer: bannedByServer,
|
|
||||||
IsTimeout: boolValue(req.Timeout, req.IsTimeout),
|
|
||||||
TokenRequestSuccess: boolPtrValue(req.TokenRequestSuccess, req.TokenSuccess),
|
|
||||||
NetworkType: trimMax(req.NetworkType, 64),
|
|
||||||
ReqClient: trimMax(meta.ReqClient, 32),
|
|
||||||
ReqAppIntel: trimMax(meta.ReqAppIntel, 255),
|
|
||||||
ClientIP: trimMax(meta.ClientIP, 64),
|
|
||||||
UserAgent: trimMax(meta.UserAgent, 255),
|
|
||||||
RawPayload: strings.TrimSpace(meta.RawPayload),
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &AgoraErrorReportResponse{
|
|
||||||
ID: strconv.FormatInt(id, 10),
|
|
||||||
BannedByServer: bannedByServer,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageAgoraErrorLogs returns admin Agora error logs.
|
|
||||||
func (s *Service) PageAgoraErrorLogs(ctx context.Context, query AgoraErrorLogQuery) (*AgoraErrorLogPageResponse, error) {
|
|
||||||
if s == nil || s.db == nil {
|
|
||||||
return nil, NewAppError(http.StatusServiceUnavailable, "error_log_unavailable", "error log service is unavailable")
|
|
||||||
}
|
|
||||||
cursor, limit := normalizePage(query.Cursor, query.Limit)
|
|
||||||
startTime, endTime, err := parseAdminTimeRange(query.StartTime, query.EndTime)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
filter := query
|
|
||||||
filter.Cursor = cursor
|
|
||||||
filter.Limit = limit
|
|
||||||
filter.StartAt = startTime
|
|
||||||
filter.EndAt = endTime
|
|
||||||
|
|
||||||
base := s.agoraErrorLogQuery(ctx, filter)
|
|
||||||
var total int64
|
|
||||||
if err := base.Count(&total).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []model.AgoraErrorLog
|
|
||||||
if err := s.agoraErrorLogQuery(ctx, filter).
|
|
||||||
Order("create_time desc, id desc").
|
|
||||||
Offset((cursor - 1) * limit).
|
|
||||||
Limit(limit).
|
|
||||||
Find(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &AgoraErrorLogPageResponse{
|
|
||||||
Records: agoraErrorLogItems(rows),
|
|
||||||
Total: total,
|
|
||||||
Current: cursor,
|
|
||||||
Size: limit,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgoraErrorLogQuery is the admin filter for Agora error logs.
|
|
||||||
type AgoraErrorLogQuery struct {
|
|
||||||
SysOrigin string
|
|
||||||
UserID int64
|
|
||||||
RoomID string
|
|
||||||
AgoraUID string
|
|
||||||
ErrorCodeType string
|
|
||||||
ReasonType string
|
|
||||||
BannedOnly bool
|
|
||||||
NetworkType string
|
|
||||||
StartTime string
|
|
||||||
EndTime string
|
|
||||||
Cursor int
|
|
||||||
Limit int
|
|
||||||
StartAt time.Time
|
|
||||||
EndAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) agoraErrorLogQuery(ctx context.Context, filter AgoraErrorLogQuery) *gorm.DB {
|
|
||||||
query := s.db.WithContext(ctx).Model(&model.AgoraErrorLog{}).
|
|
||||||
Where("create_time >= ? AND create_time <= ?", filter.StartAt, filter.EndAt)
|
|
||||||
if sysOrigin := normalizeSysOrigin(filter.SysOrigin); sysOrigin != "" {
|
|
||||||
query = query.Where("sys_origin = ?", sysOrigin)
|
|
||||||
}
|
|
||||||
if filter.UserID > 0 {
|
|
||||||
query = query.Where("user_id = ?", filter.UserID)
|
|
||||||
}
|
|
||||||
if roomID := strings.TrimSpace(filter.RoomID); roomID != "" {
|
|
||||||
query = query.Where("room_id = ?", roomID)
|
|
||||||
}
|
|
||||||
if agoraUID := strings.TrimSpace(filter.AgoraUID); agoraUID != "" {
|
|
||||||
query = query.Where("agora_uid = ?", agoraUID)
|
|
||||||
}
|
|
||||||
if codeType := strings.TrimSpace(filter.ErrorCodeType); codeType != "" {
|
|
||||||
query = query.Where("error_code_type LIKE ?", "%"+codeType+"%")
|
|
||||||
}
|
|
||||||
if reasonType := strings.TrimSpace(filter.ReasonType); reasonType != "" {
|
|
||||||
query = query.Where("connection_changed_reason_type LIKE ?", "%"+reasonType+"%")
|
|
||||||
}
|
|
||||||
if filter.BannedOnly {
|
|
||||||
query = query.Where("banned_by_server = ?", true)
|
|
||||||
}
|
|
||||||
if networkType := strings.TrimSpace(filter.NetworkType); networkType != "" {
|
|
||||||
query = query.Where("network_type = ?", networkType)
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
func agoraErrorLogItems(rows []model.AgoraErrorLog) []AgoraErrorLogItem {
|
|
||||||
items := make([]AgoraErrorLogItem, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, AgoraErrorLogItem{
|
|
||||||
ID: strconv.FormatInt(row.ID, 10),
|
|
||||||
SysOrigin: row.SysOrigin,
|
|
||||||
UserID: strconv.FormatInt(row.UserID, 10),
|
|
||||||
RoomID: row.RoomID,
|
|
||||||
AgoraUID: row.AgoraUID,
|
|
||||||
ErrorCodeType: row.ErrorCodeType,
|
|
||||||
ConnectionChangedReasonType: row.ConnectionChangedReasonType,
|
|
||||||
BannedByServer: row.BannedByServer,
|
|
||||||
IsTimeout: row.IsTimeout,
|
|
||||||
TokenRequestSuccess: row.TokenRequestSuccess,
|
|
||||||
NetworkType: row.NetworkType,
|
|
||||||
ReqClient: row.ReqClient,
|
|
||||||
ReqAppIntel: row.ReqAppIntel,
|
|
||||||
ClientIP: row.ClientIP,
|
|
||||||
UserAgent: row.UserAgent,
|
|
||||||
RawPayload: row.RawPayload,
|
|
||||||
CreateTime: formatDateTime(row.CreateTime),
|
|
||||||
UpdateTime: formatDateTime(row.UpdateTime),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
package errorlog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReportAgoraErrorDetectsBannedByServer(t *testing.T) {
|
|
||||||
service := newTestService(t)
|
|
||||||
tokenOK := false
|
|
||||||
timeout := true
|
|
||||||
|
|
||||||
resp, err := service.ReportAgoraError(context.Background(), common.AuthUser{
|
|
||||||
UserID: 1234567,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
}, AgoraErrorReportRequest{
|
|
||||||
RoomID: StringValue("9001"),
|
|
||||||
AgoraUID: StringValue("8888"),
|
|
||||||
ErrorCodeType: "ERR_LEAVE_CHANNEL_REJECTED",
|
|
||||||
ConnectionChangedReasonType: "BannedByServer(3)",
|
|
||||||
Timeout: &timeout,
|
|
||||||
TokenRequestSuccess: &tokenOK,
|
|
||||||
NetworkType: "wifi",
|
|
||||||
}, AgoraErrorReportContext{
|
|
||||||
ReqClient: "Android",
|
|
||||||
ReqAppIntel: "version=1.0.0;build=100",
|
|
||||||
ClientIP: "127.0.0.1",
|
|
||||||
RawPayload: `{"roomId":"9001","connectionChangedReasonType":"BannedByServer(3)"}`,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReportAgoraError returned error: %v", err)
|
|
||||||
}
|
|
||||||
if !resp.BannedByServer {
|
|
||||||
t.Fatalf("BannedByServer = false, want true")
|
|
||||||
}
|
|
||||||
|
|
||||||
page, err := service.PageAgoraErrorLogs(context.Background(), AgoraErrorLogQuery{
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
BannedOnly: true,
|
|
||||||
Cursor: 1,
|
|
||||||
Limit: 20,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("PageAgoraErrorLogs returned error: %v", err)
|
|
||||||
}
|
|
||||||
if page.Total != 1 || len(page.Records) != 1 {
|
|
||||||
t.Fatalf("page total=%d records=%d, want 1", page.Total, len(page.Records))
|
|
||||||
}
|
|
||||||
record := page.Records[0]
|
|
||||||
if record.RoomID != "9001" || record.AgoraUID != "8888" || !record.IsTimeout {
|
|
||||||
t.Fatalf("unexpected record: %+v", record)
|
|
||||||
}
|
|
||||||
if record.TokenRequestSuccess == nil || *record.TokenRequestSuccess {
|
|
||||||
t.Fatalf("TokenRequestSuccess = %v, want false", record.TokenRequestSuccess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStringValueAcceptsNumericJSON(t *testing.T) {
|
|
||||||
var value StringValue
|
|
||||||
if err := value.UnmarshalJSON([]byte(`1838887943867809794`)); err != nil {
|
|
||||||
t.Fatalf("UnmarshalJSON returned error: %v", err)
|
|
||||||
}
|
|
||||||
if value.String() != "1838887943867809794" {
|
|
||||||
t.Fatalf("value = %q, want numeric string", value.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestService(t *testing.T) *Service {
|
|
||||||
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.AgoraErrorLog{}); err != nil {
|
|
||||||
t.Fatalf("auto migrate: %v", err)
|
|
||||||
}
|
|
||||||
return NewService(db)
|
|
||||||
}
|
|
||||||
@ -1,263 +0,0 @@
|
|||||||
package errorlog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const adminTimeLayout = "2006-01-02 15:04:05"
|
|
||||||
|
|
||||||
type AppError = common.AppError
|
|
||||||
type AuthUser = common.AuthUser
|
|
||||||
|
|
||||||
var NewAppError = common.NewAppError
|
|
||||||
|
|
||||||
type dbHandle interface {
|
|
||||||
WithContext(ctx context.Context) *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service handles app error logs.
|
|
||||||
type Service struct {
|
|
||||||
db dbHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService creates an app error log service.
|
|
||||||
func NewService(db dbHandle) *Service {
|
|
||||||
return &Service{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringValue accepts JSON string or number and stores it as a string.
|
|
||||||
type StringValue string
|
|
||||||
|
|
||||||
func (v StringValue) String() string {
|
|
||||||
return strings.TrimSpace(string(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON accepts quoted strings and numeric IDs from clients.
|
|
||||||
func (v *StringValue) UnmarshalJSON(data []byte) error {
|
|
||||||
raw := strings.TrimSpace(string(data))
|
|
||||||
if raw == "" || raw == "null" {
|
|
||||||
*v = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(raw, "\"") {
|
|
||||||
unquoted, err := strconv.Unquote(raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = StringValue(strings.TrimSpace(unquoted))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
*v = StringValue(raw)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgoraErrorReportRequest is the client report payload for Agora errors.
|
|
||||||
type AgoraErrorReportRequest struct {
|
|
||||||
RoomID StringValue `json:"roomId"`
|
|
||||||
AgoraUID StringValue `json:"agoraUid"`
|
|
||||||
ErrorCodeType string `json:"errorCodeType"`
|
|
||||||
ConnectionChangedReasonType string `json:"connectionChangedReasonType"`
|
|
||||||
ReasonType string `json:"reasonType"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
Timeout *bool `json:"timeout"`
|
|
||||||
IsTimeout *bool `json:"isTimeout"`
|
|
||||||
TokenRequestSuccess *bool `json:"tokenRequestSuccess"`
|
|
||||||
TokenSuccess *bool `json:"tokenSuccess"`
|
|
||||||
NetworkType string `json:"networkType"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgoraErrorReportContext carries request metadata captured at the router layer.
|
|
||||||
type AgoraErrorReportContext struct {
|
|
||||||
ReqClient string
|
|
||||||
ReqAppIntel string
|
|
||||||
ClientIP string
|
|
||||||
UserAgent string
|
|
||||||
RawPayload string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgoraErrorReportResponse is returned after a report is persisted.
|
|
||||||
type AgoraErrorReportResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
BannedByServer bool `json:"bannedByServer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgoraErrorLogPageResponse is the admin page response.
|
|
||||||
type AgoraErrorLogPageResponse struct {
|
|
||||||
Records []AgoraErrorLogItem `json:"records"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Current int `json:"current"`
|
|
||||||
Size int `json:"size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgoraErrorLogItem is an admin-readable Agora error log.
|
|
||||||
type AgoraErrorLogItem struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
UserID string `json:"userId"`
|
|
||||||
RoomID string `json:"roomId"`
|
|
||||||
AgoraUID string `json:"agoraUid"`
|
|
||||||
ErrorCodeType string `json:"errorCodeType"`
|
|
||||||
ConnectionChangedReasonType string `json:"connectionChangedReasonType"`
|
|
||||||
BannedByServer bool `json:"bannedByServer"`
|
|
||||||
IsTimeout bool `json:"isTimeout"`
|
|
||||||
TokenRequestSuccess *bool `json:"tokenRequestSuccess"`
|
|
||||||
NetworkType string `json:"networkType"`
|
|
||||||
ReqClient string `json:"reqClient"`
|
|
||||||
ReqAppIntel string `json:"reqAppIntel"`
|
|
||||||
ClientIP string `json:"clientIp"`
|
|
||||||
UserAgent string `json:"userAgent"`
|
|
||||||
RawPayload string `json:"rawPayload"`
|
|
||||||
CreateTime string `json:"createTime"`
|
|
||||||
UpdateTime string `json:"updateTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePage(cursor int, limit int) (int, int) {
|
|
||||||
if cursor <= 0 {
|
|
||||||
cursor = 1
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 20
|
|
||||||
}
|
|
||||||
if limit > 100 {
|
|
||||||
limit = 100
|
|
||||||
}
|
|
||||||
return cursor, limit
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeSysOrigin(sysOrigin string) string {
|
|
||||||
return strings.ToUpper(strings.TrimSpace(sysOrigin))
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimMax(value string, max int) string {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if max > 0 && len(value) > max {
|
|
||||||
return value[:max]
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolValue(primary *bool, aliases ...*bool) bool {
|
|
||||||
if primary != nil {
|
|
||||||
return *primary
|
|
||||||
}
|
|
||||||
for _, alias := range aliases {
|
|
||||||
if alias != nil {
|
|
||||||
return *alias
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolPtrValue(primary *bool, aliases ...*bool) *bool {
|
|
||||||
if primary != nil {
|
|
||||||
return primary
|
|
||||||
}
|
|
||||||
for _, alias := range aliases {
|
|
||||||
if alias != nil {
|
|
||||||
return alias
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstNonBlank(values ...string) string {
|
|
||||||
for _, value := range values {
|
|
||||||
if value = strings.TrimSpace(value); value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBannedByServer(reason string) bool {
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(reason))
|
|
||||||
if normalized == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return normalized == "3" ||
|
|
||||||
strings.Contains(normalized, "bannedbyserver") ||
|
|
||||||
strings.Contains(normalized, "banned_by_server") ||
|
|
||||||
strings.Contains(normalized, "banned by server")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDateTime(value time.Time) string {
|
|
||||||
if value.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return value.Format(adminTimeLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAdminTimeRange(startRaw string, endRaw string) (time.Time, time.Time, error) {
|
|
||||||
startRaw = strings.TrimSpace(startRaw)
|
|
||||||
endRaw = strings.TrimSpace(endRaw)
|
|
||||||
now := time.Now()
|
|
||||||
if startRaw == "" && endRaw == "" {
|
|
||||||
return now.AddDate(0, 0, -7), now, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var start time.Time
|
|
||||||
var end time.Time
|
|
||||||
var err error
|
|
||||||
if startRaw != "" {
|
|
||||||
start, err = parseAdminTime(startRaw, false)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, time.Time{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if endRaw != "" {
|
|
||||||
end, err = parseAdminTime(endRaw, true)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, time.Time{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if startRaw == "" {
|
|
||||||
start = end.AddDate(0, 0, -7)
|
|
||||||
}
|
|
||||||
if endRaw == "" {
|
|
||||||
end = start.AddDate(0, 0, 7)
|
|
||||||
}
|
|
||||||
if end.Before(start) {
|
|
||||||
return time.Time{}, time.Time{}, NewAppError(http.StatusBadRequest, "bad_time_range", "endTime must be greater than startTime")
|
|
||||||
}
|
|
||||||
return start, end, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAdminTime(raw string, endOfDay bool) (time.Time, error) {
|
|
||||||
raw = strings.TrimSpace(raw)
|
|
||||||
if raw == "" {
|
|
||||||
return time.Time{}, nil
|
|
||||||
}
|
|
||||||
if millis, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
|
||||||
return time.UnixMilli(millis), nil
|
|
||||||
}
|
|
||||||
for _, layout := range []string{
|
|
||||||
adminTimeLayout,
|
|
||||||
time.RFC3339,
|
|
||||||
"2006-01-02T15:04:05.000Z07:00",
|
|
||||||
"2006-01-02",
|
|
||||||
} {
|
|
||||||
var parsed time.Time
|
|
||||||
var err error
|
|
||||||
if strings.Contains(layout, "Z07:00") || layout == time.RFC3339 {
|
|
||||||
parsed, err = time.Parse(layout, raw)
|
|
||||||
} else {
|
|
||||||
parsed, err = time.ParseInLocation(layout, raw, time.Local)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if layout == "2006-01-02" && endOfDay {
|
|
||||||
parsed = parsed.Add(24*time.Hour - time.Nanosecond)
|
|
||||||
}
|
|
||||||
return parsed, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return time.Time{}, NewAppError(http.StatusBadRequest, "bad_time", fmt.Sprintf("invalid time: %s", raw))
|
|
||||||
}
|
|
||||||
@ -1,223 +0,0 @@
|
|||||||
package userbadge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type badgePictureRow struct {
|
|
||||||
BadgeID int64 `gorm:"column:badge_id"`
|
|
||||||
SourceType sql.NullString `gorm:"column:source_type"`
|
|
||||||
BadgeLevel sql.NullInt64 `gorm:"column:badge_level"`
|
|
||||||
Milestone sql.NullInt64 `gorm:"column:milestone"`
|
|
||||||
BadgeName sql.NullString `gorm:"column:badge_name"`
|
|
||||||
BadgeKey sql.NullString `gorm:"column:badge_key"`
|
|
||||||
SelectURL sql.NullString `gorm:"column:select_url"`
|
|
||||||
NotSelectURL sql.NullString `gorm:"column:not_select_url"`
|
|
||||||
AnimationURL sql.NullString `gorm:"column:animation_url"`
|
|
||||||
ExpireTime time.Time `gorm:"column:expire_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current returns the current user's badge list split by long and short display type.
|
|
||||||
func (s *Service) Current(ctx context.Context, user AuthUser) (*CurrentBadgeListResponse, error) {
|
|
||||||
if s == nil || s.db == nil {
|
|
||||||
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
|
|
||||||
}
|
|
||||||
sysOrigin, err := normalizeUser(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s.currentForUser(ctx, sysOrigin, user.UserID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other returns another user's badge list in the current authenticated sysOrigin.
|
|
||||||
func (s *Service) Other(ctx context.Context, viewer AuthUser, targetUserID int64) (*CurrentBadgeListResponse, error) {
|
|
||||||
if s == nil || s.db == nil {
|
|
||||||
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
|
|
||||||
}
|
|
||||||
sysOrigin, err := normalizeUser(viewer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if targetUserID <= 0 {
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "invalid_target_user_id", "target userId is required")
|
|
||||||
}
|
|
||||||
return s.currentForUser(ctx, sysOrigin, targetUserID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) currentForUser(ctx context.Context, sysOrigin string, userID int64) (*CurrentBadgeListResponse, error) {
|
|
||||||
now := s.now()
|
|
||||||
longBadges, vipBadgeID, err := s.loadLongBadges(ctx, sysOrigin, userID, now)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
shortBadges, err := s.loadShortBadges(ctx, sysOrigin, userID, vipBadgeID, now)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
badges := make([]CurrentBadgeItem, 0, len(longBadges)+len(shortBadges))
|
|
||||||
badges = append(badges, longBadges...)
|
|
||||||
badges = append(badges, shortBadges...)
|
|
||||||
|
|
||||||
return &CurrentBadgeListResponse{
|
|
||||||
UserID: userID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
Badges: badges,
|
|
||||||
LongBadges: longBadges,
|
|
||||||
ShortBadges: shortBadges,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadLongBadges(ctx context.Context, sysOrigin string, userID int64, now time.Time) ([]CurrentBadgeItem, int64, error) {
|
|
||||||
var state model.UserVipState
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Where("sys_origin = ? AND user_id = ?", sysOrigin, userID).
|
|
||||||
First(&state).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return []CurrentBadgeItem{}, 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if !isActiveVIPState(state, now) || state.BadgeResourceID <= 0 {
|
|
||||||
return []CurrentBadgeItem{}, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
row, err := s.loadBadgePicture(ctx, sysOrigin, state.BadgeResourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
item := CurrentBadgeItem{
|
|
||||||
ID: formatID(state.BadgeResourceID),
|
|
||||||
BadgeID: formatID(state.BadgeResourceID),
|
|
||||||
Type: badgeDisplayTypeLong,
|
|
||||||
SourceType: badgeSourceTypeVIP,
|
|
||||||
BadgeLevel: nullInt(row.BadgeLevel),
|
|
||||||
BadgeKey: nullString(row.BadgeKey),
|
|
||||||
Level: state.Level,
|
|
||||||
LevelCode: state.LevelCode,
|
|
||||||
DisplayName: state.DisplayName,
|
|
||||||
Use: true,
|
|
||||||
ExpireTime: formatMillis(state.ExpireAt),
|
|
||||||
ExpireAt: formatTime(state.ExpireAt),
|
|
||||||
}
|
|
||||||
item.BadgeName = firstNonEmpty(nullString(row.BadgeName), state.BadgeName, state.DisplayName)
|
|
||||||
item.Name = item.BadgeName
|
|
||||||
item.SelectURL = firstNonEmpty(nullString(row.SelectURL), state.BadgeURL)
|
|
||||||
item.NotSelectURL = nullString(row.NotSelectURL)
|
|
||||||
item.AnimationURL = nullString(row.AnimationURL)
|
|
||||||
item.URL = firstNonEmpty(item.SelectURL, state.BadgeURL)
|
|
||||||
if row.Milestone.Valid && row.Milestone.Int64 > 0 {
|
|
||||||
item.Milestone = formatID(row.Milestone.Int64)
|
|
||||||
}
|
|
||||||
if item.Name == "" && item.URL == "" {
|
|
||||||
return []CurrentBadgeItem{}, state.BadgeResourceID, nil
|
|
||||||
}
|
|
||||||
return []CurrentBadgeItem{item}, state.BadgeResourceID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadShortBadges(ctx context.Context, sysOrigin string, userID int64, excludeBadgeID int64, now time.Time) ([]CurrentBadgeItem, error) {
|
|
||||||
rows := make([]badgePictureRow, 0)
|
|
||||||
query := s.db.WithContext(ctx).
|
|
||||||
Table("user_badge_backpack AS backpack").
|
|
||||||
Select(`
|
|
||||||
backpack.badge_id AS badge_id,
|
|
||||||
backpack.expire_time AS expire_time,
|
|
||||||
config.type AS source_type,
|
|
||||||
config.badge_level AS badge_level,
|
|
||||||
config.milestone AS milestone,
|
|
||||||
config.badge_name AS badge_name,
|
|
||||||
config.badge_key AS badge_key,
|
|
||||||
picture.select_url AS select_url,
|
|
||||||
picture.not_select_url AS not_select_url,
|
|
||||||
picture.animation_url AS animation_url
|
|
||||||
`).
|
|
||||||
Joins("JOIN sys_badge_config AS config ON config.id = backpack.badge_id").
|
|
||||||
Joins("JOIN sys_badge_picture_config AS picture ON picture.badge_config_id = config.id AND picture.sys_origin = ?", sysOrigin).
|
|
||||||
Where("backpack.user_id = ?", userID).
|
|
||||||
Where("backpack.is_use_props = ?", true).
|
|
||||||
Where("config.is_del = ?", false).
|
|
||||||
Where("config.type IN ?", []string{badgeSourceTypeActivity, badgeSourceTypeAchievement}).
|
|
||||||
Where("(backpack.expire_type = ? OR backpack.expire_time > ?)", expireTypePermanent, now).
|
|
||||||
Order("backpack.update_time DESC, backpack.id DESC")
|
|
||||||
if excludeBadgeID > 0 {
|
|
||||||
query = query.Where("backpack.badge_id <> ?", excludeBadgeID)
|
|
||||||
}
|
|
||||||
if err := query.Scan(&rows).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
items := make([]CurrentBadgeItem, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
item := CurrentBadgeItem{
|
|
||||||
ID: formatID(row.BadgeID),
|
|
||||||
BadgeID: formatID(row.BadgeID),
|
|
||||||
Type: badgeDisplayTypeShort,
|
|
||||||
SourceType: nullString(row.SourceType),
|
|
||||||
Name: nullString(row.BadgeName),
|
|
||||||
BadgeName: nullString(row.BadgeName),
|
|
||||||
BadgeKey: nullString(row.BadgeKey),
|
|
||||||
BadgeLevel: nullInt(row.BadgeLevel),
|
|
||||||
URL: nullString(row.SelectURL),
|
|
||||||
SelectURL: nullString(row.SelectURL),
|
|
||||||
NotSelectURL: nullString(row.NotSelectURL),
|
|
||||||
AnimationURL: nullString(row.AnimationURL),
|
|
||||||
Use: true,
|
|
||||||
ExpireTime: formatMillis(row.ExpireTime),
|
|
||||||
ExpireAt: formatTime(row.ExpireTime),
|
|
||||||
}
|
|
||||||
if row.Milestone.Valid && row.Milestone.Int64 > 0 {
|
|
||||||
item.Milestone = formatID(row.Milestone.Int64)
|
|
||||||
}
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadBadgePicture(ctx context.Context, sysOrigin string, badgeID int64) (badgePictureRow, error) {
|
|
||||||
var row badgePictureRow
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Table("sys_badge_config AS config").
|
|
||||||
Select(`
|
|
||||||
config.id AS badge_id,
|
|
||||||
config.type AS source_type,
|
|
||||||
config.badge_level AS badge_level,
|
|
||||||
config.milestone AS milestone,
|
|
||||||
config.badge_name AS badge_name,
|
|
||||||
config.badge_key AS badge_key,
|
|
||||||
picture.select_url AS select_url,
|
|
||||||
picture.not_select_url AS not_select_url,
|
|
||||||
picture.animation_url AS animation_url
|
|
||||||
`).
|
|
||||||
Joins("LEFT JOIN sys_badge_picture_config AS picture ON picture.badge_config_id = config.id AND picture.sys_origin = ?", sysOrigin).
|
|
||||||
Where("config.id = ? AND config.is_del = ?", badgeID, false).
|
|
||||||
Scan(&row).Error
|
|
||||||
return row, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func isActiveVIPState(state model.UserVipState, now time.Time) bool {
|
|
||||||
return strings.EqualFold(strings.TrimSpace(state.Status), statusActive) && state.Level > 0 && state.ExpireAt.After(now)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullString(value sql.NullString) string {
|
|
||||||
if !value.Valid {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(value.String)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullInt(value sql.NullInt64) int {
|
|
||||||
if !value.Valid {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(value.Int64)
|
|
||||||
}
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
package userbadge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCurrentBadgesSplitLongAndShort(t *testing.T) {
|
|
||||||
db := newUserBadgeTestDB(t)
|
|
||||||
now := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)
|
|
||||||
userID := int64(42)
|
|
||||||
if err := db.Create(&model.UserVipState{
|
|
||||||
ID: 1,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
UserID: userID,
|
|
||||||
Level: 2,
|
|
||||||
LevelCode: "VIP2",
|
|
||||||
DisplayName: "VIP 2",
|
|
||||||
Status: statusActive,
|
|
||||||
StartAt: now.Add(-time.Hour),
|
|
||||||
ExpireAt: now.Add(24 * time.Hour),
|
|
||||||
BadgeResourceID: 101,
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create vip state: %v", err)
|
|
||||||
}
|
|
||||||
seedBadgeConfig(t, db, 101, "ACTIVITY", "VIP Badge", "VIP_BADGE", 5)
|
|
||||||
seedBadgeConfig(t, db, 102, badgeSourceTypeActivity, "Activity Badge", "ACTIVITY_BADGE", 3)
|
|
||||||
seedBadgeConfig(t, db, 103, badgeSourceTypeAchievement, "Achievement Badge", "ACHIEVEMENT_BADGE", 7)
|
|
||||||
seedBadgeConfig(t, db, 104, "ADMINISTRATOR", "Admin Badge", "ADMIN_BADGE", 0)
|
|
||||||
seedBadgeConfig(t, db, 105, badgeSourceTypeActivity, "Expired Badge", "EXPIRED_BADGE", 0)
|
|
||||||
seedBadgeConfig(t, db, 106, badgeSourceTypeActivity, "Unused Badge", "UNUSED_BADGE", 0)
|
|
||||||
for _, badgeID := range []int64{101, 102, 103, 104, 105, 106} {
|
|
||||||
if err := db.Create(&model.SysBadgePictureConfig{
|
|
||||||
ID: badgeID + 1000,
|
|
||||||
BadgeConfigID: badgeID,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
SelectURL: "https://cdn.example.com/" + formatID(badgeID) + ".png",
|
|
||||||
NotSelectURL: "https://cdn.example.com/" + formatID(badgeID) + "-off.png",
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create picture %d: %v", badgeID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
backpacks := []model.UserBadgeBackpack{
|
|
||||||
{ID: 2001, UserID: userID, BadgeID: 101, ExpireType: "TEMPORARY", ExpireTime: now.Add(24 * time.Hour), UseProps: true, UpdateTime: now.Add(5 * time.Minute)},
|
|
||||||
{ID: 2002, UserID: userID, BadgeID: 102, ExpireType: "TEMPORARY", ExpireTime: now.Add(2 * time.Hour), UseProps: true, UpdateTime: now.Add(4 * time.Minute)},
|
|
||||||
{ID: 2003, UserID: userID, BadgeID: 103, ExpireType: expireTypePermanent, ExpireTime: now.Add(-24 * time.Hour), UseProps: true, UpdateTime: now.Add(3 * time.Minute)},
|
|
||||||
{ID: 2004, UserID: userID, BadgeID: 104, ExpireType: "TEMPORARY", ExpireTime: now.Add(2 * time.Hour), UseProps: true, UpdateTime: now.Add(2 * time.Minute)},
|
|
||||||
{ID: 2005, UserID: userID, BadgeID: 105, ExpireType: "TEMPORARY", ExpireTime: now.Add(-time.Minute), UseProps: true, UpdateTime: now.Add(time.Minute)},
|
|
||||||
{ID: 2006, UserID: userID, BadgeID: 106, ExpireType: "TEMPORARY", ExpireTime: now.Add(2 * time.Hour), UseProps: false, UpdateTime: now},
|
|
||||||
}
|
|
||||||
if err := db.Create(&backpacks).Error; err != nil {
|
|
||||||
t.Fatalf("create backpacks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := NewService(db)
|
|
||||||
service.now = func() time.Time { return now }
|
|
||||||
resp, err := service.Current(context.Background(), common.AuthUser{UserID: userID, SysOrigin: "likei"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Current() error = %v", err)
|
|
||||||
}
|
|
||||||
if resp.SysOrigin != "LIKEI" {
|
|
||||||
t.Fatalf("SysOrigin = %q, want LIKEI", resp.SysOrigin)
|
|
||||||
}
|
|
||||||
if len(resp.LongBadges) != 1 {
|
|
||||||
t.Fatalf("long badges len = %d, want 1: %#v", len(resp.LongBadges), resp.LongBadges)
|
|
||||||
}
|
|
||||||
if len(resp.ShortBadges) != 2 {
|
|
||||||
t.Fatalf("short badges len = %d, want 2: %#v", len(resp.ShortBadges), resp.ShortBadges)
|
|
||||||
}
|
|
||||||
if len(resp.Badges) != 3 {
|
|
||||||
t.Fatalf("badges len = %d, want 3", len(resp.Badges))
|
|
||||||
}
|
|
||||||
long := resp.LongBadges[0]
|
|
||||||
if long.Type != badgeDisplayTypeLong || long.SourceType != badgeSourceTypeVIP || long.BadgeID != "101" || long.LevelCode != "VIP2" {
|
|
||||||
t.Fatalf("long badge = %#v", long)
|
|
||||||
}
|
|
||||||
if long.Name != "VIP Badge" || long.URL != "https://cdn.example.com/101.png" {
|
|
||||||
t.Fatalf("long badge name/url = %q/%q", long.Name, long.URL)
|
|
||||||
}
|
|
||||||
if resp.ShortBadges[0].BadgeID != "102" || resp.ShortBadges[0].SourceType != badgeSourceTypeActivity {
|
|
||||||
t.Fatalf("first short badge = %#v", resp.ShortBadges[0])
|
|
||||||
}
|
|
||||||
if resp.ShortBadges[1].BadgeID != "103" || resp.ShortBadges[1].SourceType != badgeSourceTypeAchievement {
|
|
||||||
t.Fatalf("second short badge = %#v", resp.ShortBadges[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCurrentBadgesRejectsInvalidUser(t *testing.T) {
|
|
||||||
service := NewService(newUserBadgeTestDB(t))
|
|
||||||
_, err := service.Current(context.Background(), common.AuthUser{SysOrigin: "LIKEI"})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Current() error = nil, want invalid user")
|
|
||||||
}
|
|
||||||
appErr, ok := err.(*AppError)
|
|
||||||
if !ok || appErr.Code != "invalid_user" {
|
|
||||||
t.Fatalf("error = %#v, want invalid_user", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOtherBadgesUsesTargetUser(t *testing.T) {
|
|
||||||
db := newUserBadgeTestDB(t)
|
|
||||||
now := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)
|
|
||||||
viewerID := int64(42)
|
|
||||||
targetID := int64(99)
|
|
||||||
seedBadgeConfig(t, db, 201, badgeSourceTypeActivity, "Viewer Badge", "VIEWER_BADGE", 1)
|
|
||||||
seedBadgeConfig(t, db, 202, badgeSourceTypeAchievement, "Target Badge", "TARGET_BADGE", 2)
|
|
||||||
for _, badgeID := range []int64{201, 202} {
|
|
||||||
if err := db.Create(&model.SysBadgePictureConfig{
|
|
||||||
ID: badgeID + 1000,
|
|
||||||
BadgeConfigID: badgeID,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
SelectURL: "https://cdn.example.com/" + formatID(badgeID) + ".png",
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create picture %d: %v", badgeID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
backpacks := []model.UserBadgeBackpack{
|
|
||||||
{ID: 3001, UserID: viewerID, BadgeID: 201, ExpireType: "TEMPORARY", ExpireTime: now.Add(2 * time.Hour), UseProps: true, UpdateTime: now},
|
|
||||||
{ID: 3002, UserID: targetID, BadgeID: 202, ExpireType: "TEMPORARY", ExpireTime: now.Add(2 * time.Hour), UseProps: true, UpdateTime: now.Add(time.Minute)},
|
|
||||||
}
|
|
||||||
if err := db.Create(&backpacks).Error; err != nil {
|
|
||||||
t.Fatalf("create backpacks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := NewService(db)
|
|
||||||
service.now = func() time.Time { return now }
|
|
||||||
resp, err := service.Other(context.Background(), common.AuthUser{UserID: viewerID, SysOrigin: "LIKEI"}, targetID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Other() error = %v", err)
|
|
||||||
}
|
|
||||||
if resp.UserID != targetID {
|
|
||||||
t.Fatalf("UserID = %d, want target %d", resp.UserID, targetID)
|
|
||||||
}
|
|
||||||
if len(resp.ShortBadges) != 1 || resp.ShortBadges[0].BadgeID != "202" {
|
|
||||||
t.Fatalf("short badges = %#v, want target badge 202", resp.ShortBadges)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOtherBadgesRejectsInvalidTargetUser(t *testing.T) {
|
|
||||||
service := NewService(newUserBadgeTestDB(t))
|
|
||||||
_, err := service.Other(context.Background(), common.AuthUser{UserID: 42, SysOrigin: "LIKEI"}, 0)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Other() error = nil, want invalid target user")
|
|
||||||
}
|
|
||||||
appErr, ok := err.(*AppError)
|
|
||||||
if !ok || appErr.Code != "invalid_target_user_id" {
|
|
||||||
t.Fatalf("error = %#v, want invalid_target_user_id", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUserBadgeTestDB(t *testing.T) *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.UserVipState{},
|
|
||||||
&model.UserBadgeBackpack{},
|
|
||||||
&model.SysBadgeConfig{},
|
|
||||||
&model.SysBadgePictureConfig{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("migrate sqlite: %v", err)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedBadgeConfig(t *testing.T, db *gorm.DB, id int64, sourceType string, name string, key string, level int) {
|
|
||||||
t.Helper()
|
|
||||||
if err := db.Create(&model.SysBadgeConfig{
|
|
||||||
ID: id,
|
|
||||||
Type: sourceType,
|
|
||||||
BadgeName: name,
|
|
||||||
BadgeKey: key,
|
|
||||||
BadgeLevel: level,
|
|
||||||
Milestone: id * 10,
|
|
||||||
Del: false,
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("create badge config %d: %v", id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
package userbadge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
badgeDisplayTypeLong = "LONG"
|
|
||||||
badgeDisplayTypeShort = "SHORT"
|
|
||||||
|
|
||||||
badgeSourceTypeVIP = "VIP"
|
|
||||||
badgeSourceTypeActivity = "ACTIVITY"
|
|
||||||
badgeSourceTypeAchievement = "ACHIEVEMENT"
|
|
||||||
|
|
||||||
expireTypePermanent = "PERMANENT"
|
|
||||||
statusActive = "ACTIVE"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppError = common.AppError
|
|
||||||
type AuthUser = common.AuthUser
|
|
||||||
|
|
||||||
var NewAppError = common.NewAppError
|
|
||||||
|
|
||||||
type dbHandle interface {
|
|
||||||
WithContext(ctx context.Context) *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service provides current-user badge aggregation for App clients.
|
|
||||||
type Service struct {
|
|
||||||
db dbHandle
|
|
||||||
now func() time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService creates the user badge aggregation service.
|
|
||||||
func NewService(db dbHandle) *Service {
|
|
||||||
return &Service{
|
|
||||||
db: db,
|
|
||||||
now: time.Now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentBadgeListResponse is the App-facing current badge payload.
|
|
||||||
type CurrentBadgeListResponse struct {
|
|
||||||
UserID int64 `json:"userId"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
Badges []CurrentBadgeItem `json:"badges"`
|
|
||||||
LongBadges []CurrentBadgeItem `json:"longBadges"`
|
|
||||||
ShortBadges []CurrentBadgeItem `json:"shortBadges"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentBadgeItem is one current badge. Type is LONG for VIP badges and SHORT
|
|
||||||
// for activity or achievement badges.
|
|
||||||
type CurrentBadgeItem struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
BadgeID string `json:"badgeId"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
SourceType string `json:"sourceType"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
BadgeName string `json:"badgeName"`
|
|
||||||
BadgeKey string `json:"badgeKey,omitempty"`
|
|
||||||
BadgeLevel int `json:"badgeLevel,omitempty"`
|
|
||||||
Milestone string `json:"milestone,omitempty"`
|
|
||||||
URL string `json:"url,omitempty"`
|
|
||||||
SelectURL string `json:"selectUrl,omitempty"`
|
|
||||||
NotSelectURL string `json:"notSelectUrl,omitempty"`
|
|
||||||
AnimationURL string `json:"animationUrl,omitempty"`
|
|
||||||
Level int `json:"level,omitempty"`
|
|
||||||
LevelCode string `json:"levelCode,omitempty"`
|
|
||||||
DisplayName string `json:"displayName,omitempty"`
|
|
||||||
Use bool `json:"use"`
|
|
||||||
ExpireTime int64 `json:"expireTime,omitempty"`
|
|
||||||
ExpireAt string `json:"expireAt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeUser(user AuthUser) (string, error) {
|
|
||||||
if user.UserID <= 0 {
|
|
||||||
return "", NewAppError(http.StatusUnauthorized, "invalid_user", "user is required")
|
|
||||||
}
|
|
||||||
sysOrigin := strings.ToUpper(strings.TrimSpace(user.SysOrigin))
|
|
||||||
if sysOrigin == "" {
|
|
||||||
return "", NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required")
|
|
||||||
}
|
|
||||||
return sysOrigin, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatID(id int64) string {
|
|
||||||
if id <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strconv.FormatInt(id, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseUserID parses a userId query parameter.
|
|
||||||
func ParseUserID(raw string) int64 {
|
|
||||||
value := strings.TrimSpace(raw)
|
|
||||||
if value == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatMillis(t time.Time) int64 {
|
|
||||||
if t.IsZero() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return t.UnixMilli()
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatTime(t time.Time) string {
|
|
||||||
if t.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return t.Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstNonEmpty(values ...string) string {
|
|
||||||
for _, value := range values {
|
|
||||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@ -163,7 +163,6 @@ func (s *Service) SaveLevelConfigs(ctx context.Context, req SaveLevelConfigsRequ
|
|||||||
}
|
}
|
||||||
row.NeedEnergy = item.NeedEnergy
|
row.NeedEnergy = item.NeedEnergy
|
||||||
row.ShakeThresholdPercent = threshold
|
row.ShakeThresholdPercent = threshold
|
||||||
row.PreviewRocketURL = strings.TrimSpace(item.PreviewRocketURL)
|
|
||||||
row.RocketIconURL = strings.TrimSpace(item.RocketIconURL)
|
row.RocketIconURL = strings.TrimSpace(item.RocketIconURL)
|
||||||
row.RocketAnimationURL = strings.TrimSpace(item.RocketAnimationURL)
|
row.RocketAnimationURL = strings.TrimSpace(item.RocketAnimationURL)
|
||||||
row.ProgressBarURL = strings.TrimSpace(item.ProgressBarURL)
|
row.ProgressBarURL = strings.TrimSpace(item.ProgressBarURL)
|
||||||
@ -265,12 +264,6 @@ func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRe
|
|||||||
} else if amount <= 0 {
|
} else if amount <= 0 {
|
||||||
amount = 1
|
amount = 1
|
||||||
}
|
}
|
||||||
rewardCopies := item.RewardCopies
|
|
||||||
if scene != rewardSceneInRoom {
|
|
||||||
rewardCopies = 1
|
|
||||||
} else if rewardCopies <= 0 {
|
|
||||||
rewardCopies = 1
|
|
||||||
}
|
|
||||||
var row model.VoiceRoomRocketRewardConfig
|
var row model.VoiceRoomRocketRewardConfig
|
||||||
if item.ID > 0 {
|
if item.ID > 0 {
|
||||||
if err := tx.Where("id = ? AND sys_origin = ?", item.ID, sysOrigin).First(&row).Error; err != nil {
|
if err := tx.Where("id = ? AND sys_origin = ?", item.ID, sysOrigin).First(&row).Error; err != nil {
|
||||||
@ -295,7 +288,6 @@ func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRe
|
|||||||
row.RewardCover = strings.TrimSpace(item.RewardCover)
|
row.RewardCover = strings.TrimSpace(item.RewardCover)
|
||||||
row.RewardAmount = amount
|
row.RewardAmount = amount
|
||||||
row.ExpireDays = item.ExpireDays
|
row.ExpireDays = item.ExpireDays
|
||||||
row.RewardCopies = rewardCopies
|
|
||||||
row.Weight = item.Weight
|
row.Weight = item.Weight
|
||||||
row.Sort = item.Sort
|
row.Sort = item.Sort
|
||||||
row.Enabled = item.Enabled
|
row.Enabled = item.Enabled
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
package voiceroomrocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSaveLevelConfigsPersistsPreviewRocketURL(t *testing.T) {
|
|
||||||
service, _ := newTestService(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
saved, err := service.SaveLevelConfigs(ctx, SaveLevelConfigsRequest{
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Levels: []LevelConfigPayload{
|
|
||||||
{
|
|
||||||
Level: 1,
|
|
||||||
NeedEnergy: 100,
|
|
||||||
ShakeThresholdPercent: "95.0000",
|
|
||||||
PreviewRocketURL: "https://cdn.example.com/preview-rocket.png",
|
|
||||||
RocketIconURL: "https://cdn.example.com/rocket-icon.png",
|
|
||||||
RocketAnimationURL: "https://cdn.example.com/rocket-launch.svga",
|
|
||||||
Enabled: true,
|
|
||||||
Sort: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SaveLevelConfigs error = %v", err)
|
|
||||||
}
|
|
||||||
if len(saved) != 1 {
|
|
||||||
t.Fatalf("saved level count = %d, want 1", len(saved))
|
|
||||||
}
|
|
||||||
if saved[0].PreviewRocketURL != "https://cdn.example.com/preview-rocket.png" {
|
|
||||||
t.Fatalf("previewRocketUrl = %q", saved[0].PreviewRocketURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
listed, err := service.ListLevelConfigs(ctx, "LIKEI")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListLevelConfigs error = %v", err)
|
|
||||||
}
|
|
||||||
if len(listed) != 1 || listed[0].PreviewRocketURL != saved[0].PreviewRocketURL {
|
|
||||||
t.Fatalf("listed levels = %+v", listed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetStatusReturnsRocketResourcesToApp(t *testing.T) {
|
|
||||||
service, _ := newTestService(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
_, err := service.SaveLevelConfigs(ctx, SaveLevelConfigsRequest{
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Levels: []LevelConfigPayload{
|
|
||||||
{
|
|
||||||
Level: 1,
|
|
||||||
NeedEnergy: 100,
|
|
||||||
ShakeThresholdPercent: "95.0000",
|
|
||||||
PreviewRocketURL: "https://cdn.example.com/preview-rocket.png",
|
|
||||||
RocketIconURL: "https://cdn.example.com/rocket-icon.png",
|
|
||||||
RocketAnimationURL: "https://cdn.example.com/rocket-launch.svga",
|
|
||||||
Enabled: true,
|
|
||||||
Sort: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SaveLevelConfigs error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := service.GetStatus(ctx, AuthUser{SysOrigin: "LIKEI"}, 9001)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetStatus error = %v", err)
|
|
||||||
}
|
|
||||||
if status.PreviewRocketURL != "https://cdn.example.com/preview-rocket.png" {
|
|
||||||
t.Fatalf("status previewRocketUrl = %q", status.PreviewRocketURL)
|
|
||||||
}
|
|
||||||
if status.RocketIconURL != "https://cdn.example.com/rocket-icon.png" {
|
|
||||||
t.Fatalf("status rocketIconUrl = %q", status.RocketIconURL)
|
|
||||||
}
|
|
||||||
if status.RocketAnimationURL != "https://cdn.example.com/rocket-launch.svga" {
|
|
||||||
t.Fatalf("status rocketAnimationUrl = %q", status.RocketAnimationURL)
|
|
||||||
}
|
|
||||||
if len(status.Levels) != 1 || status.Levels[0].PreviewRocketURL != status.PreviewRocketURL {
|
|
||||||
t.Fatalf("status levels = %+v", status.Levels)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -176,7 +176,7 @@ func TestInRoomRewardWorkerSnapshotsSelectsAndGrants(t *testing.T) {
|
|||||||
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
||||||
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
||||||
seedRewardConfig(t, db, 1, rewardSceneIgnite, rewardTypeGold, 5)
|
seedRewardConfig(t, db, 1, rewardSceneIgnite, rewardTypeGold, 5)
|
||||||
seedRewardConfigWithCopies(t, db, 1, rewardSceneInRoom, rewardTypeGold, 1, 3)
|
seedRewardConfig(t, db, 1, rewardSceneInRoom, rewardTypeGold, 1)
|
||||||
|
|
||||||
roomID := int64(9001)
|
roomID := int64(9001)
|
||||||
for _, userID := range []int64{1001, 1002, 1003, 1004} {
|
for _, userID := range []int64{1001, 1002, 1003, 1004} {
|
||||||
@ -255,67 +255,6 @@ func TestInRoomRewardWorkerSnapshotsSelectsAndGrants(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessPendingRewardsGrantsBadgeThroughBadgeGateway(t *testing.T) {
|
|
||||||
gateway := newFakeRocketGateway()
|
|
||||||
service, db := newTestService(t)
|
|
||||||
service.rewardGateway = gateway
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
badgeID := int64(3001)
|
|
||||||
expireDays := 14
|
|
||||||
record := model.VoiceRoomRocketRewardRecord{
|
|
||||||
ID: 8801,
|
|
||||||
LaunchID: 9901,
|
|
||||||
LaunchNo: "launch-badge",
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
RoomID: 9001,
|
|
||||||
DayKey: "20260515",
|
|
||||||
RoundNo: 1,
|
|
||||||
Level: 1,
|
|
||||||
UserID: 1001,
|
|
||||||
RewardScene: rewardSceneTop1,
|
|
||||||
RewardConfigID: 7701,
|
|
||||||
RewardType: rewardTypeBadge,
|
|
||||||
RewardItemID: &badgeID,
|
|
||||||
RewardName: "activity badge",
|
|
||||||
RewardAmount: 1,
|
|
||||||
ExpireDays: &expireDays,
|
|
||||||
GrantEventID: "VOICE_ROOM_ROCKET:launch-badge:TOP1:1001:7701",
|
|
||||||
GrantStatus: rewardStatusPending,
|
|
||||||
PopupStatus: popupStatusUnread,
|
|
||||||
CreateTime: time.Now(),
|
|
||||||
UpdateTime: time.Now(),
|
|
||||||
}
|
|
||||||
if err := db.Create(&record).Error; err != nil {
|
|
||||||
t.Fatalf("seed reward record: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
granted, err := service.ProcessPendingRewards(ctx, 10)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ProcessPendingRewards() error = %v", err)
|
|
||||||
}
|
|
||||||
if granted != 1 {
|
|
||||||
t.Fatalf("granted = %d, want 1", granted)
|
|
||||||
}
|
|
||||||
if len(gateway.badges) != 1 {
|
|
||||||
t.Fatalf("badge grants = %d, want 1", len(gateway.badges))
|
|
||||||
}
|
|
||||||
if gateway.badges[0] != (fakeBadgeGrant{userID: 1001, badgeID: badgeID, days: expireDays}) {
|
|
||||||
t.Fatalf("badge grant = %+v, want user 1001 badge %d days %d", gateway.badges[0], badgeID, expireDays)
|
|
||||||
}
|
|
||||||
if len(gateway.props) != 0 {
|
|
||||||
t.Fatalf("props grants = %d, want 0", len(gateway.props))
|
|
||||||
}
|
|
||||||
|
|
||||||
var latest model.VoiceRoomRocketRewardRecord
|
|
||||||
if err := db.Where("id = ?", record.ID).First(&latest).Error; err != nil {
|
|
||||||
t.Fatalf("load reward record: %v", err)
|
|
||||||
}
|
|
||||||
if latest.GrantStatus != rewardStatusSuccess {
|
|
||||||
t.Fatalf("grant status = %s, want %s", latest.GrantStatus, rewardStatusSuccess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProcessGiftPayloadDelaysLaunchWithRedis(t *testing.T) {
|
func TestProcessGiftPayloadDelaysLaunchWithRedis(t *testing.T) {
|
||||||
gateway := newFakeRocketGateway()
|
gateway := newFakeRocketGateway()
|
||||||
service, db, _, mr := newTestServiceWithRedis(t, gateway)
|
service, db, _, mr := newTestServiceWithRedis(t, gateway)
|
||||||
@ -397,106 +336,11 @@ func TestPickInRoomRewardCanBeEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGrantGoldRewardUsesWalletRocketRewardOrigin(t *testing.T) {
|
|
||||||
service, _ := newTestService(t)
|
|
||||||
gateway := newFakeRocketGateway()
|
|
||||||
service.wallet = gateway
|
|
||||||
record := model.VoiceRoomRocketRewardRecord{
|
|
||||||
UserID: 1001,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Level: 1,
|
|
||||||
RewardScene: rewardSceneInRoom,
|
|
||||||
RewardType: rewardTypeGold,
|
|
||||||
RewardAmount: 100,
|
|
||||||
GrantEventID: "VOICE_ROOM_ROCKET:launch-1:IN_ROOM:1001:1",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := service.grantGoldReward(context.Background(), record); err != nil {
|
|
||||||
t.Fatalf("grantGoldReward() error = %v", err)
|
|
||||||
}
|
|
||||||
walletEventID := walletGoldRewardEventID(record.GrantEventID)
|
|
||||||
cmd, ok := gateway.goldEvents[walletEventID]
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("gold wallet event was not created")
|
|
||||||
}
|
|
||||||
if len(cmd.EventID) > 50 {
|
|
||||||
t.Fatalf("wallet event id length = %d, want <= 50: %s", len(cmd.EventID), cmd.EventID)
|
|
||||||
}
|
|
||||||
if cmd.Origin != walletOriginRocketReward {
|
|
||||||
t.Fatalf("wallet origin = %q, want %q", cmd.Origin, walletOriginRocketReward)
|
|
||||||
}
|
|
||||||
if cmd.CustomizeOrigin != "" {
|
|
||||||
t.Fatalf("customize origin = %q, want empty", cmd.CustomizeOrigin)
|
|
||||||
}
|
|
||||||
if cmd.CustomizeOriginDesc != walletOriginRocketRewardDesc {
|
|
||||||
t.Fatalf("wallet origin desc = %q, want %q", cmd.CustomizeOriginDesc, walletOriginRocketRewardDesc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewardGrantEventIDFitsWalletAssetRecord(t *testing.T) {
|
|
||||||
eventID := rewardGrantEventID("VOICE_ROOM_ROCKET_LAUNCH:20260515:9001:ROUND:1:LEVEL:6:EXTRA-LONG", rewardSceneInRoom, 1001001001001, 9009009009001)
|
|
||||||
if len(eventID) > 50 {
|
|
||||||
t.Fatalf("grant event id length = %d, want <= 50: %s", len(eventID), eventID)
|
|
||||||
}
|
|
||||||
if eventID != rewardGrantEventID("VOICE_ROOM_ROCKET_LAUNCH:20260515:9001:ROUND:1:LEVEL:6:EXTRA-LONG", rewardSceneInRoom, 1001001001001, 9009009009001) {
|
|
||||||
t.Fatalf("grant event id must be deterministic")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInRoomRewardCopiesLimitSelection(t *testing.T) {
|
|
||||||
service, _ := newTestService(t)
|
|
||||||
service.cfg.VoiceRoomRocket.InRoomRewardUserLimit = 10
|
|
||||||
launch := model.VoiceRoomRocketLaunch{LaunchNo: "VRR-copies"}
|
|
||||||
audience := []roomAudienceUser{
|
|
||||||
{UserID: 1001},
|
|
||||||
{UserID: 1002},
|
|
||||||
{UserID: 1003},
|
|
||||||
{UserID: 1004},
|
|
||||||
}
|
|
||||||
rewards := []model.VoiceRoomRocketRewardConfig{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
RewardScene: rewardSceneInRoom,
|
|
||||||
RewardType: rewardTypeGold,
|
|
||||||
RewardName: "金币",
|
|
||||||
RewardAmount: 100,
|
|
||||||
RewardCopies: 2,
|
|
||||||
Weight: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
selected := service.selectInRoomRewardUsers(launch, audience, rewards)
|
|
||||||
if len(selected) != 2 {
|
|
||||||
t.Fatalf("selected users = %+v, want 2 users", selected)
|
|
||||||
}
|
|
||||||
picked := service.pickInRoomRewardsForUsers(launch, rewards, selected)
|
|
||||||
if len(picked) != 2 {
|
|
||||||
t.Fatalf("picked rewards = %+v, want 2 users with rewards", picked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNotifyLaunchPublishesIMAndRedisStream(t *testing.T) {
|
func TestNotifyLaunchPublishesIMAndRedisStream(t *testing.T) {
|
||||||
gateway := newFakeRocketGateway()
|
gateway := newFakeRocketGateway()
|
||||||
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
|
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
|
||||||
defer mr.Close()
|
defer mr.Close()
|
||||||
seedRocketConfig(t, db)
|
seedRocketConfig(t, db)
|
||||||
now := time.Now()
|
|
||||||
if err := db.Create(&model.VoiceRoomRocketLevelConfig{
|
|
||||||
ID: 991,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Level: 1,
|
|
||||||
NeedEnergy: 100,
|
|
||||||
ShakeThresholdPercent: "95.0000",
|
|
||||||
PreviewRocketURL: "https://cdn.example.com/preview-rocket.png",
|
|
||||||
RocketIconURL: "https://cdn.example.com/rocket-icon.png",
|
|
||||||
RocketAnimationURL: "https://cdn.example.com/rocket-launch.svga",
|
|
||||||
Enabled: true,
|
|
||||||
Sort: 1,
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}).Error; err != nil {
|
|
||||||
t.Fatalf("seed level config: %v", err)
|
|
||||||
}
|
|
||||||
fakeIM := &fakeRocketIM{}
|
fakeIM := &fakeRocketIM{}
|
||||||
service.SetIMGateway(fakeIM)
|
service.SetIMGateway(fakeIM)
|
||||||
|
|
||||||
@ -516,23 +360,6 @@ func TestNotifyLaunchPublishesIMAndRedisStream(t *testing.T) {
|
|||||||
if len(fakeIM.messages) != 1 || fakeIM.messages[0].groupID != "room-9001" {
|
if len(fakeIM.messages) != 1 || fakeIM.messages[0].groupID != "room-9001" {
|
||||||
t.Fatalf("im messages = %+v", fakeIM.messages)
|
t.Fatalf("im messages = %+v", fakeIM.messages)
|
||||||
}
|
}
|
||||||
body, ok := fakeIM.messages[0].body.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("im body = %#v", fakeIM.messages[0].body)
|
|
||||||
}
|
|
||||||
data, ok := body["data"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("im data = %#v", body["data"])
|
|
||||||
}
|
|
||||||
if data["previewRocketUrl"] != "https://cdn.example.com/preview-rocket.png" {
|
|
||||||
t.Fatalf("previewRocketUrl = %#v", data["previewRocketUrl"])
|
|
||||||
}
|
|
||||||
if data["rocketIconUrl"] != "https://cdn.example.com/rocket-icon.png" {
|
|
||||||
t.Fatalf("rocketIconUrl = %#v", data["rocketIconUrl"])
|
|
||||||
}
|
|
||||||
if data["rocketAnimationUrl"] != "https://cdn.example.com/rocket-launch.svga" {
|
|
||||||
t.Fatalf("rocketAnimationUrl = %#v", data["rocketAnimationUrl"])
|
|
||||||
}
|
|
||||||
count, err := rdb.XLen(context.Background(), "voice_room:rocket:broadcast").Result()
|
count, err := rdb.XLen(context.Background(), "voice_room:rocket:broadcast").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("xlen broadcast: %v", err)
|
t.Fatalf("xlen broadcast: %v", err)
|
||||||
@ -630,11 +457,10 @@ func TestSaveRewardConfigsReplacesAndClearsSelectedLevel(t *testing.T) {
|
|||||||
Rewards: []RewardConfigPayload{
|
Rewards: []RewardConfigPayload{
|
||||||
{
|
{
|
||||||
Level: 1,
|
Level: 1,
|
||||||
RewardScene: rewardSceneInRoom,
|
RewardScene: rewardSceneTop1,
|
||||||
RewardType: rewardTypeGold,
|
RewardType: rewardTypeGold,
|
||||||
RewardName: "new gold",
|
RewardName: "new gold",
|
||||||
RewardAmount: 99,
|
RewardAmount: 99,
|
||||||
RewardCopies: 10,
|
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Sort: 1,
|
Sort: 1,
|
||||||
},
|
},
|
||||||
@ -643,8 +469,8 @@ func TestSaveRewardConfigsReplacesAndClearsSelectedLevel(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SaveRewardConfigs(replace) error = %v", err)
|
t.Fatalf("SaveRewardConfigs(replace) error = %v", err)
|
||||||
}
|
}
|
||||||
if len(saved) != 1 || saved[0].RewardAmount != 99 || saved[0].RewardCopies != 10 {
|
if len(saved) != 1 || saved[0].RewardAmount != 99 {
|
||||||
t.Fatalf("saved rewards = %+v, want one amount 99 and 10 copies", saved)
|
t.Fatalf("saved rewards = %+v, want one amount 99", saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
var level1Count int64
|
var level1Count int64
|
||||||
@ -744,13 +570,7 @@ func newTestServiceWithRedis(t *testing.T, gateway *fakeRocketGateway) (*Service
|
|||||||
type fakeRocketGateway struct {
|
type fakeRocketGateway struct {
|
||||||
goldEvents map[string]integration.GoldReceiptCommand
|
goldEvents map[string]integration.GoldReceiptCommand
|
||||||
props []integration.GivePropsBackpackRequest
|
props []integration.GivePropsBackpackRequest
|
||||||
badges []fakeBadgeGrant
|
badges []int64
|
||||||
}
|
|
||||||
|
|
||||||
type fakeBadgeGrant struct {
|
|
||||||
userID int64
|
|
||||||
badgeID int64
|
|
||||||
days int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeRocketIM struct {
|
type fakeRocketIM struct {
|
||||||
@ -817,7 +637,7 @@ func (g *fakeRocketGateway) SwitchUseProps(ctx context.Context, userID int64, pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *fakeRocketGateway) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
func (g *fakeRocketGateway) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
||||||
g.badges = append(g.badges, fakeBadgeGrant{userID: userID, badgeID: badgeID, days: days})
|
g.badges = append(g.badges, badgeID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -891,10 +711,6 @@ func seedLevelConfigs(t *testing.T, db *gorm.DB, energy []int64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func seedRewardConfig(t *testing.T, db *gorm.DB, level int, scene string, rewardType string, amount int64) {
|
func seedRewardConfig(t *testing.T, db *gorm.DB, level int, scene string, rewardType string, amount int64) {
|
||||||
seedRewardConfigWithCopies(t, db, level, scene, rewardType, amount, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedRewardConfigWithCopies(t *testing.T, db *gorm.DB, level int, scene string, rewardType string, amount int64, copies int) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
row := model.VoiceRoomRocketRewardConfig{
|
row := model.VoiceRoomRocketRewardConfig{
|
||||||
@ -905,7 +721,6 @@ func seedRewardConfigWithCopies(t *testing.T, db *gorm.DB, level int, scene stri
|
|||||||
RewardType: rewardType,
|
RewardType: rewardType,
|
||||||
RewardName: scene + " reward",
|
RewardName: scene + " reward",
|
||||||
RewardAmount: amount,
|
RewardAmount: amount,
|
||||||
RewardCopies: copies,
|
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
CreateTime: now,
|
CreateTime: now,
|
||||||
UpdateTime: now,
|
UpdateTime: now,
|
||||||
|
|||||||
@ -88,11 +88,7 @@ func (s *Service) processInRoomRewardLaunch(ctx context.Context, launchNo string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rewards, err := s.loadInRoomRewardConfigs(ctx, launch.SysOrigin, launch.Level)
|
selected := s.selectInRoomRewardUsers(launch, audience)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
selected := s.selectInRoomRewardUsers(launch, audience, rewards)
|
|
||||||
if err := s.saveAudienceSnapshot(ctx, tx, launch, audience, selected, now); err != nil {
|
if err := s.saveAudienceSnapshot(ctx, tx, launch, audience, selected, now); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -104,9 +100,12 @@ func (s *Service) processInRoomRewardLaunch(ctx context.Context, launchNo string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pickedRewards := s.pickInRoomRewardsForUsers(launch, rewards, selected)
|
rewards, err := s.loadInRoomRewardConfigs(ctx, launch.SysOrigin, launch.Level)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, userID := range selected {
|
for _, userID := range selected {
|
||||||
for _, reward := range pickedRewards[userID] {
|
for _, reward := range s.pickInRoomRewardsForUser(launch, rewards, userID) {
|
||||||
record, err := s.createRewardRecord(ctx, tx, launch, reward, userID, now)
|
record, err := s.createRewardRecord(ctx, tx, launch, reward, userID, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -202,8 +201,11 @@ func (s *Service) loadInRoomRewardConfigs(ctx context.Context, sysOrigin string,
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) selectInRoomRewardUsers(launch model.VoiceRoomRocketLaunch, audience []roomAudienceUser, rewards []model.VoiceRoomRocketRewardConfig) []int64 {
|
func (s *Service) selectInRoomRewardUsers(launch model.VoiceRoomRocketLaunch, audience []roomAudienceUser) []int64 {
|
||||||
limit := s.inRoomRewardSelectionLimit(rewards)
|
limit := s.cfg.VoiceRoomRocket.InRoomRewardUserLimit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = defaultInRoomRewardUserLimit
|
||||||
|
}
|
||||||
if len(audience) == 0 {
|
if len(audience) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -226,80 +228,37 @@ func (s *Service) selectInRoomRewardUsers(launch model.VoiceRoomRocketLaunch, au
|
|||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) inRoomRewardSelectionLimit(rewards []model.VoiceRoomRocketRewardConfig) int {
|
|
||||||
totalCopies := 0
|
|
||||||
for _, reward := range rewards {
|
|
||||||
if !strings.EqualFold(strings.TrimSpace(reward.RewardScene), rewardSceneInRoom) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
totalCopies += positiveOrDefault(reward.RewardCopies, 1)
|
|
||||||
}
|
|
||||||
if totalCopies > 0 {
|
|
||||||
return totalCopies
|
|
||||||
}
|
|
||||||
limit := s.cfg.VoiceRoomRocket.InRoomRewardUserLimit
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = defaultInRoomRewardUserLimit
|
|
||||||
}
|
|
||||||
return limit
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) pickInRoomRewardsForUser(launch model.VoiceRoomRocketLaunch, rewards []model.VoiceRoomRocketRewardConfig, userID int64) []model.VoiceRoomRocketRewardConfig {
|
func (s *Service) pickInRoomRewardsForUser(launch model.VoiceRoomRocketLaunch, rewards []model.VoiceRoomRocketRewardConfig, userID int64) []model.VoiceRoomRocketRewardConfig {
|
||||||
picked := s.pickInRoomRewardsForUsers(launch, rewards, []int64{userID})
|
|
||||||
return picked[userID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) pickInRoomRewardsForUsers(launch model.VoiceRoomRocketLaunch, rewards []model.VoiceRoomRocketRewardConfig, userIDs []int64) map[int64][]model.VoiceRoomRocketRewardConfig {
|
|
||||||
result := make(map[int64][]model.VoiceRoomRocketRewardConfig, len(userIDs))
|
|
||||||
if len(rewards) == 0 {
|
if len(rewards) == 0 {
|
||||||
return result
|
return nil
|
||||||
}
|
}
|
||||||
|
weighted := make([]model.VoiceRoomRocketRewardConfig, 0, len(rewards))
|
||||||
type rewardCandidate struct {
|
totalWeight := 0
|
||||||
reward model.VoiceRoomRocketRewardConfig
|
|
||||||
remaining int
|
|
||||||
}
|
|
||||||
weighted := make([]rewardCandidate, 0, len(rewards))
|
|
||||||
for _, reward := range rewards {
|
for _, reward := range rewards {
|
||||||
weight := reward.Weight
|
weight := reward.Weight
|
||||||
if weight <= 0 {
|
if weight <= 0 {
|
||||||
weight = 1
|
weight = 1
|
||||||
}
|
}
|
||||||
reward.Weight = weight
|
reward.Weight = weight
|
||||||
weighted = append(weighted, rewardCandidate{
|
weighted = append(weighted, reward)
|
||||||
reward: reward,
|
totalWeight += weight
|
||||||
remaining: positiveOrDefault(reward.RewardCopies, 1),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
if totalWeight <= 0 {
|
||||||
for _, userID := range userIDs {
|
return nil
|
||||||
totalWeight := 0
|
}
|
||||||
for _, candidate := range weighted {
|
rng := rand.New(rand.NewSource(seedFromStrings(launch.LaunchNo, strconv.FormatInt(userID, 10), "reward")))
|
||||||
if candidate.remaining > 0 {
|
point := rng.Intn(totalWeight) + 1
|
||||||
totalWeight += candidate.reward.Weight
|
acc := 0
|
||||||
}
|
for _, reward := range weighted {
|
||||||
}
|
acc += reward.Weight
|
||||||
if totalWeight <= 0 {
|
if point <= acc {
|
||||||
continue
|
if strings.EqualFold(strings.TrimSpace(reward.RewardType), rewardTypeNone) {
|
||||||
}
|
return nil
|
||||||
rng := rand.New(rand.NewSource(seedFromStrings(launch.LaunchNo, strconv.FormatInt(userID, 10), "reward")))
|
|
||||||
point := rng.Intn(totalWeight) + 1
|
|
||||||
acc := 0
|
|
||||||
for idx := range weighted {
|
|
||||||
if weighted[idx].remaining <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
acc += weighted[idx].reward.Weight
|
|
||||||
if point <= acc {
|
|
||||||
weighted[idx].remaining--
|
|
||||||
if !strings.EqualFold(strings.TrimSpace(weighted[idx].reward.RewardType), rewardTypeNone) {
|
|
||||||
result[userID] = []model.VoiceRoomRocketRewardConfig{weighted[idx].reward}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
return []model.VoiceRoomRocketRewardConfig{reward}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedFromStrings(parts ...string) int64 {
|
func seedFromStrings(parts ...string) int64 {
|
||||||
|
|||||||
@ -140,7 +140,6 @@ func levelPayload(row model.VoiceRoomRocketLevelConfig) LevelConfigPayload {
|
|||||||
Level: row.Level,
|
Level: row.Level,
|
||||||
NeedEnergy: row.NeedEnergy,
|
NeedEnergy: row.NeedEnergy,
|
||||||
ShakeThresholdPercent: row.ShakeThresholdPercent,
|
ShakeThresholdPercent: row.ShakeThresholdPercent,
|
||||||
PreviewRocketURL: row.PreviewRocketURL,
|
|
||||||
RocketIconURL: row.RocketIconURL,
|
RocketIconURL: row.RocketIconURL,
|
||||||
RocketAnimationURL: row.RocketAnimationURL,
|
RocketAnimationURL: row.RocketAnimationURL,
|
||||||
ProgressBarURL: row.ProgressBarURL,
|
ProgressBarURL: row.ProgressBarURL,
|
||||||
@ -160,7 +159,6 @@ func rewardPayload(row model.VoiceRoomRocketRewardConfig) RewardConfigPayload {
|
|||||||
RewardCover: row.RewardCover,
|
RewardCover: row.RewardCover,
|
||||||
RewardAmount: row.RewardAmount,
|
RewardAmount: row.RewardAmount,
|
||||||
ExpireDays: row.ExpireDays,
|
ExpireDays: row.ExpireDays,
|
||||||
RewardCopies: positiveOrDefault(row.RewardCopies, 1),
|
|
||||||
Weight: row.Weight,
|
Weight: row.Weight,
|
||||||
Sort: row.Sort,
|
Sort: row.Sort,
|
||||||
Enabled: row.Enabled,
|
Enabled: row.Enabled,
|
||||||
|
|||||||
@ -120,29 +120,18 @@ func (s *Service) notifyLaunch(ctx context.Context, launch model.VoiceRoomRocket
|
|||||||
if launch.IgniteUserID != nil {
|
if launch.IgniteUserID != nil {
|
||||||
triggerUserID = strconv.FormatInt(*launch.IgniteUserID, 10)
|
triggerUserID = strconv.FormatInt(*launch.IgniteUserID, 10)
|
||||||
}
|
}
|
||||||
previewRocketURL := ""
|
|
||||||
rocketIconURL := ""
|
|
||||||
rocketAnimationURL := ""
|
|
||||||
if levelConfig, err := s.loadLevelConfig(ctx, launch.SysOrigin, launch.Level); err == nil {
|
|
||||||
previewRocketURL = levelConfig.PreviewRocketURL
|
|
||||||
rocketIconURL = levelConfig.RocketIconURL
|
|
||||||
rocketAnimationURL = levelConfig.RocketAnimationURL
|
|
||||||
}
|
|
||||||
body := map[string]any{
|
body := map[string]any{
|
||||||
"type": imTypeLaunchBroadcast,
|
"type": imTypeLaunchBroadcast,
|
||||||
"data": map[string]any{
|
"data": map[string]any{
|
||||||
"roomId": strconv.FormatInt(launch.RoomID, 10),
|
"roomId": strconv.FormatInt(launch.RoomID, 10),
|
||||||
"roomAccount": launch.RoomAccount,
|
"roomAccount": launch.RoomAccount,
|
||||||
"roomName": roomName,
|
"roomName": roomName,
|
||||||
"roundNo": launch.RoundNo,
|
"roundNo": launch.RoundNo,
|
||||||
"level": launch.Level,
|
"level": launch.Level,
|
||||||
"durationSeconds": durationSeconds,
|
"durationSeconds": durationSeconds,
|
||||||
"previewRocketUrl": previewRocketURL,
|
"triggerUser": triggerUser,
|
||||||
"rocketIconUrl": rocketIconURL,
|
"triggerUserId": triggerUserID,
|
||||||
"rocketAnimationUrl": rocketAnimationURL,
|
"launchNo": launch.LaunchNo,
|
||||||
"triggerUser": triggerUser,
|
|
||||||
"triggerUserId": triggerUserID,
|
|
||||||
"launchNo": launch.LaunchNo,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if s.repo.Redis != nil {
|
if s.repo.Redis != nil {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package voiceroomrocket
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha1"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -188,8 +187,7 @@ func (s *Service) grantGoldReward(ctx context.Context, row model.VoiceRoomRocket
|
|||||||
if s.wallet == nil {
|
if s.wallet == nil {
|
||||||
return NewAppError(http.StatusServiceUnavailable, "wallet_unavailable", "wallet gateway is unavailable")
|
return NewAppError(http.StatusServiceUnavailable, "wallet_unavailable", "wallet gateway is unavailable")
|
||||||
}
|
}
|
||||||
walletEventID := walletGoldRewardEventID(row.GrantEventID)
|
exists, err := s.wallet.ExistsGoldEvent(ctx, row.GrantEventID)
|
||||||
exists, err := s.wallet.ExistsGoldEvent(ctx, walletEventID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -198,13 +196,13 @@ func (s *Service) grantGoldReward(ctx context.Context, row model.VoiceRoomRocket
|
|||||||
ReceiptType: "INCOME",
|
ReceiptType: "INCOME",
|
||||||
UserID: row.UserID,
|
UserID: row.UserID,
|
||||||
SysOrigin: row.SysOrigin,
|
SysOrigin: row.SysOrigin,
|
||||||
EventID: walletEventID,
|
EventID: row.GrantEventID,
|
||||||
Remark: fmt.Sprintf("voice room rocket %s level %d", row.RewardScene, row.Level),
|
Remark: fmt.Sprintf("voice room rocket %s level %d", row.RewardScene, row.Level),
|
||||||
Amount: integration.NewPennyAmountPayloadFromDollar(row.RewardAmount),
|
Amount: integration.NewPennyAmountPayloadFromDollar(row.RewardAmount),
|
||||||
CloseDelayAsset: true,
|
CloseDelayAsset: true,
|
||||||
OpUserType: "BACK",
|
OpUserType: "BACK",
|
||||||
Origin: walletOriginRocketReward,
|
CustomizeOrigin: "VOICE_ROOM_ROCKET",
|
||||||
CustomizeOriginDesc: walletOriginRocketRewardDesc,
|
CustomizeOriginDesc: "Voice Room Rocket Reward",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -343,19 +341,5 @@ func (s *Service) acquireRewardLock(ctx context.Context, rewardRecordID int64) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rewardGrantEventID(launchNo, scene string, userID int64, rewardConfigID int64) string {
|
func rewardGrantEventID(launchNo, scene string, userID int64, rewardConfigID int64) string {
|
||||||
raw := fmt.Sprintf("%s:%s:%d:%d", launchNo, strings.ToUpper(strings.TrimSpace(scene)), userID, rewardConfigID)
|
return fmt.Sprintf("VOICE_ROOM_ROCKET:%s:%s:%d:%d", launchNo, strings.ToUpper(strings.TrimSpace(scene)), userID, rewardConfigID)
|
||||||
return "VRR:" + hashString(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func walletGoldRewardEventID(grantEventID string) string {
|
|
||||||
grantEventID = strings.TrimSpace(grantEventID)
|
|
||||||
if len(grantEventID) > 0 && len(grantEventID) <= 50 {
|
|
||||||
return grantEventID
|
|
||||||
}
|
|
||||||
return "VRR:" + hashString(grantEventID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashString(value string) string {
|
|
||||||
sum := sha1.Sum([]byte(value))
|
|
||||||
return fmt.Sprintf("%x", sum)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,9 +58,6 @@ func (s *Service) GetStatus(ctx context.Context, user AuthUser, roomID int64) (*
|
|||||||
DisplayPercent: displayPercent,
|
DisplayPercent: displayPercent,
|
||||||
LaunchCountdownSeconds: countdownSeconds,
|
LaunchCountdownSeconds: countdownSeconds,
|
||||||
Shake: shake,
|
Shake: shake,
|
||||||
PreviewRocketURL: currentLevelConfig.PreviewRocketURL,
|
|
||||||
RocketIconURL: currentLevelConfig.RocketIconURL,
|
|
||||||
RocketAnimationURL: currentLevelConfig.RocketAnimationURL,
|
|
||||||
Levels: buildLevelPayloads(levels),
|
Levels: buildLevelPayloads(levels),
|
||||||
RewardPreview: rewardPreviewPayload(rewards),
|
RewardPreview: rewardPreviewPayload(rewards),
|
||||||
RocketKings: kings,
|
RocketKings: kings,
|
||||||
|
|||||||
@ -77,9 +77,6 @@ const (
|
|||||||
propsTypeGift = "GIFT"
|
propsTypeGift = "GIFT"
|
||||||
propsTypeBadge = "BADGE"
|
propsTypeBadge = "BADGE"
|
||||||
|
|
||||||
walletOriginRocketReward = "ROCKET_REWARD"
|
|
||||||
walletOriginRocketRewardDesc = "Rocket reward"
|
|
||||||
|
|
||||||
imTypeStatusUpdate = "VOICE_ROOM_ROCKET_STATUS_UPDATE"
|
imTypeStatusUpdate = "VOICE_ROOM_ROCKET_STATUS_UPDATE"
|
||||||
imTypeLaunchBroadcast = "VOICE_ROOM_ROCKET_LAUNCH_BROADCAST"
|
imTypeLaunchBroadcast = "VOICE_ROOM_ROCKET_LAUNCH_BROADCAST"
|
||||||
imTypeRewardPopup = "VOICE_ROOM_ROCKET_REWARD_POPUP"
|
imTypeRewardPopup = "VOICE_ROOM_ROCKET_REWARD_POPUP"
|
||||||
@ -318,7 +315,6 @@ type LevelConfigPayload struct {
|
|||||||
Level int `json:"level"`
|
Level int `json:"level"`
|
||||||
NeedEnergy int64 `json:"needEnergy"`
|
NeedEnergy int64 `json:"needEnergy"`
|
||||||
ShakeThresholdPercent string `json:"shakeThresholdPercent"`
|
ShakeThresholdPercent string `json:"shakeThresholdPercent"`
|
||||||
PreviewRocketURL string `json:"previewRocketUrl,omitempty"`
|
|
||||||
RocketIconURL string `json:"rocketIconUrl,omitempty"`
|
RocketIconURL string `json:"rocketIconUrl,omitempty"`
|
||||||
RocketAnimationURL string `json:"rocketAnimationUrl,omitempty"`
|
RocketAnimationURL string `json:"rocketAnimationUrl,omitempty"`
|
||||||
ProgressBarURL string `json:"progressBarUrl,omitempty"`
|
ProgressBarURL string `json:"progressBarUrl,omitempty"`
|
||||||
@ -336,7 +332,6 @@ type RewardConfigPayload struct {
|
|||||||
RewardCover string `json:"rewardCover,omitempty"`
|
RewardCover string `json:"rewardCover,omitempty"`
|
||||||
RewardAmount int64 `json:"rewardAmount"`
|
RewardAmount int64 `json:"rewardAmount"`
|
||||||
ExpireDays *int `json:"expireDays,omitempty"`
|
ExpireDays *int `json:"expireDays,omitempty"`
|
||||||
RewardCopies int `json:"rewardCopies"`
|
|
||||||
Weight int `json:"weight"`
|
Weight int `json:"weight"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@ -403,9 +398,6 @@ type StatusResponse struct {
|
|||||||
DisplayPercent int `json:"displayPercent"`
|
DisplayPercent int `json:"displayPercent"`
|
||||||
LaunchCountdownSeconds int `json:"launchCountdownSeconds,omitempty"`
|
LaunchCountdownSeconds int `json:"launchCountdownSeconds,omitempty"`
|
||||||
Shake bool `json:"shake"`
|
Shake bool `json:"shake"`
|
||||||
PreviewRocketURL string `json:"previewRocketUrl,omitempty"`
|
|
||||||
RocketIconURL string `json:"rocketIconUrl,omitempty"`
|
|
||||||
RocketAnimationURL string `json:"rocketAnimationUrl,omitempty"`
|
|
||||||
Levels []LevelConfigPayload `json:"levels"`
|
Levels []LevelConfigPayload `json:"levels"`
|
||||||
RewardPreview RewardPreviewResponse `json:"rewardPreview"`
|
RewardPreview RewardPreviewResponse `json:"rewardPreview"`
|
||||||
RocketKings []KingRecord `json:"rocketKings"`
|
RocketKings []KingRecord `json:"rocketKings"`
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_config (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_config (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||||
timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '业务时区',
|
timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '业务时区',
|
||||||
@ -10,188 +10,186 @@ CREATE TABLE IF NOT EXISTS voice_room_rocket_config (
|
|||||||
in_room_reward_delay_seconds INT NOT NULL DEFAULT 10 COMMENT '在房间奖励延迟窗口秒数',
|
in_room_reward_delay_seconds INT NOT NULL DEFAULT 10 COMMENT '在房间奖励延迟窗口秒数',
|
||||||
energy_rule_json JSON DEFAULT NULL COMMENT '礼物折算规则',
|
energy_rule_json JSON DEFAULT NULL COMMENT '礼物折算规则',
|
||||||
rule_text JSON DEFAULT NULL COMMENT '规则说明多语言文案',
|
rule_text JSON DEFAULT NULL COMMENT '规则说明多语言文案',
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_config (sys_origin)
|
UNIQUE KEY uk_voice_room_rocket_config (sys_origin)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭系统配置';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭系统配置';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_level_config (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_level_config (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
level INT NOT NULL COMMENT '火箭等级,Lv.1-Lv.6',
|
level INT NOT NULL COMMENT 'Lv.1-Lv.6',
|
||||||
need_energy BIGINT NOT NULL COMMENT '本级发射所需能量',
|
need_energy BIGINT NOT NULL COMMENT '本级发射所需能量',
|
||||||
shake_threshold_percent DECIMAL(8,4) NOT NULL DEFAULT 0 COMMENT '抖动阈值百分比',
|
shake_threshold_percent DECIMAL(8,4) NOT NULL DEFAULT 0 COMMENT '抖动阈值百分比',
|
||||||
preview_rocket_url VARCHAR(512) DEFAULT NULL COMMENT '预览火箭资源URL',
|
rocket_icon_url VARCHAR(512) DEFAULT NULL,
|
||||||
rocket_icon_url VARCHAR(512) DEFAULT NULL COMMENT '火箭图标资源URL',
|
rocket_animation_url VARCHAR(512) DEFAULT NULL,
|
||||||
rocket_animation_url VARCHAR(512) DEFAULT NULL COMMENT '火箭发射动画资源URL',
|
progress_bar_url VARCHAR(512) DEFAULT NULL,
|
||||||
progress_bar_url VARCHAR(512) DEFAULT NULL COMMENT '进度条资源URL(历史字段)',
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
sort INT NOT NULL DEFAULT 0,
|
||||||
sort INT NOT NULL DEFAULT 0 COMMENT '排序值,升序',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
UNIQUE KEY uk_voice_room_rocket_level (sys_origin, level)
|
UNIQUE KEY uk_voice_room_rocket_level (sys_origin, level)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭等级配置';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭等级配置';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_reward_config (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_reward_config (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
level INT NOT NULL COMMENT '适用等级',
|
level INT NOT NULL COMMENT '适用等级',
|
||||||
reward_scene VARCHAR(32) NOT NULL COMMENT 'TOP1/IGNITE/IN_ROOM',
|
reward_scene VARCHAR(32) NOT NULL COMMENT 'TOP1/IGNITE/IN_ROOM',
|
||||||
reward_type VARCHAR(32) NOT NULL COMMENT 'GOLD/MOUNT/AVATAR_FRAME/CHAT_BUBBLE/VIP_CARD/GIFT/BADGE/NONE',
|
reward_type VARCHAR(32) NOT NULL COMMENT 'GOLD/MOUNT/AVATAR_FRAME/CHAT_BUBBLE/VIP_CARD/GIFT/BADGE',
|
||||||
reward_item_id BIGINT DEFAULT NULL COMMENT '道具、礼物、权益等资源ID',
|
reward_item_id BIGINT DEFAULT NULL COMMENT '道具、礼物、权益等资源ID',
|
||||||
reward_name VARCHAR(128) NOT NULL COMMENT '奖励名称',
|
reward_name VARCHAR(128) NOT NULL,
|
||||||
reward_cover VARCHAR(512) DEFAULT NULL COMMENT '奖励封面URL',
|
reward_cover VARCHAR(512) DEFAULT NULL,
|
||||||
reward_amount BIGINT NOT NULL DEFAULT 1 COMMENT '金币数量或道具数量/天数',
|
reward_amount BIGINT NOT NULL DEFAULT 1 COMMENT '金币数量或道具数量/天数',
|
||||||
expire_days INT DEFAULT NULL COMMENT '时效类奖励有效天数',
|
expire_days INT DEFAULT NULL COMMENT '时效类奖励有效天数',
|
||||||
reward_copies INT NOT NULL DEFAULT 1 COMMENT 'IN_ROOM 本奖励每次发射可发放份数',
|
weight INT NOT NULL DEFAULT 0 COMMENT 'IN_ROOM 随机权重;0 表示固定发放',
|
||||||
weight INT NOT NULL DEFAULT 0 COMMENT 'IN_ROOM 随机抽取权重,小于等于0按1处理',
|
sort INT NOT NULL DEFAULT 0,
|
||||||
sort INT NOT NULL DEFAULT 0 COMMENT '排序值,升序',
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
KEY idx_voice_room_rocket_reward_scene (sys_origin, level, reward_scene, enabled)
|
KEY idx_voice_room_rocket_reward_scene (sys_origin, level, reward_scene, enabled)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭奖励配置';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭奖励配置';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_status (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_status (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
room_id BIGINT NOT NULL COMMENT '房间ID',
|
room_id BIGINT NOT NULL,
|
||||||
day_key VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
day_key VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
||||||
round_no INT NOT NULL DEFAULT 1 COMMENT '当天第几轮火箭',
|
round_no INT NOT NULL DEFAULT 1,
|
||||||
current_level INT NOT NULL DEFAULT 1 COMMENT '当前等级',
|
current_level INT NOT NULL DEFAULT 1,
|
||||||
current_energy BIGINT NOT NULL DEFAULT 0 COMMENT '当前等级已累计能量',
|
current_energy BIGINT NOT NULL DEFAULT 0,
|
||||||
need_energy BIGINT NOT NULL COMMENT '当前等级所需能量',
|
need_energy BIGINT NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL COMMENT 'CHARGING/LAUNCHING',
|
status VARCHAR(32) NOT NULL COMMENT 'CHARGING/LAUNCHING',
|
||||||
level_start_time DATETIME NOT NULL COMMENT '当前等级开始时间',
|
level_start_time DATETIME NOT NULL,
|
||||||
last_event_time DATETIME DEFAULT NULL COMMENT '最近一次有效送礼事件时间',
|
last_event_time DATETIME DEFAULT NULL,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_status (sys_origin, room_id, day_key)
|
UNIQUE KEY uk_voice_room_rocket_status (sys_origin, room_id, day_key)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭当前状态';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭当前状态';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_level_round (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_level_round (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
room_id BIGINT NOT NULL COMMENT '房间ID',
|
room_id BIGINT NOT NULL,
|
||||||
day_key VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
day_key VARCHAR(16) NOT NULL,
|
||||||
round_no INT NOT NULL COMMENT '当天第几轮火箭',
|
round_no INT NOT NULL,
|
||||||
level INT NOT NULL COMMENT '火箭等级',
|
level INT NOT NULL,
|
||||||
need_energy BIGINT NOT NULL COMMENT '本级所需能量',
|
need_energy BIGINT NOT NULL,
|
||||||
final_energy BIGINT NOT NULL DEFAULT 0 COMMENT '本级最终累计能量',
|
final_energy BIGINT NOT NULL DEFAULT 0,
|
||||||
status VARCHAR(32) NOT NULL COMMENT 'CHARGING/LAUNCHED',
|
status VARCHAR(32) NOT NULL COMMENT 'CHARGING/LAUNCHED',
|
||||||
top1_user_id BIGINT DEFAULT NULL COMMENT '本级贡献第一用户ID',
|
top1_user_id BIGINT DEFAULT NULL,
|
||||||
ignite_user_id BIGINT DEFAULT NULL COMMENT '点火用户ID',
|
ignite_user_id BIGINT DEFAULT NULL,
|
||||||
reach_time DATETIME DEFAULT NULL COMMENT '能量达标时间',
|
reach_time DATETIME DEFAULT NULL COMMENT '能量达标时间',
|
||||||
launch_time DATETIME DEFAULT NULL COMMENT '发射时间',
|
launch_time DATETIME DEFAULT NULL COMMENT '发射时间',
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_level_round (sys_origin, room_id, day_key, round_no, level),
|
UNIQUE KEY uk_voice_room_rocket_level_round (sys_origin, room_id, day_key, round_no, level),
|
||||||
KEY idx_voice_room_rocket_level_round_room (sys_origin, room_id, day_key, round_no)
|
KEY idx_voice_room_rocket_level_round_room (sys_origin, room_id, day_key, round_no)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭每轮每级快照';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭每轮每级快照';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_contribution (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_contribution (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
room_id BIGINT NOT NULL COMMENT '房间ID',
|
room_id BIGINT NOT NULL,
|
||||||
day_key VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
day_key VARCHAR(16) NOT NULL,
|
||||||
round_no INT NOT NULL COMMENT '当天第几轮火箭',
|
round_no INT NOT NULL,
|
||||||
level INT NOT NULL COMMENT '火箭等级',
|
level INT NOT NULL,
|
||||||
user_id BIGINT NOT NULL COMMENT '贡献用户ID',
|
user_id BIGINT NOT NULL,
|
||||||
score_energy BIGINT NOT NULL DEFAULT 0 COMMENT '计入榜单的能量值',
|
score_energy BIGINT NOT NULL DEFAULT 0,
|
||||||
gift_count INT NOT NULL DEFAULT 0 COMMENT '计入火箭的送礼次数',
|
gift_count INT NOT NULL DEFAULT 0,
|
||||||
first_contribute_time DATETIME NOT NULL COMMENT '首次贡献时间',
|
first_contribute_time DATETIME NOT NULL,
|
||||||
last_contribute_time DATETIME NOT NULL COMMENT '最近贡献时间',
|
last_contribute_time DATETIME NOT NULL,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_contribution_user (sys_origin, room_id, day_key, round_no, level, user_id),
|
UNIQUE KEY uk_voice_room_rocket_contribution_user (sys_origin, room_id, day_key, round_no, level, user_id),
|
||||||
KEY idx_voice_room_rocket_contribution_rank (sys_origin, room_id, day_key, round_no, level, score_energy, last_contribute_time)
|
KEY idx_voice_room_rocket_contribution_rank (sys_origin, room_id, day_key, round_no, level, score_energy, last_contribute_time)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭贡献榜';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭贡献榜';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_gift_event_log (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_gift_event_log (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
track_id VARCHAR(128) NOT NULL COMMENT '送礼事件唯一追踪ID',
|
track_id VARCHAR(128) NOT NULL,
|
||||||
room_id BIGINT NOT NULL COMMENT '房间ID',
|
room_id BIGINT NOT NULL,
|
||||||
send_user_id BIGINT NOT NULL COMMENT '送礼用户ID',
|
send_user_id BIGINT NOT NULL,
|
||||||
gift_id BIGINT DEFAULT NULL COMMENT '礼物ID',
|
gift_id BIGINT DEFAULT NULL,
|
||||||
gift_type VARCHAR(32) DEFAULT NULL COMMENT '礼物类型',
|
gift_type VARCHAR(32) DEFAULT NULL,
|
||||||
gift_tab VARCHAR(64) DEFAULT NULL COMMENT '礼物分类',
|
gift_tab VARCHAR(64) DEFAULT NULL,
|
||||||
raw_energy BIGINT NOT NULL DEFAULT 0 COMMENT '原始能量值',
|
raw_energy BIGINT NOT NULL DEFAULT 0,
|
||||||
accepted_energy BIGINT NOT NULL DEFAULT 0 COMMENT '实际计入火箭能量值',
|
accepted_energy BIGINT NOT NULL DEFAULT 0,
|
||||||
day_key VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
day_key VARCHAR(16) NOT NULL,
|
||||||
round_no INT NOT NULL COMMENT '当天第几轮火箭',
|
round_no INT NOT NULL,
|
||||||
level INT NOT NULL COMMENT '火箭等级',
|
level INT NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL COMMENT 'IGNORED/SUCCESS/FAILED',
|
status VARCHAR(32) NOT NULL COMMENT 'IGNORED/SUCCESS/FAILED',
|
||||||
raw_payload MEDIUMTEXT COMMENT '原始消息内容',
|
raw_payload MEDIUMTEXT,
|
||||||
error_message VARCHAR(1024) DEFAULT NULL COMMENT '失败原因',
|
error_message VARCHAR(1024) DEFAULT NULL,
|
||||||
event_time DATETIME NOT NULL COMMENT '事件发生时间',
|
event_time DATETIME NOT NULL,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_track (track_id),
|
UNIQUE KEY uk_voice_room_rocket_track (track_id),
|
||||||
KEY idx_voice_room_rocket_event_room (sys_origin, room_id, day_key, round_no, level)
|
KEY idx_voice_room_rocket_event_room (sys_origin, room_id, day_key, round_no, level)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭送礼事件日志';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭送礼事件日志';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_launch (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_launch (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
launch_no VARCHAR(64) NOT NULL COMMENT '发射编号',
|
launch_no VARCHAR(64) NOT NULL,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
room_id BIGINT NOT NULL COMMENT '房间ID',
|
room_id BIGINT NOT NULL,
|
||||||
room_account VARCHAR(64) DEFAULT NULL COMMENT '房间号',
|
room_account VARCHAR(64) DEFAULT NULL,
|
||||||
day_key VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
day_key VARCHAR(16) NOT NULL,
|
||||||
round_no INT NOT NULL COMMENT '当天第几轮火箭',
|
round_no INT NOT NULL,
|
||||||
level INT NOT NULL COMMENT '火箭等级',
|
level INT NOT NULL,
|
||||||
final_energy BIGINT NOT NULL COMMENT '发射时最终能量',
|
final_energy BIGINT NOT NULL,
|
||||||
top1_user_id BIGINT DEFAULT NULL COMMENT '贡献第一用户ID',
|
top1_user_id BIGINT DEFAULT NULL,
|
||||||
ignite_user_id BIGINT DEFAULT NULL COMMENT '点火用户ID',
|
ignite_user_id BIGINT DEFAULT NULL,
|
||||||
in_room_snapshot_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '在房用户快照状态,PENDING/SUCCESS/FAILED',
|
in_room_snapshot_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
|
||||||
launch_time DATETIME NOT NULL COMMENT '发射时间',
|
launch_time DATETIME NOT NULL,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_launch_no (launch_no),
|
UNIQUE KEY uk_voice_room_rocket_launch_no (launch_no),
|
||||||
UNIQUE KEY uk_voice_room_rocket_launch_level (sys_origin, room_id, day_key, round_no, level),
|
UNIQUE KEY uk_voice_room_rocket_launch_level (sys_origin, room_id, day_key, round_no, level),
|
||||||
KEY idx_voice_room_rocket_launch_room (sys_origin, room_id, launch_time)
|
KEY idx_voice_room_rocket_launch_room (sys_origin, room_id, launch_time)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭发射记录';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭发射记录';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_reward_record (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_reward_record (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
launch_id BIGINT NOT NULL COMMENT '发射记录ID',
|
launch_id BIGINT NOT NULL,
|
||||||
launch_no VARCHAR(64) NOT NULL COMMENT '发射编号',
|
launch_no VARCHAR(64) NOT NULL,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
room_id BIGINT NOT NULL COMMENT '房间ID',
|
room_id BIGINT NOT NULL,
|
||||||
day_key VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
day_key VARCHAR(16) NOT NULL,
|
||||||
round_no INT NOT NULL COMMENT '当天第几轮火箭',
|
round_no INT NOT NULL,
|
||||||
level INT NOT NULL COMMENT '火箭等级',
|
level INT NOT NULL,
|
||||||
user_id BIGINT NOT NULL COMMENT '获奖用户ID',
|
user_id BIGINT NOT NULL,
|
||||||
reward_scene VARCHAR(32) NOT NULL COMMENT '奖励场景,TOP1/IGNITE/IN_ROOM',
|
reward_scene VARCHAR(32) NOT NULL,
|
||||||
reward_config_id BIGINT NOT NULL COMMENT '奖励配置ID',
|
reward_config_id BIGINT NOT NULL,
|
||||||
reward_type VARCHAR(32) NOT NULL COMMENT '奖励类型,GOLD/MOUNT/AVATAR_FRAME/CHAT_BUBBLE/VIP_CARD/GIFT/BADGE/NONE',
|
reward_type VARCHAR(32) NOT NULL,
|
||||||
reward_item_id BIGINT DEFAULT NULL COMMENT '奖励资源ID',
|
reward_item_id BIGINT DEFAULT NULL,
|
||||||
reward_name VARCHAR(128) NOT NULL COMMENT '奖励名称',
|
reward_name VARCHAR(128) NOT NULL,
|
||||||
reward_cover VARCHAR(512) DEFAULT NULL COMMENT '奖励封面URL',
|
reward_cover VARCHAR(512) DEFAULT NULL,
|
||||||
reward_amount BIGINT NOT NULL DEFAULT 1 COMMENT '金币数量或道具数量/天数',
|
reward_amount BIGINT NOT NULL DEFAULT 1,
|
||||||
expire_days INT DEFAULT NULL COMMENT '时效类奖励有效天数',
|
expire_days INT DEFAULT NULL,
|
||||||
grant_event_id VARCHAR(128) NOT NULL COMMENT '发奖幂等事件ID',
|
grant_event_id VARCHAR(128) NOT NULL,
|
||||||
grant_status VARCHAR(32) NOT NULL COMMENT 'PENDING/SUCCESS/FAILED',
|
grant_status VARCHAR(32) NOT NULL COMMENT 'PENDING/SUCCESS/FAILED',
|
||||||
popup_status VARCHAR(32) NOT NULL DEFAULT 'UNREAD' COMMENT 'UNREAD/READ',
|
popup_status VARCHAR(32) NOT NULL DEFAULT 'UNREAD' COMMENT 'UNREAD/READ',
|
||||||
error_message VARCHAR(1024) DEFAULT NULL COMMENT '发奖失败原因',
|
error_message VARCHAR(1024) DEFAULT NULL,
|
||||||
grant_time DATETIME DEFAULT NULL COMMENT '发奖完成时间',
|
grant_time DATETIME DEFAULT NULL,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_reward_event (grant_event_id),
|
UNIQUE KEY uk_voice_room_rocket_reward_event (grant_event_id),
|
||||||
KEY idx_voice_room_rocket_reward_user (sys_origin, user_id, create_time),
|
KEY idx_voice_room_rocket_reward_user (sys_origin, user_id, create_time),
|
||||||
KEY idx_voice_room_rocket_reward_launch (launch_id, user_id),
|
KEY idx_voice_room_rocket_reward_launch (launch_id, user_id),
|
||||||
KEY idx_voice_room_rocket_reward_retry (grant_status, update_time)
|
KEY idx_voice_room_rocket_reward_retry (grant_status, update_time)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭奖励记录';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭奖励记录';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS voice_room_rocket_audience_snapshot (
|
CREATE TABLE IF NOT EXISTS voice_room_rocket_audience_snapshot (
|
||||||
id BIGINT NOT NULL PRIMARY KEY COMMENT '主键ID',
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
launch_id BIGINT NOT NULL COMMENT '发射记录ID',
|
launch_id BIGINT NOT NULL,
|
||||||
sys_origin VARCHAR(32) NOT NULL COMMENT '系统来源',
|
sys_origin VARCHAR(32) NOT NULL,
|
||||||
room_id BIGINT NOT NULL COMMENT '房间ID',
|
room_id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL COMMENT '在房用户ID',
|
user_id BIGINT NOT NULL,
|
||||||
last_seen_time DATETIME NOT NULL COMMENT '最近在线时间',
|
last_seen_time DATETIME NOT NULL,
|
||||||
selected TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否被抽中发放在房奖励',
|
selected TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_voice_room_rocket_audience (launch_id, user_id),
|
UNIQUE KEY uk_voice_room_rocket_audience (launch_id, user_id),
|
||||||
KEY idx_voice_room_rocket_audience_launch (launch_id, selected)
|
KEY idx_voice_room_rocket_audience_launch (launch_id, selected)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭在房间用户快照';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='语音房火箭在房间用户快照';
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
SET @current_schema := DATABASE();
|
|
||||||
|
|
||||||
SET @add_voice_room_rocket_preview_url_sql := IF(
|
|
||||||
NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = @current_schema
|
|
||||||
AND table_name = 'voice_room_rocket_level_config'
|
|
||||||
AND column_name = 'preview_rocket_url'
|
|
||||||
),
|
|
||||||
'ALTER TABLE `voice_room_rocket_level_config` ADD COLUMN `preview_rocket_url` varchar(512) DEFAULT NULL COMMENT ''预览火箭资源URL'' AFTER `shake_threshold_percent`',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE add_voice_room_rocket_preview_url_stmt FROM @add_voice_room_rocket_preview_url_sql;
|
|
||||||
EXECUTE add_voice_room_rocket_preview_url_stmt;
|
|
||||||
DEALLOCATE PREPARE add_voice_room_rocket_preview_url_stmt;
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
SET @current_schema := DATABASE();
|
|
||||||
|
|
||||||
SET @add_voice_room_rocket_reward_copies_sql := IF(
|
|
||||||
NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = @current_schema
|
|
||||||
AND table_name = 'voice_room_rocket_reward_config'
|
|
||||||
AND column_name = 'reward_copies'
|
|
||||||
),
|
|
||||||
'ALTER TABLE `voice_room_rocket_reward_config` ADD COLUMN `reward_copies` int NOT NULL DEFAULT 1 COMMENT ''IN_ROOM 本奖励每次发射可发放份数'' AFTER `expire_days`',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE add_voice_room_rocket_reward_copies_stmt FROM @add_voice_room_rocket_reward_copies_sql;
|
|
||||||
EXECUTE add_voice_room_rocket_reward_copies_stmt;
|
|
||||||
DEALLOCATE PREPARE add_voice_room_rocket_reward_copies_stmt;
|
|
||||||
@ -1,178 +0,0 @@
|
|||||||
ALTER TABLE `voice_room_rocket_config`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭系统配置',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
|
||||||
MODIFY COLUMN `timezone` VARCHAR(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '业务时区',
|
|
||||||
MODIFY COLUMN `max_level` INT NOT NULL DEFAULT 6 COMMENT '最大等级',
|
|
||||||
MODIFY COLUMN `ranking_top_limit` INT NOT NULL DEFAULT 100 COMMENT '火箭王榜单展示人数',
|
|
||||||
MODIFY COLUMN `reward_record_keep_days` INT NOT NULL DEFAULT 35 COMMENT '用户奖励记录展示天数',
|
|
||||||
MODIFY COLUMN `broadcast_duration_seconds` INT NOT NULL DEFAULT 7 COMMENT '发射广播展示秒数',
|
|
||||||
MODIFY COLUMN `in_room_reward_delay_seconds` INT NOT NULL DEFAULT 10 COMMENT '在房间奖励延迟窗口秒数',
|
|
||||||
MODIFY COLUMN `energy_rule_json` JSON DEFAULT NULL COMMENT '礼物折算规则',
|
|
||||||
MODIFY COLUMN `rule_text` JSON DEFAULT NULL COMMENT '规则说明多语言文案',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_level_config`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭等级配置',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `level` INT NOT NULL COMMENT '火箭等级,Lv.1-Lv.6',
|
|
||||||
MODIFY COLUMN `need_energy` BIGINT NOT NULL COMMENT '本级发射所需能量',
|
|
||||||
MODIFY COLUMN `shake_threshold_percent` DECIMAL(8,4) NOT NULL DEFAULT 0 COMMENT '抖动阈值百分比',
|
|
||||||
MODIFY COLUMN `preview_rocket_url` VARCHAR(512) DEFAULT NULL COMMENT '预览火箭资源URL',
|
|
||||||
MODIFY COLUMN `rocket_icon_url` VARCHAR(512) DEFAULT NULL COMMENT '火箭图标资源URL',
|
|
||||||
MODIFY COLUMN `rocket_animation_url` VARCHAR(512) DEFAULT NULL COMMENT '火箭发射动画资源URL',
|
|
||||||
MODIFY COLUMN `progress_bar_url` VARCHAR(512) DEFAULT NULL COMMENT '进度条资源URL(历史字段)',
|
|
||||||
MODIFY COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
|
||||||
MODIFY COLUMN `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值,升序',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_reward_config`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭奖励配置',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `level` INT NOT NULL COMMENT '适用等级',
|
|
||||||
MODIFY COLUMN `reward_scene` VARCHAR(32) NOT NULL COMMENT '奖励场景,TOP1/IGNITE/IN_ROOM',
|
|
||||||
MODIFY COLUMN `reward_type` VARCHAR(32) NOT NULL COMMENT '奖励类型,GOLD/MOUNT/AVATAR_FRAME/CHAT_BUBBLE/VIP_CARD/GIFT/BADGE/NONE',
|
|
||||||
MODIFY COLUMN `reward_item_id` BIGINT DEFAULT NULL COMMENT '道具、礼物、权益等资源ID',
|
|
||||||
MODIFY COLUMN `reward_name` VARCHAR(128) NOT NULL COMMENT '奖励名称',
|
|
||||||
MODIFY COLUMN `reward_cover` VARCHAR(512) DEFAULT NULL COMMENT '奖励封面URL',
|
|
||||||
MODIFY COLUMN `reward_amount` BIGINT NOT NULL DEFAULT 1 COMMENT '金币数量或道具数量/天数',
|
|
||||||
MODIFY COLUMN `expire_days` INT DEFAULT NULL COMMENT '时效类奖励有效天数',
|
|
||||||
MODIFY COLUMN `reward_copies` INT NOT NULL DEFAULT 1 COMMENT 'IN_ROOM 本奖励每次发射可发放份数',
|
|
||||||
MODIFY COLUMN `weight` INT NOT NULL DEFAULT 0 COMMENT 'IN_ROOM 随机抽取权重,小于等于0按1处理',
|
|
||||||
MODIFY COLUMN `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值,升序',
|
|
||||||
MODIFY COLUMN `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_status`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭当前状态',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `room_id` BIGINT NOT NULL COMMENT '房间ID',
|
|
||||||
MODIFY COLUMN `day_key` VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
|
||||||
MODIFY COLUMN `round_no` INT NOT NULL DEFAULT 1 COMMENT '当天第几轮火箭',
|
|
||||||
MODIFY COLUMN `current_level` INT NOT NULL DEFAULT 1 COMMENT '当前等级',
|
|
||||||
MODIFY COLUMN `current_energy` BIGINT NOT NULL DEFAULT 0 COMMENT '当前等级已累计能量',
|
|
||||||
MODIFY COLUMN `need_energy` BIGINT NOT NULL COMMENT '当前等级所需能量',
|
|
||||||
MODIFY COLUMN `status` VARCHAR(32) NOT NULL COMMENT '状态,CHARGING/LAUNCHING',
|
|
||||||
MODIFY COLUMN `level_start_time` DATETIME NOT NULL COMMENT '当前等级开始时间',
|
|
||||||
MODIFY COLUMN `last_event_time` DATETIME DEFAULT NULL COMMENT '最近一次有效送礼事件时间',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_level_round`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭每轮每级快照',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `room_id` BIGINT NOT NULL COMMENT '房间ID',
|
|
||||||
MODIFY COLUMN `day_key` VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
|
||||||
MODIFY COLUMN `round_no` INT NOT NULL COMMENT '当天第几轮火箭',
|
|
||||||
MODIFY COLUMN `level` INT NOT NULL COMMENT '火箭等级',
|
|
||||||
MODIFY COLUMN `need_energy` BIGINT NOT NULL COMMENT '本级所需能量',
|
|
||||||
MODIFY COLUMN `final_energy` BIGINT NOT NULL DEFAULT 0 COMMENT '本级最终累计能量',
|
|
||||||
MODIFY COLUMN `status` VARCHAR(32) NOT NULL COMMENT '状态,CHARGING/LAUNCHED',
|
|
||||||
MODIFY COLUMN `top1_user_id` BIGINT DEFAULT NULL COMMENT '本级贡献第一用户ID',
|
|
||||||
MODIFY COLUMN `ignite_user_id` BIGINT DEFAULT NULL COMMENT '点火用户ID',
|
|
||||||
MODIFY COLUMN `reach_time` DATETIME DEFAULT NULL COMMENT '能量达标时间',
|
|
||||||
MODIFY COLUMN `launch_time` DATETIME DEFAULT NULL COMMENT '发射时间',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_contribution`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭贡献榜',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `room_id` BIGINT NOT NULL COMMENT '房间ID',
|
|
||||||
MODIFY COLUMN `day_key` VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
|
||||||
MODIFY COLUMN `round_no` INT NOT NULL COMMENT '当天第几轮火箭',
|
|
||||||
MODIFY COLUMN `level` INT NOT NULL COMMENT '火箭等级',
|
|
||||||
MODIFY COLUMN `user_id` BIGINT NOT NULL COMMENT '贡献用户ID',
|
|
||||||
MODIFY COLUMN `score_energy` BIGINT NOT NULL DEFAULT 0 COMMENT '计入榜单的能量值',
|
|
||||||
MODIFY COLUMN `gift_count` INT NOT NULL DEFAULT 0 COMMENT '计入火箭的送礼次数',
|
|
||||||
MODIFY COLUMN `first_contribute_time` DATETIME NOT NULL COMMENT '首次贡献时间',
|
|
||||||
MODIFY COLUMN `last_contribute_time` DATETIME NOT NULL COMMENT '最近贡献时间',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_gift_event_log`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭送礼事件日志',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `track_id` VARCHAR(128) NOT NULL COMMENT '送礼事件唯一追踪ID',
|
|
||||||
MODIFY COLUMN `room_id` BIGINT NOT NULL COMMENT '房间ID',
|
|
||||||
MODIFY COLUMN `send_user_id` BIGINT NOT NULL COMMENT '送礼用户ID',
|
|
||||||
MODIFY COLUMN `gift_id` BIGINT DEFAULT NULL COMMENT '礼物ID',
|
|
||||||
MODIFY COLUMN `gift_type` VARCHAR(32) DEFAULT NULL COMMENT '礼物类型',
|
|
||||||
MODIFY COLUMN `gift_tab` VARCHAR(64) DEFAULT NULL COMMENT '礼物分类',
|
|
||||||
MODIFY COLUMN `raw_energy` BIGINT NOT NULL DEFAULT 0 COMMENT '原始能量值',
|
|
||||||
MODIFY COLUMN `accepted_energy` BIGINT NOT NULL DEFAULT 0 COMMENT '实际计入火箭能量值',
|
|
||||||
MODIFY COLUMN `day_key` VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
|
||||||
MODIFY COLUMN `round_no` INT NOT NULL COMMENT '当天第几轮火箭',
|
|
||||||
MODIFY COLUMN `level` INT NOT NULL COMMENT '火箭等级',
|
|
||||||
MODIFY COLUMN `status` VARCHAR(32) NOT NULL COMMENT '处理状态,IGNORED/SUCCESS/FAILED',
|
|
||||||
MODIFY COLUMN `raw_payload` MEDIUMTEXT COMMENT '原始消息内容',
|
|
||||||
MODIFY COLUMN `error_message` VARCHAR(1024) DEFAULT NULL COMMENT '失败原因',
|
|
||||||
MODIFY COLUMN `event_time` DATETIME NOT NULL COMMENT '事件发生时间',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_launch`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭发射记录',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `launch_no` VARCHAR(64) NOT NULL COMMENT '发射编号',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `room_id` BIGINT NOT NULL COMMENT '房间ID',
|
|
||||||
MODIFY COLUMN `room_account` VARCHAR(64) DEFAULT NULL COMMENT '房间号',
|
|
||||||
MODIFY COLUMN `day_key` VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
|
||||||
MODIFY COLUMN `round_no` INT NOT NULL COMMENT '当天第几轮火箭',
|
|
||||||
MODIFY COLUMN `level` INT NOT NULL COMMENT '火箭等级',
|
|
||||||
MODIFY COLUMN `final_energy` BIGINT NOT NULL COMMENT '发射时最终能量',
|
|
||||||
MODIFY COLUMN `top1_user_id` BIGINT DEFAULT NULL COMMENT '贡献第一用户ID',
|
|
||||||
MODIFY COLUMN `ignite_user_id` BIGINT DEFAULT NULL COMMENT '点火用户ID',
|
|
||||||
MODIFY COLUMN `in_room_snapshot_status` VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '在房用户快照状态,PENDING/SUCCESS/FAILED',
|
|
||||||
MODIFY COLUMN `launch_time` DATETIME NOT NULL COMMENT '发射时间',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_reward_record`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭奖励记录',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `launch_id` BIGINT NOT NULL COMMENT '发射记录ID',
|
|
||||||
MODIFY COLUMN `launch_no` VARCHAR(64) NOT NULL COMMENT '发射编号',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `room_id` BIGINT NOT NULL COMMENT '房间ID',
|
|
||||||
MODIFY COLUMN `day_key` VARCHAR(16) NOT NULL COMMENT 'UTC+3 业务日 yyyyMMdd',
|
|
||||||
MODIFY COLUMN `round_no` INT NOT NULL COMMENT '当天第几轮火箭',
|
|
||||||
MODIFY COLUMN `level` INT NOT NULL COMMENT '火箭等级',
|
|
||||||
MODIFY COLUMN `user_id` BIGINT NOT NULL COMMENT '获奖用户ID',
|
|
||||||
MODIFY COLUMN `reward_scene` VARCHAR(32) NOT NULL COMMENT '奖励场景,TOP1/IGNITE/IN_ROOM',
|
|
||||||
MODIFY COLUMN `reward_config_id` BIGINT NOT NULL COMMENT '奖励配置ID',
|
|
||||||
MODIFY COLUMN `reward_type` VARCHAR(32) NOT NULL COMMENT '奖励类型,GOLD/MOUNT/AVATAR_FRAME/CHAT_BUBBLE/VIP_CARD/GIFT/BADGE/NONE',
|
|
||||||
MODIFY COLUMN `reward_item_id` BIGINT DEFAULT NULL COMMENT '奖励资源ID',
|
|
||||||
MODIFY COLUMN `reward_name` VARCHAR(128) NOT NULL COMMENT '奖励名称',
|
|
||||||
MODIFY COLUMN `reward_cover` VARCHAR(512) DEFAULT NULL COMMENT '奖励封面URL',
|
|
||||||
MODIFY COLUMN `reward_amount` BIGINT NOT NULL DEFAULT 1 COMMENT '金币数量或道具数量/天数',
|
|
||||||
MODIFY COLUMN `expire_days` INT DEFAULT NULL COMMENT '时效类奖励有效天数',
|
|
||||||
MODIFY COLUMN `grant_event_id` VARCHAR(128) NOT NULL COMMENT '发奖幂等事件ID',
|
|
||||||
MODIFY COLUMN `grant_status` VARCHAR(32) NOT NULL COMMENT '发奖状态,PENDING/SUCCESS/FAILED',
|
|
||||||
MODIFY COLUMN `popup_status` VARCHAR(32) NOT NULL DEFAULT 'UNREAD' COMMENT '弹窗状态,UNREAD/READ',
|
|
||||||
MODIFY COLUMN `error_message` VARCHAR(1024) DEFAULT NULL COMMENT '发奖失败原因',
|
|
||||||
MODIFY COLUMN `grant_time` DATETIME DEFAULT NULL COMMENT '发奖完成时间',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
MODIFY COLUMN `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间';
|
|
||||||
|
|
||||||
ALTER TABLE `voice_room_rocket_audience_snapshot`
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT = '语音房火箭在房用户快照',
|
|
||||||
MODIFY COLUMN `id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
MODIFY COLUMN `launch_id` BIGINT NOT NULL COMMENT '发射记录ID',
|
|
||||||
MODIFY COLUMN `sys_origin` VARCHAR(32) NOT NULL COMMENT '系统来源',
|
|
||||||
MODIFY COLUMN `room_id` BIGINT NOT NULL COMMENT '房间ID',
|
|
||||||
MODIFY COLUMN `user_id` BIGINT NOT NULL COMMENT '在房用户ID',
|
|
||||||
MODIFY COLUMN `last_seen_time` DATETIME NOT NULL COMMENT '最近在线时间',
|
|
||||||
MODIFY COLUMN `selected` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否被抽中发放在房奖励',
|
|
||||||
MODIFY COLUMN `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间';
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS `agora_error_log` (
|
|
||||||
`id` BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
`sys_origin` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '系统来源',
|
|
||||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '上报用户ID',
|
|
||||||
`room_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '房间ID',
|
|
||||||
`agora_uid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'Agora UID',
|
|
||||||
`error_code_type` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'Agora ErrorCodeType',
|
|
||||||
`connection_changed_reason_type` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'Agora ConnectionChangedReasonType',
|
|
||||||
`banned_by_server` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否 BannedByServer/kicking-rule',
|
|
||||||
`is_timeout` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否超时',
|
|
||||||
`token_request_success` TINYINT(1) DEFAULT NULL COMMENT 'token 请求是否成功',
|
|
||||||
`network_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '网络类型',
|
|
||||||
`req_client` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '请求客户端',
|
|
||||||
`req_app_intel` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'App版本信息请求头',
|
|
||||||
`client_ip` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '客户端IP',
|
|
||||||
`user_agent` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'User-Agent',
|
|
||||||
`raw_payload` MEDIUMTEXT COMMENT '原始上报内容',
|
|
||||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_agora_error_sys_time` (`sys_origin`, `create_time`),
|
|
||||||
KEY `idx_agora_error_user_time` (`user_id`, `create_time`),
|
|
||||||
KEY `idx_agora_error_room_time` (`room_id`, `create_time`),
|
|
||||||
KEY `idx_agora_error_code_time` (`error_code_type`, `create_time`),
|
|
||||||
KEY `idx_agora_error_reason` (`connection_changed_reason_type`),
|
|
||||||
KEY `idx_agora_error_banned_time` (`banned_by_server`, `create_time`),
|
|
||||||
KEY `idx_agora_error_network` (`network_type`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Agora错误日志';
|
|
||||||
Loading…
x
Reference in New Issue
Block a user