徽章
This commit is contained in:
parent
31433b0a1b
commit
41c1d326ee
@ -30,6 +30,7 @@ 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"
|
||||||
@ -58,6 +59,7 @@ 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)
|
||||||
@ -117,6 +119,7 @@ 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,
|
||||||
|
|||||||
49
internal/model/badge_models.go
Normal file
49
internal/model/badge_models.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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,6 +28,7 @@ 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"`
|
||||||
|
|||||||
@ -24,6 +24,7 @@ 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"
|
||||||
@ -53,6 +54,7 @@ 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
|
||||||
@ -79,6 +81,7 @@ 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)
|
||||||
|
|||||||
25
internal/router/user_badge_routes.go
Normal file
25
internal/router/user_badge_routes.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
121
internal/router/user_badge_routes_test.go
Normal file
121
internal/router/user_badge_routes_test.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
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 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
|
||||||
|
}
|
||||||
205
internal/service/userbadge/current.go
Normal file
205
internal/service/userbadge/current.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
now := s.now()
|
||||||
|
longBadges, vipBadgeID, err := s.loadLongBadges(ctx, sysOrigin, user.UserID, now)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shortBadges, err := s.loadShortBadges(ctx, sysOrigin, user.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: user.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)
|
||||||
|
}
|
||||||
137
internal/service/userbadge/current_test.go
Normal file
137
internal/service/userbadge/current_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
internal/service/userbadge/types.go
Normal file
122
internal/service/userbadge/types.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,6 +163,7 @@ 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)
|
||||||
|
|||||||
44
internal/service/voiceroomrocket/config_test.go
Normal file
44
internal/service/voiceroomrocket/config_test.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -140,6 +140,7 @@ 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,
|
||||||
|
|||||||
@ -315,6 +315,7 @@ 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"`
|
||||||
|
|||||||
@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS voice_room_rocket_level_config (
|
|||||||
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,
|
||||||
rocket_animation_url VARCHAR(512) DEFAULT NULL,
|
rocket_animation_url VARCHAR(512) DEFAULT NULL,
|
||||||
progress_bar_url VARCHAR(512) DEFAULT NULL,
|
progress_bar_url VARCHAR(512) DEFAULT NULL,
|
||||||
|
|||||||
16
migrations/037_voice_room_rocket_preview_rocket.sql
Normal file
16
migrations/037_voice_room_rocket_preview_rocket.sql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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;
|
||||||
Loading…
x
Reference in New Issue
Block a user