Compare commits
No commits in common. "ab8f7111693d6d88e671c18be013b1867bb5b00f" and "df7446fb8cb5c4105781db26775d69387a99bced" have entirely different histories.
ab8f711169
...
df7446fb8c
@ -20,7 +20,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/lingxian"
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
"chatapp3-golang/internal/service/managercenter"
|
|
||||||
"chatapp3-golang/internal/service/rechargeagency"
|
"chatapp3-golang/internal/service/rechargeagency"
|
||||||
"chatapp3-golang/internal/service/rechargereward"
|
"chatapp3-golang/internal/service/rechargereward"
|
||||||
"chatapp3-golang/internal/service/regionimgroup"
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
@ -60,7 +59,6 @@ func main() {
|
|||||||
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
hostCenterService := hostcenter.NewService(app.Config, &app.Gateways)
|
hostCenterService := hostcenter.NewService(app.Config, &app.Gateways)
|
||||||
managerCenterService := managercenter.NewService(app.Repository.DB, &app.Gateways)
|
|
||||||
|
|
||||||
baishunService.SetTaskEventReporter(taskCenterService)
|
baishunService.SetTaskEventReporter(taskCenterService)
|
||||||
gameOpenService.SetTaskEventReporter(taskCenterService)
|
gameOpenService.SetTaskEventReporter(taskCenterService)
|
||||||
@ -94,7 +92,6 @@ func main() {
|
|||||||
GameOpen: gameOpenService,
|
GameOpen: gameOpenService,
|
||||||
GameProviders: gameProviders,
|
GameProviders: gameProviders,
|
||||||
LuckyGift: luckyGiftService,
|
LuckyGift: luckyGiftService,
|
||||||
ManagerCenter: managerCenterService,
|
|
||||||
HostCenter: hostCenterService,
|
HostCenter: hostCenterService,
|
||||||
RechargeAgency: rechargeAgencyService,
|
RechargeAgency: rechargeAgencyService,
|
||||||
RechargeReward: rechargeRewardService,
|
RechargeReward: rechargeRewardService,
|
||||||
|
|||||||
@ -246,7 +246,7 @@ func Load() Config {
|
|||||||
BridgeBaseURL: getEnvAny([]string{"CHATAPP_JAVA_BRIDGE_BASE_URL", "INVITE_JAVA_BRIDGE_BASE_URL"}, consoleBaseURL),
|
BridgeBaseURL: getEnvAny([]string{"CHATAPP_JAVA_BRIDGE_BASE_URL", "INVITE_JAVA_BRIDGE_BASE_URL"}, consoleBaseURL),
|
||||||
ExternalBaseURL: getEnvAny([]string{"CHATAPP_JAVA_EXTERNAL_BASE_URL", "INVITE_JAVA_EXTERNAL_BASE_URL"}, "http://127.0.0.1:3000"),
|
ExternalBaseURL: getEnvAny([]string{"CHATAPP_JAVA_EXTERNAL_BASE_URL", "INVITE_JAVA_EXTERNAL_BASE_URL"}, "http://127.0.0.1:3000"),
|
||||||
OtherBaseURL: getEnvAny([]string{"CHATAPP_JAVA_OTHER_BASE_URL", "GAME_JAVA_OTHER_BASE_URL", "INVITE_JAVA_APP_BASE_URL"}, javaAppBaseURL),
|
OtherBaseURL: getEnvAny([]string{"CHATAPP_JAVA_OTHER_BASE_URL", "GAME_JAVA_OTHER_BASE_URL", "INVITE_JAVA_APP_BASE_URL"}, javaAppBaseURL),
|
||||||
OrderBaseURL: getEnvAny([]string{"CHATAPP_JAVA_ORDER_BASE_URL", "GAME_JAVA_ORDER_BASE_URL", "LIKEI_GATEWAY_ROUTE_ORDER_URI"}, "http://127.0.0.1:2600"),
|
OrderBaseURL: getEnvAny([]string{"CHATAPP_JAVA_ORDER_BASE_URL", "GAME_JAVA_ORDER_BASE_URL"}, "http://127.0.0.1:2600"),
|
||||||
WalletBaseURL: getEnvAny([]string{"CHATAPP_JAVA_WALLET_BASE_URL", "GAME_JAVA_WALLET_BASE_URL"}, "http://127.0.0.1:2300"),
|
WalletBaseURL: getEnvAny([]string{"CHATAPP_JAVA_WALLET_BASE_URL", "GAME_JAVA_WALLET_BASE_URL"}, "http://127.0.0.1:2300"),
|
||||||
},
|
},
|
||||||
Worker: WorkerConfig{
|
Worker: WorkerConfig{
|
||||||
|
|||||||
@ -96,16 +96,6 @@ func (g *Gateways) GivePropsBackpack(ctx context.Context, req GivePropsBackpackR
|
|||||||
return g.RewardDispatch.GivePropsBackpack(ctx, req)
|
return g.RewardDispatch.GivePropsBackpack(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNobleVIPAbility 透传到贵族 VIP 能力配置接口。
|
|
||||||
func (g *Gateways) GetNobleVIPAbility(ctx context.Context, sourceID int64) (PropsNobleVIPAbility, error) {
|
|
||||||
return g.RewardQuery.GetNobleVIPAbility(ctx, sourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserMaxNobleVIPAbility 透传到用户当前最高贵族 VIP 能力配置接口。
|
|
||||||
func (g *Gateways) GetUserMaxNobleVIPAbility(ctx context.Context, userID int64) (PropsNobleVIPAbility, error) {
|
|
||||||
return g.RewardQuery.GetUserMaxNobleVIPAbility(ctx, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SwitchUseProps 透传到道具使用切换接口。
|
// SwitchUseProps 透传到道具使用切换接口。
|
||||||
func (g *Gateways) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
func (g *Gateways) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
||||||
return g.RewardDispatch.SwitchUseProps(ctx, userID, propsID)
|
return g.RewardDispatch.SwitchUseProps(ctx, userID, propsID)
|
||||||
@ -161,11 +151,6 @@ func (g *Gateways) RemoveUserProfileCacheAll(ctx context.Context, userID int64)
|
|||||||
return g.Profile.RemoveUserProfileCacheAll(ctx, userID)
|
return g.Profile.RemoveUserProfileCacheAll(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PunishAccount 透传到账号处罚接口。
|
|
||||||
func (g *Gateways) PunishAccount(ctx context.Context, req PunishAccountRequest) error {
|
|
||||||
return g.Profile.PunishAccount(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListCurrentWeekRoomContribution 透传到房间流水网关。
|
// ListCurrentWeekRoomContribution 透传到房间流水网关。
|
||||||
func (g *Gateways) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
func (g *Gateways) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
||||||
return g.Room.ListCurrentWeekRoomContribution(ctx, limit)
|
return g.Room.ListCurrentWeekRoomContribution(ctx, limit)
|
||||||
@ -321,16 +306,6 @@ func (g *RewardQueryGateway) GetRewardGroupDetail(ctx context.Context, groupID i
|
|||||||
return g.client.GetRewardGroupDetail(ctx, groupID)
|
return g.client.GetRewardGroupDetail(ctx, groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNobleVIPAbility 查询贵族 VIP 能力配置。
|
|
||||||
func (g *RewardQueryGateway) GetNobleVIPAbility(ctx context.Context, sourceID int64) (PropsNobleVIPAbility, error) {
|
|
||||||
return g.client.GetNobleVIPAbility(ctx, sourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserMaxNobleVIPAbility 查询用户当前最高贵族 VIP 能力配置。
|
|
||||||
func (g *RewardQueryGateway) GetUserMaxNobleVIPAbility(ctx context.Context, userID int64) (PropsNobleVIPAbility, error) {
|
|
||||||
return g.client.GetUserMaxNobleVIPAbility(ctx, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProfileGateway 负责用户资料相关接口。
|
// ProfileGateway 负责用户资料相关接口。
|
||||||
type ProfileGateway struct {
|
type ProfileGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
@ -371,11 +346,6 @@ func (g *ProfileGateway) RemoveUserProfileCacheAll(ctx context.Context, userID i
|
|||||||
return g.client.RemoveUserProfileCacheAll(ctx, userID)
|
return g.client.RemoveUserProfileCacheAll(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PunishAccount 处罚用户账号。
|
|
||||||
func (g *ProfileGateway) PunishAccount(ctx context.Context, req PunishAccountRequest) error {
|
|
||||||
return g.client.PunishAccount(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomGateway 负责房间资料和房间流水相关接口。
|
// RoomGateway 负责房间资料和房间流水相关接口。
|
||||||
type RoomGateway struct {
|
type RoomGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
|
|||||||
@ -51,7 +51,6 @@ type UserProfile struct {
|
|||||||
CountryName string `json:"countryName"`
|
CountryName string `json:"countryName"`
|
||||||
RegionCode string `json:"regionCode"`
|
RegionCode string `json:"regionCode"`
|
||||||
OriginSys string `json:"originSys"`
|
OriginSys string `json:"originSys"`
|
||||||
AccountStatus string `json:"accountStatus"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRegion struct {
|
type UserRegion struct {
|
||||||
@ -160,25 +159,6 @@ type GivePropsBackpackRequest struct {
|
|||||||
OpUser int64 `json:"opUser,omitempty"`
|
OpUser int64 `json:"opUser,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropsNobleVIPAbility struct {
|
|
||||||
ID Int64Value `json:"id"`
|
|
||||||
VIPType string `json:"vipType"`
|
|
||||||
VIPLevel int `json:"vipLevel"`
|
|
||||||
AdminNumber int `json:"adminNumber"`
|
|
||||||
RoomMaxMember int `json:"roomMaxMember"`
|
|
||||||
AvatarFrameID Int64Value `json:"avatarFrameId"`
|
|
||||||
CarID Int64Value `json:"carId"`
|
|
||||||
ChatBubbleID Int64Value `json:"chatBubbleId"`
|
|
||||||
DataCardID Int64Value `json:"dataCardId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PunishAccountRequest struct {
|
|
||||||
BeOptUserID int64 `json:"beOptUserId"`
|
|
||||||
OptUserID int64 `json:"optUserId"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendActivityRewardRequest struct {
|
type SendActivityRewardRequest struct {
|
||||||
TrackID int64 `json:"trackId"`
|
TrackID int64 `json:"trackId"`
|
||||||
Origin string `json:"origin"`
|
Origin string `json:"origin"`
|
||||||
@ -615,26 +595,6 @@ func (c *Client) GivePropsBackpack(ctx context.Context, req GivePropsBackpackReq
|
|||||||
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/props/client/giveProps", req, nil, nil)
|
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/props/client/giveProps", req, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetNobleVIPAbility(ctx context.Context, sourceID int64) (PropsNobleVIPAbility, error) {
|
|
||||||
endpoint := c.cfg.Java.OtherBaseURL + "/props/noble-vip/client/getAbilityDTO?sourceId=" +
|
|
||||||
url.QueryEscape(strconv.FormatInt(sourceID, 10))
|
|
||||||
var resp resultResponse[PropsNobleVIPAbility]
|
|
||||||
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
|
||||||
return PropsNobleVIPAbility{}, err
|
|
||||||
}
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) GetUserMaxNobleVIPAbility(ctx context.Context, userID int64) (PropsNobleVIPAbility, error) {
|
|
||||||
endpoint := c.cfg.Java.OtherBaseURL + "/props/noble-vip/client/getUserMaxAbilityDTO?userId=" +
|
|
||||||
url.QueryEscape(strconv.FormatInt(userID, 10))
|
|
||||||
var resp resultResponse[PropsNobleVIPAbility]
|
|
||||||
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
|
||||||
return PropsNobleVIPAbility{}, err
|
|
||||||
}
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
func (c *Client) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
||||||
endpoint := c.cfg.Java.OtherBaseURL + "/props/client/switch/use-props?userId=" +
|
endpoint := c.cfg.Java.OtherBaseURL + "/props/client/switch/use-props?userId=" +
|
||||||
url.QueryEscape(strconv.FormatInt(userID, 10)) +
|
url.QueryEscape(strconv.FormatInt(userID, 10)) +
|
||||||
@ -966,10 +926,6 @@ func (c *Client) RemoveUserProfileCacheAll(ctx context.Context, userID int64) er
|
|||||||
return c.getJSON(ctx, endpoint, nil, nil)
|
return c.getJSON(ctx, endpoint, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) PunishAccount(ctx context.Context, req PunishAccountRequest) error {
|
|
||||||
return c.postJSON(ctx, c.cfg.Java.OtherBaseURL+"/app-user/account/punishment", req, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
func (c *Client) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
||||||
endpoint := c.cfg.Java.WalletBaseURL + "/wallet/gold/client/mapBalance"
|
endpoint := c.cfg.Java.WalletBaseURL + "/wallet/gold/client/mapBalance"
|
||||||
var resp resultResponse[map[string]int64]
|
var resp resultResponse[map[string]int64]
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/service/managercenter"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registerManagerCenterRoutes 注册经理中心 H5 路由。
|
|
||||||
func registerManagerCenterRoutes(engine *gin.Engine, javaClient authGateway, service *managercenter.Service) {
|
|
||||||
if service == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
group := engine.Group("/app/h5/manager-center")
|
|
||||||
group.Use(authMiddleware(javaClient))
|
|
||||||
|
|
||||||
group.GET("/profile", func(c *gin.Context) {
|
|
||||||
resp, err := service.GetProfile(c.Request.Context(), mustAuthUser(c))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
group.GET("/users/search", func(c *gin.Context) {
|
|
||||||
account := strings.TrimSpace(c.Query("account"))
|
|
||||||
resp, err := service.SearchUser(c.Request.Context(), mustAuthUser(c), account)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
group.GET("/props", func(c *gin.Context) {
|
|
||||||
resp, err := service.ListProps(c.Request.Context(), mustAuthUser(c), c.Query("type"))
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
group.POST("/props/send", func(c *gin.Context) {
|
|
||||||
var req managercenter.SendPropsRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, managercenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := service.SendProps(c.Request.Context(), mustAuthUser(c), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
|
|
||||||
group.POST("/users/ban", func(c *gin.Context) {
|
|
||||||
var req managercenter.BanUserRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, managercenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := service.BanUser(c.Request.Context(), mustAuthUser(c), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -14,7 +14,6 @@ import (
|
|||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
"chatapp3-golang/internal/service/lingxian"
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
"chatapp3-golang/internal/service/managercenter"
|
|
||||||
"chatapp3-golang/internal/service/rechargeagency"
|
"chatapp3-golang/internal/service/rechargeagency"
|
||||||
"chatapp3-golang/internal/service/rechargereward"
|
"chatapp3-golang/internal/service/rechargereward"
|
||||||
"chatapp3-golang/internal/service/regionimgroup"
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
@ -38,7 +37,6 @@ type Services struct {
|
|||||||
GameOpen *gameopen.GameOpenService
|
GameOpen *gameopen.GameOpenService
|
||||||
GameProviders *gameprovider.Registry
|
GameProviders *gameprovider.Registry
|
||||||
LuckyGift *luckygift.LuckyGiftService
|
LuckyGift *luckygift.LuckyGiftService
|
||||||
ManagerCenter *managercenter.Service
|
|
||||||
HostCenter *hostcenter.Service
|
HostCenter *hostcenter.Service
|
||||||
RechargeAgency *rechargeagency.Service
|
RechargeAgency *rechargeagency.Service
|
||||||
RechargeReward *rechargereward.Service
|
RechargeReward *rechargereward.Service
|
||||||
@ -81,7 +79,6 @@ func NewRouter(
|
|||||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||||
registerLingxianRoutes(engine, javaClient, services.Lingxian)
|
registerLingxianRoutes(engine, javaClient, services.Lingxian)
|
||||||
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
||||||
registerManagerCenterRoutes(engine, javaClient, services.ManagerCenter)
|
|
||||||
|
|
||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,654 +0,0 @@
|
|||||||
package managercenter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type administratorRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
UserID int64 `gorm:"column:user_id"`
|
|
||||||
Status bool `gorm:"column:status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (administratorRow) TableName() string {
|
|
||||||
return "sys_administrator"
|
|
||||||
}
|
|
||||||
|
|
||||||
type userBaseInfoRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
Account string `gorm:"column:account"`
|
|
||||||
UserAvatar string `gorm:"column:user_avatar"`
|
|
||||||
UserNickname string `gorm:"column:user_nickname"`
|
|
||||||
CountryCode string `gorm:"column:country_code"`
|
|
||||||
CountryName string `gorm:"column:country_name"`
|
|
||||||
OriginSys string `gorm:"column:origin_sys"`
|
|
||||||
AccountStatus string `gorm:"column:account_status"`
|
|
||||||
IsDel bool `gorm:"column:is_del"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (userBaseInfoRow) TableName() string {
|
|
||||||
return "user_base_info"
|
|
||||||
}
|
|
||||||
|
|
||||||
type propsSourceRecordRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
Type string `gorm:"column:type"`
|
|
||||||
Code string `gorm:"column:code"`
|
|
||||||
Name string `gorm:"column:name"`
|
|
||||||
Cover string `gorm:"column:cover"`
|
|
||||||
SourceURL string `gorm:"column:source_url"`
|
|
||||||
Expand string `gorm:"column:expand"`
|
|
||||||
AdminFree bool `gorm:"column:admin_free"`
|
|
||||||
IsDel bool `gorm:"column:is_del"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (propsSourceRecordRow) TableName() string {
|
|
||||||
return "props_source_record"
|
|
||||||
}
|
|
||||||
|
|
||||||
type propsNobleVIPAbilityRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
VIPLevel int `gorm:"column:vip_level"`
|
|
||||||
AvatarFrameID int64 `gorm:"column:avatar_frame_id"`
|
|
||||||
CarID int64 `gorm:"column:car_id"`
|
|
||||||
ChatBubbleID int64 `gorm:"column:chat_bubble_id"`
|
|
||||||
DataCardID int64 `gorm:"column:data_card_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (propsNobleVIPAbilityRow) TableName() string {
|
|
||||||
return "props_noble_vip_ability"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetProfile(ctx context.Context, user AuthUser) (*ProfileResponse, error) {
|
|
||||||
if err := s.ensureManager(ctx, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
view, err := s.loadUserViewByID(ctx, user.UserID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ProfileResponse{Manager: view}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SearchUser(ctx context.Context, user AuthUser, account string) (*UserSearchResponse, error) {
|
|
||||||
if err := s.ensureManager(ctx, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
account = strings.TrimSpace(account)
|
|
||||||
if account == "" {
|
|
||||||
return nil, badRequest("account_required", "account is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
view, err := s.findUserView(ctx, account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &UserSearchResponse{User: view}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListProps(ctx context.Context, user AuthUser, propsType string) (*PropsListResponse, error) {
|
|
||||||
if err := s.ensureManager(ctx, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if s.db == nil {
|
|
||||||
return nil, serviceUnavailable("database_unavailable", "database is unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
propsType = normalizePropsType(propsType)
|
|
||||||
if propsType == propsTypeNobleVIP {
|
|
||||||
return s.listVIPProps(ctx, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := s.db.WithContext(ctx).
|
|
||||||
Table("props_source_record").
|
|
||||||
Select("id, type, code, name, cover, source_url, expand, admin_free, is_del").
|
|
||||||
Where("is_del = ? AND admin_free = ?", false, true)
|
|
||||||
if propsType != "" {
|
|
||||||
query = query.Where("type = ?", propsType)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rows []propsSourceRecordRow
|
|
||||||
if err := query.Order("type ASC, id ASC").Find(&rows).Error; err != nil {
|
|
||||||
return nil, serverError("props_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
vipLevels, err := s.loadVIPLevels(ctx, rows)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
records := make([]PropsView, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
view, ok := propsRowToView(row, vipLevels)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
records = append(records, view)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PropsListResponse{
|
|
||||||
Type: propsType,
|
|
||||||
Records: records,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendProps(ctx context.Context, user AuthUser, req SendPropsRequest) (*SendPropsResponse, error) {
|
|
||||||
if err := s.ensureManager(ctx, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if req.AcceptUserID.Int64() <= 0 {
|
|
||||||
return nil, badRequest("accept_user_required", "acceptUserId is required")
|
|
||||||
}
|
|
||||||
if req.PropsID.Int64() <= 0 {
|
|
||||||
return nil, badRequest("props_required", "propsId is required")
|
|
||||||
}
|
|
||||||
if s.java == nil {
|
|
||||||
return nil, serviceUnavailable("java_gateway_unavailable", "java gateway is unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := s.loadUserRowByID(ctx, req.AcceptUserID.Int64())
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, notFound("user_not_found", "user not found")
|
|
||||||
}
|
|
||||||
return nil, serverError("user_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
prop, err := s.loadGiftableProps(ctx, req.PropsID.Int64())
|
|
||||||
if err != nil {
|
|
||||||
if isAppErrorCode(err, "prop_not_giftable") {
|
|
||||||
if resp, vipErr := s.sendVIPGift(ctx, user, target, req.PropsID.Int64()); vipErr != nil {
|
|
||||||
return nil, vipErr
|
|
||||||
} else if resp != nil {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
days := propsGiftDays(prop.Type)
|
|
||||||
grants := []propsGrant{{
|
|
||||||
propsID: prop.ID,
|
|
||||||
typ: prop.Type,
|
|
||||||
days: days,
|
|
||||||
}}
|
|
||||||
if prop.Type == propsTypeNobleVIP {
|
|
||||||
if _, err := s.loadGiftableNobleVIPAbility(ctx, prop.ID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ability, err := s.java.GetNobleVIPAbility(ctx, prop.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, serverError("vip_ability_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
grants = append(grants, nobleVIPAccessoryGrants(ability)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, grant := range grants {
|
|
||||||
if err := s.java.GivePropsBackpack(ctx, integration.GivePropsBackpackRequest{
|
|
||||||
AcceptUserID: req.AcceptUserID.Int64(),
|
|
||||||
PropsID: grant.propsID,
|
|
||||||
Type: grant.typ,
|
|
||||||
Origin: adminFreeOrigin,
|
|
||||||
OriginDesc: adminFreeOriginDesc,
|
|
||||||
Days: grant.days,
|
|
||||||
UseProps: true,
|
|
||||||
AllowGive: false,
|
|
||||||
OpUser: user.UserID,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, serverError("send_props_failed", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if prop.Type == propsTypeNobleVIP {
|
|
||||||
if err := s.switchMaxNobleVIP(ctx, req.AcceptUserID.Int64()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SendPropsResponse{
|
|
||||||
Success: true,
|
|
||||||
AcceptUserID: req.AcceptUserID,
|
|
||||||
PropsID: req.PropsID,
|
|
||||||
Type: prop.Type,
|
|
||||||
Days: days,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) BanUser(ctx context.Context, user AuthUser, req BanUserRequest) (*BanUserResponse, error) {
|
|
||||||
if err := s.ensureManager(ctx, user); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if req.UserID.Int64() <= 0 {
|
|
||||||
return nil, badRequest("user_required", "userId is required")
|
|
||||||
}
|
|
||||||
if s.java == nil {
|
|
||||||
return nil, serviceUnavailable("java_gateway_unavailable", "java gateway is unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
target, err := s.loadUserRowByID(ctx, req.UserID.Int64())
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, notFound("user_not_found", "user not found")
|
|
||||||
}
|
|
||||||
return nil, serverError("user_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRecharge, err := s.userHasRecharge(ctx, target.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if hasRecharge {
|
|
||||||
return nil, badRequest("user_recharged", "only users without recharge can be banned")
|
|
||||||
}
|
|
||||||
|
|
||||||
description := buildBanDescription(user.UserID, req.Reason)
|
|
||||||
if err := s.java.PunishAccount(ctx, integration.PunishAccountRequest{
|
|
||||||
BeOptUserID: target.ID,
|
|
||||||
OptUserID: user.UserID,
|
|
||||||
Status: accountStatusArchive,
|
|
||||||
Description: description,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, serverError("ban_user_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &BanUserResponse{
|
|
||||||
Success: true,
|
|
||||||
UserID: req.UserID,
|
|
||||||
AccountStatus: accountStatusArchive,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureManager(ctx context.Context, user AuthUser) error {
|
|
||||||
if user.UserID <= 0 {
|
|
||||||
return unauthorized("invalid_user", "authenticated user is required")
|
|
||||||
}
|
|
||||||
if s.db == nil {
|
|
||||||
return serviceUnavailable("database_unavailable", "database is unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Table("sys_administrator").
|
|
||||||
Where("user_id = ? AND status = ?", user.UserID, true).
|
|
||||||
Count(&count).Error
|
|
||||||
if err != nil {
|
|
||||||
return serverError("manager_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
return forbidden("not_manager", "user is not manager")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) findUserView(ctx context.Context, account string) (UserView, error) {
|
|
||||||
if userID, err := strconv.ParseInt(account, 10, 64); err == nil && userID > 0 {
|
|
||||||
if row, err := s.loadUserRowByID(ctx, userID); err == nil {
|
|
||||||
return s.userRowToView(ctx, row)
|
|
||||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return UserView{}, serverError("user_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.java != nil {
|
|
||||||
profile, err := s.java.GetUserProfileByAccountOne(ctx, account)
|
|
||||||
if err == nil && int64(profile.ID) > 0 {
|
|
||||||
if row, rowErr := s.loadUserRowByID(ctx, int64(profile.ID)); rowErr == nil {
|
|
||||||
view, viewErr := s.userRowToView(ctx, row)
|
|
||||||
if viewErr != nil {
|
|
||||||
return UserView{}, viewErr
|
|
||||||
}
|
|
||||||
overlayJavaProfile(&view, profile)
|
|
||||||
return view, nil
|
|
||||||
} else if !errors.Is(rowErr, gorm.ErrRecordNotFound) {
|
|
||||||
return UserView{}, serverError("user_query_failed", rowErr.Error())
|
|
||||||
}
|
|
||||||
return s.javaProfileToView(ctx, profile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row, err := s.loadUserRowByAccount(ctx, account)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return UserView{}, notFound("user_not_found", "user not found")
|
|
||||||
}
|
|
||||||
return UserView{}, serverError("user_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
return s.userRowToView(ctx, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadUserViewByID(ctx context.Context, userID int64) (UserView, error) {
|
|
||||||
row, err := s.loadUserRowByID(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return UserView{}, notFound("user_not_found", "user not found")
|
|
||||||
}
|
|
||||||
return UserView{}, serverError("user_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
return s.userRowToView(ctx, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadUserRowByID(ctx context.Context, userID int64) (userBaseInfoRow, error) {
|
|
||||||
var row userBaseInfoRow
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Table("user_base_info").
|
|
||||||
Where("id = ? AND is_del = ?", userID, false).
|
|
||||||
First(&row).Error
|
|
||||||
return row, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadUserRowByAccount(ctx context.Context, account string) (userBaseInfoRow, error) {
|
|
||||||
var row userBaseInfoRow
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Table("user_base_info").
|
|
||||||
Where("account = ? AND is_del = ?", account, false).
|
|
||||||
First(&row).Error
|
|
||||||
return row, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) userRowToView(ctx context.Context, row userBaseInfoRow) (UserView, error) {
|
|
||||||
hasRecharge, canBan := s.userRechargeEligibility(ctx, row.ID)
|
|
||||||
return UserView{
|
|
||||||
UserID: ID(row.ID),
|
|
||||||
Account: row.Account,
|
|
||||||
UserAvatar: row.UserAvatar,
|
|
||||||
UserNickname: row.UserNickname,
|
|
||||||
CountryCode: row.CountryCode,
|
|
||||||
CountryName: row.CountryName,
|
|
||||||
OriginSys: row.OriginSys,
|
|
||||||
AccountStatus: row.AccountStatus,
|
|
||||||
HasRecharge: hasRecharge,
|
|
||||||
CanBan: canBan,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) javaProfileToView(ctx context.Context, profile integration.UserProfile) (UserView, error) {
|
|
||||||
hasRecharge, canBan := s.userRechargeEligibility(ctx, int64(profile.ID))
|
|
||||||
return UserView{
|
|
||||||
UserID: ID(profile.ID),
|
|
||||||
Account: profile.Account,
|
|
||||||
UserAvatar: profile.UserAvatar,
|
|
||||||
UserNickname: profile.UserNickname,
|
|
||||||
CountryCode: profile.CountryCode,
|
|
||||||
CountryName: profile.CountryName,
|
|
||||||
OriginSys: profile.OriginSys,
|
|
||||||
AccountStatus: profile.AccountStatus,
|
|
||||||
HasRecharge: hasRecharge,
|
|
||||||
CanBan: canBan,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func overlayJavaProfile(view *UserView, profile integration.UserProfile) {
|
|
||||||
if profile.Account != "" {
|
|
||||||
view.Account = profile.Account
|
|
||||||
}
|
|
||||||
if profile.UserAvatar != "" {
|
|
||||||
view.UserAvatar = profile.UserAvatar
|
|
||||||
}
|
|
||||||
if profile.UserNickname != "" {
|
|
||||||
view.UserNickname = profile.UserNickname
|
|
||||||
}
|
|
||||||
if profile.CountryCode != "" {
|
|
||||||
view.CountryCode = profile.CountryCode
|
|
||||||
}
|
|
||||||
if profile.CountryName != "" {
|
|
||||||
view.CountryName = profile.CountryName
|
|
||||||
}
|
|
||||||
if profile.OriginSys != "" {
|
|
||||||
view.OriginSys = profile.OriginSys
|
|
||||||
}
|
|
||||||
if profile.AccountStatus != "" {
|
|
||||||
view.AccountStatus = profile.AccountStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) userHasRecharge(ctx context.Context, userID int64) (bool, error) {
|
|
||||||
if userID <= 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if s.java == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
recharges, err := s.java.MapUserTotalRecharge(ctx, []int64{userID})
|
|
||||||
if err != nil {
|
|
||||||
return false, serverError("recharge_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
for _, item := range recharges[userID] {
|
|
||||||
raw := strings.TrimSpace(string(item.Amount))
|
|
||||||
if raw == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
amount, err := strconv.ParseFloat(raw, 64)
|
|
||||||
if err != nil {
|
|
||||||
return false, serverError("recharge_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
if amount > 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) userRechargeEligibility(ctx context.Context, userID int64) (bool, bool) {
|
|
||||||
hasRecharge, err := s.userHasRecharge(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return false, false
|
|
||||||
}
|
|
||||||
return hasRecharge, !hasRecharge
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadGiftableProps(ctx context.Context, propsID int64) (propsSourceRecordRow, error) {
|
|
||||||
var row propsSourceRecordRow
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Table("props_source_record").
|
|
||||||
Select("id, type, code, name, cover, source_url, expand, admin_free, is_del").
|
|
||||||
Where("id = ? AND is_del = ? AND admin_free = ?", propsID, false, true).
|
|
||||||
First(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return propsSourceRecordRow{}, badRequest("prop_not_giftable", "props is not admin-free giftable")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return propsSourceRecordRow{}, serverError("props_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
return row, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadGiftableNobleVIPAbility(ctx context.Context, propsID int64) (propsNobleVIPAbilityRow, error) {
|
|
||||||
var row propsNobleVIPAbilityRow
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Table("props_noble_vip_ability").
|
|
||||||
Where("id = ?", propsID).
|
|
||||||
First(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return propsNobleVIPAbilityRow{}, badRequest("vip_not_giftable", "noble VIP ability is not giftable")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return propsNobleVIPAbilityRow{}, serverError("vip_ability_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
if row.VIPLevel > 3 {
|
|
||||||
return propsNobleVIPAbilityRow{}, badRequest("vip_not_giftable", "only noble VIP level 1-3 can be gifted")
|
|
||||||
}
|
|
||||||
return row, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadVIPLevels(ctx context.Context, rows []propsSourceRecordRow) (map[int64]int, error) {
|
|
||||||
ids := make([]int64, 0)
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.Type == propsTypeNobleVIP {
|
|
||||||
ids = append(ids, row.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(ids) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var abilities []propsNobleVIPAbilityRow
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Table("props_noble_vip_ability").
|
|
||||||
Select("id, vip_level").
|
|
||||||
Where("id IN ?", ids).
|
|
||||||
Find(&abilities).Error; err != nil {
|
|
||||||
return nil, serverError("vip_ability_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
levels := make(map[int64]int, len(abilities))
|
|
||||||
for _, ability := range abilities {
|
|
||||||
levels[ability.ID] = ability.VIPLevel
|
|
||||||
}
|
|
||||||
return levels, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type propsGrant struct {
|
|
||||||
propsID int64
|
|
||||||
typ string
|
|
||||||
days int
|
|
||||||
}
|
|
||||||
|
|
||||||
func nobleVIPAccessoryGrants(ability integration.PropsNobleVIPAbility) []propsGrant {
|
|
||||||
grants := make([]propsGrant, 0, 4)
|
|
||||||
appendIfPositive := func(propsID int64, typ string) {
|
|
||||||
if propsID <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
grants = append(grants, propsGrant{propsID: propsID, typ: typ, days: defaultVIPGiftDays})
|
|
||||||
}
|
|
||||||
appendIfPositive(int64(ability.CarID), propsTypeRide)
|
|
||||||
appendIfPositive(int64(ability.AvatarFrameID), propsTypeAvatarFrame)
|
|
||||||
appendIfPositive(int64(ability.ChatBubbleID), propsTypeChatBubble)
|
|
||||||
appendIfPositive(int64(ability.DataCardID), propsTypeDataCard)
|
|
||||||
return grants
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) switchMaxNobleVIP(ctx context.Context, userID int64) error {
|
|
||||||
ability, err := s.java.GetUserMaxNobleVIPAbility(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return serverError("vip_ability_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
ids := []int64{
|
|
||||||
int64(ability.ID),
|
|
||||||
int64(ability.AvatarFrameID),
|
|
||||||
int64(ability.DataCardID),
|
|
||||||
int64(ability.CarID),
|
|
||||||
int64(ability.ChatBubbleID),
|
|
||||||
}
|
|
||||||
for _, propsID := range compactPositiveIDs(ids) {
|
|
||||||
if err := s.java.SwitchUseProps(ctx, userID, propsID); err != nil {
|
|
||||||
return serverError("switch_props_failed", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil {
|
|
||||||
return serverError("remove_profile_cache_failed", err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compactPositiveIDs(ids []int64) []int64 {
|
|
||||||
seen := make(map[int64]struct{}, len(ids))
|
|
||||||
out := make([]int64, 0, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
if id <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, exists := seen[id]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[id] = struct{}{}
|
|
||||||
out = append(out, id)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func propsRowToView(row propsSourceRecordRow, vipLevels map[int64]int) (PropsView, bool) {
|
|
||||||
level := 0
|
|
||||||
if row.Type == propsTypeNobleVIP {
|
|
||||||
var exists bool
|
|
||||||
level, exists = vipLevels[row.ID]
|
|
||||||
if !exists || level > 3 {
|
|
||||||
return PropsView{}, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return PropsView{
|
|
||||||
ID: ID(row.ID),
|
|
||||||
Type: row.Type,
|
|
||||||
Code: row.Code,
|
|
||||||
Name: row.Name,
|
|
||||||
Cover: row.Cover,
|
|
||||||
SourceURL: row.SourceURL,
|
|
||||||
Expand: row.Expand,
|
|
||||||
Days: propsGiftDays(row.Type),
|
|
||||||
VIPLevel: level,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func propsGiftDays(propsType string) int {
|
|
||||||
if propsType == propsTypeNobleVIP {
|
|
||||||
return defaultVIPGiftDays
|
|
||||||
}
|
|
||||||
return defaultGiftDays
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizePropsType(propsType string) string {
|
|
||||||
return strings.ToUpper(strings.TrimSpace(propsType))
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBanDescription(managerID int64, reason string) string {
|
|
||||||
reason = strings.TrimSpace(reason)
|
|
||||||
if len([]rune(reason)) > 200 {
|
|
||||||
runes := []rune(reason)
|
|
||||||
reason = string(runes[:200])
|
|
||||||
}
|
|
||||||
if reason == "" {
|
|
||||||
return fmt.Sprintf("manager center ban non-recharged user, manager=%d", managerID)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("manager center ban non-recharged user, manager=%d, reason=%s", managerID, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAppErrorCode(err error, code string) bool {
|
|
||||||
var appErr *AppError
|
|
||||||
return errors.As(err, &appErr) && appErr.Code == code
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeSysOrigin(sysOrigin string) string {
|
|
||||||
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
|
||||||
if sysOrigin == "" {
|
|
||||||
return "LIKEI"
|
|
||||||
}
|
|
||||||
return sysOrigin
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipPropsViewFromConfig(row model.VipLevelConfig) PropsView {
|
|
||||||
days := row.DurationDays
|
|
||||||
if days <= 0 {
|
|
||||||
days = defaultVIPGiftDays
|
|
||||||
}
|
|
||||||
name := strings.TrimSpace(row.DisplayName)
|
|
||||||
if name == "" {
|
|
||||||
name = fmt.Sprintf("VIP %d", row.Level)
|
|
||||||
}
|
|
||||||
code := strings.TrimSpace(row.LevelCode)
|
|
||||||
if code == "" {
|
|
||||||
code = fmt.Sprintf("VIP%d", row.Level)
|
|
||||||
}
|
|
||||||
return PropsView{
|
|
||||||
ID: ID(row.ID),
|
|
||||||
Type: propsTypeNobleVIP,
|
|
||||||
Code: code,
|
|
||||||
Name: name,
|
|
||||||
Cover: row.BadgeURL,
|
|
||||||
SourceURL: row.BadgeURL,
|
|
||||||
Days: days,
|
|
||||||
VIPLevel: row.Level,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,424 +0,0 @@
|
|||||||
package managercenter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeManagerGateway struct {
|
|
||||||
recharges map[int64][]integration.UserTotalRecharge
|
|
||||||
rechargeErr error
|
|
||||||
abilities map[int64]integration.PropsNobleVIPAbility
|
|
||||||
maxAbility integration.PropsNobleVIPAbility
|
|
||||||
giveRequests []integration.GivePropsBackpackRequest
|
|
||||||
punishRequests []integration.PunishAccountRequest
|
|
||||||
switchIDs []int64
|
|
||||||
cacheCleared []int64
|
|
||||||
badgeIDs []int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) {
|
|
||||||
return integration.UserProfile{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) GetUserProfileByAccountOne(context.Context, string) (integration.UserProfile, error) {
|
|
||||||
return integration.UserProfile{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) MapUserTotalRecharge(_ context.Context, userIDs []int64) (map[int64][]integration.UserTotalRecharge, error) {
|
|
||||||
if f.rechargeErr != nil {
|
|
||||||
return nil, f.rechargeErr
|
|
||||||
}
|
|
||||||
result := make(map[int64][]integration.UserTotalRecharge, len(userIDs))
|
|
||||||
for _, userID := range userIDs {
|
|
||||||
result[userID] = f.recharges[userID]
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) GivePropsBackpack(_ context.Context, req integration.GivePropsBackpackRequest) error {
|
|
||||||
f.giveRequests = append(f.giveRequests, req)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) GetNobleVIPAbility(_ context.Context, sourceID int64) (integration.PropsNobleVIPAbility, error) {
|
|
||||||
return f.abilities[sourceID], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) GetUserMaxNobleVIPAbility(context.Context, int64) (integration.PropsNobleVIPAbility, error) {
|
|
||||||
return f.maxAbility, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) SwitchUseProps(_ context.Context, _ int64, propsID int64) error {
|
|
||||||
f.switchIDs = append(f.switchIDs, propsID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) ActivateTemporaryBadge(_ context.Context, _ int64, badgeID int64, _ int) error {
|
|
||||||
f.badgeIDs = append(f.badgeIDs, badgeID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) RemoveUserProfileCacheAll(_ context.Context, userID int64) error {
|
|
||||||
f.cacheCleared = append(f.cacheCleared, userID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeManagerGateway) PunishAccount(_ context.Context, req integration.PunishAccountRequest) error {
|
|
||||||
f.punishRequests = append(f.punishRequests, req)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListPropsRequiresAdminFreeAndFiltersVIPLevels(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
seedManager(t, service.db.(*gorm.DB), 1)
|
|
||||||
seedProps(t, service.db.(*gorm.DB),
|
|
||||||
propsSourceRecordRow{ID: 101, Type: propsTypeRide, Name: "Giftable ride", AdminFree: true},
|
|
||||||
propsSourceRecordRow{ID: 102, Type: propsTypeRide, Name: "Paid ride", AdminFree: false},
|
|
||||||
propsSourceRecordRow{ID: 201, Type: propsTypeNobleVIP, Name: "VIP 3", AdminFree: true},
|
|
||||||
propsSourceRecordRow{ID: 204, Type: propsTypeNobleVIP, Name: "VIP 4", AdminFree: true},
|
|
||||||
)
|
|
||||||
seedAbilities(t, service.db.(*gorm.DB),
|
|
||||||
propsNobleVIPAbilityRow{ID: 201, VIPLevel: 3},
|
|
||||||
propsNobleVIPAbilityRow{ID: 204, VIPLevel: 4},
|
|
||||||
)
|
|
||||||
|
|
||||||
resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1}, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListProps() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := map[int64]PropsView{}
|
|
||||||
for _, item := range resp.Records {
|
|
||||||
got[item.ID.Int64()] = item
|
|
||||||
}
|
|
||||||
if _, ok := got[101]; !ok {
|
|
||||||
t.Fatalf("records = %+v, want giftable ride", resp.Records)
|
|
||||||
}
|
|
||||||
if _, ok := got[102]; ok {
|
|
||||||
t.Fatalf("records = %+v, non-admin-free props must be hidden", resp.Records)
|
|
||||||
}
|
|
||||||
if item, ok := got[201]; !ok || item.Days != defaultVIPGiftDays || item.VIPLevel != 3 {
|
|
||||||
t.Fatalf("vip 201 = %+v, want level 3 with 3 days", item)
|
|
||||||
}
|
|
||||||
if _, ok := got[204]; ok {
|
|
||||||
t.Fatalf("records = %+v, VIP level 4 must be hidden", resp.Records)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListPropsReturnsVIPLevelConfigs(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedVIPConfigs(t, db,
|
|
||||||
model.VipLevelConfig{ID: 301, SysOrigin: "LIKEI", Level: 1, LevelCode: "VIP1", DisplayName: "VIP 1", Enabled: true, DurationDays: 30},
|
|
||||||
model.VipLevelConfig{ID: 302, SysOrigin: "LIKEI", Level: 2, LevelCode: "VIP2", DisplayName: "VIP 2", Enabled: false, DurationDays: 30},
|
|
||||||
model.VipLevelConfig{ID: 303, SysOrigin: "ATYOU", Level: 1, LevelCode: "VIP1", DisplayName: "ATYOU VIP 1", Enabled: true, DurationDays: 30},
|
|
||||||
)
|
|
||||||
|
|
||||||
resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, propsTypeNobleVIP)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListProps() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(resp.Records) != 1 {
|
|
||||||
t.Fatalf("records = %+v, want only one enabled LIKEI vip config", resp.Records)
|
|
||||||
}
|
|
||||||
got := resp.Records[0]
|
|
||||||
if got.ID.Int64() != 301 || got.Type != propsTypeNobleVIP || got.VIPLevel != 1 || got.Days != 30 {
|
|
||||||
t.Fatalf("vip props = %+v, want config 301 level 1", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetProfileToleratesRechargeQueryFailure(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 1, Account: "1"})
|
|
||||||
fake.rechargeErr = errors.New("order service unavailable")
|
|
||||||
|
|
||||||
resp, err := service.GetProfile(context.Background(), AuthUser{UserID: 1})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetProfile() error = %v", err)
|
|
||||||
}
|
|
||||||
if resp.Manager.HasRecharge || resp.Manager.CanBan {
|
|
||||||
t.Fatalf("manager = %+v, want unknown recharge state and ban disabled", resp.Manager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendPropsRejectsNonAdminFree(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"})
|
|
||||||
seedProps(t, db, propsSourceRecordRow{ID: 102, Type: propsTypeRide, Name: "Paid ride", AdminFree: false})
|
|
||||||
|
|
||||||
_, err := service.SendProps(context.Background(), AuthUser{UserID: 1}, SendPropsRequest{
|
|
||||||
AcceptUserID: ID(2),
|
|
||||||
PropsID: ID(102),
|
|
||||||
})
|
|
||||||
assertAppErrorCode(t, err, "prop_not_giftable")
|
|
||||||
if len(fake.giveRequests) != 0 {
|
|
||||||
t.Fatalf("give requests = %+v, want none", fake.giveRequests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendNobleVIPGivesAccessoriesAndSwitchesMax(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"})
|
|
||||||
seedProps(t, db, propsSourceRecordRow{ID: 201, Type: propsTypeNobleVIP, Name: "VIP 3", AdminFree: true})
|
|
||||||
seedAbilities(t, db, propsNobleVIPAbilityRow{ID: 201, VIPLevel: 3})
|
|
||||||
fake.abilities[201] = integration.PropsNobleVIPAbility{
|
|
||||||
ID: integration.Int64Value(201),
|
|
||||||
AvatarFrameID: integration.Int64Value(302),
|
|
||||||
CarID: integration.Int64Value(301),
|
|
||||||
ChatBubbleID: integration.Int64Value(303),
|
|
||||||
DataCardID: integration.Int64Value(304),
|
|
||||||
}
|
|
||||||
fake.maxAbility = fake.abilities[201]
|
|
||||||
|
|
||||||
resp, err := service.SendProps(context.Background(), AuthUser{UserID: 1}, SendPropsRequest{
|
|
||||||
AcceptUserID: ID(2),
|
|
||||||
PropsID: ID(201),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SendProps() error = %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Success || resp.Days != defaultVIPGiftDays {
|
|
||||||
t.Fatalf("resp = %+v, want success and VIP days", resp)
|
|
||||||
}
|
|
||||||
if len(fake.giveRequests) != 5 {
|
|
||||||
t.Fatalf("give requests = %+v, want main VIP plus 4 accessories", fake.giveRequests)
|
|
||||||
}
|
|
||||||
for _, req := range fake.giveRequests {
|
|
||||||
if req.AcceptUserID != 2 || req.Origin != adminFreeOrigin || req.OpUser != 1 || !req.UseProps || req.AllowGive {
|
|
||||||
t.Fatalf("give request = %+v, want admin-free grant to target", req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wantSwitch := []int64{201, 302, 304, 301, 303}
|
|
||||||
if !equalInt64Slice(fake.switchIDs, wantSwitch) {
|
|
||||||
t.Fatalf("switch ids = %+v, want %+v", fake.switchIDs, wantSwitch)
|
|
||||||
}
|
|
||||||
if !equalInt64Slice(fake.cacheCleared, []int64{2}) {
|
|
||||||
t.Fatalf("cacheCleared = %+v, want [2]", fake.cacheCleared)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendVIPUsesVIPLevelConfig(t *testing.T) {
|
|
||||||
service, _ := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db,
|
|
||||||
userBaseInfoRow{ID: 1, Account: "1", UserNickname: "manager", OriginSys: "LIKEI"},
|
|
||||||
userBaseInfoRow{ID: 2, Account: "2", OriginSys: "LIKEI"},
|
|
||||||
)
|
|
||||||
seedVIPConfigs(t, db, model.VipLevelConfig{
|
|
||||||
ID: 301,
|
|
||||||
SysOrigin: "LIKEI",
|
|
||||||
Level: 3,
|
|
||||||
LevelCode: "VIP3",
|
|
||||||
DisplayName: "VIP 3",
|
|
||||||
Enabled: true,
|
|
||||||
DurationDays: 30,
|
|
||||||
PriceGold: 3000,
|
|
||||||
})
|
|
||||||
|
|
||||||
resp, err := service.SendProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, SendPropsRequest{
|
|
||||||
AcceptUserID: ID(2),
|
|
||||||
PropsID: ID(301),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SendProps() error = %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Success || resp.Type != propsTypeNobleVIP || resp.VIPLevel != 3 || resp.Days != 30 {
|
|
||||||
t.Fatalf("resp = %+v, want vip gift success", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
var state model.UserVipState
|
|
||||||
if err := db.Where("sys_origin = ? AND user_id = ?", "LIKEI", int64(2)).First(&state).Error; err != nil {
|
|
||||||
t.Fatalf("load state: %v", err)
|
|
||||||
}
|
|
||||||
if state.Level != 3 || state.Status != vipStatusActive {
|
|
||||||
t.Fatalf("state = %+v, want vip3 active", state)
|
|
||||||
}
|
|
||||||
var orders int64
|
|
||||||
if err := db.Model(&model.VipOrderRecord{}).Where("user_id = ? AND order_type = ?", int64(2), vipOrderTypeGMGift).Count(&orders).Error; err != nil {
|
|
||||||
t.Fatalf("count orders: %v", err)
|
|
||||||
}
|
|
||||||
if orders != 1 {
|
|
||||||
t.Fatalf("orders = %d, want 1", orders)
|
|
||||||
}
|
|
||||||
var records int64
|
|
||||||
if err := db.Model(&gmVIPGiftRecordRow{}).Where("user_id = ? AND vip_level = ?", int64(2), 3).Count(&records).Error; err != nil {
|
|
||||||
t.Fatalf("count gift records: %v", err)
|
|
||||||
}
|
|
||||||
if records != 1 {
|
|
||||||
t.Fatalf("gift records = %d, want 1", records)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBanUserRejectsRechargedUser(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"})
|
|
||||||
fake.recharges[2] = []integration.UserTotalRecharge{{Amount: integration.DecimalString("1.00")}}
|
|
||||||
|
|
||||||
_, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)})
|
|
||||||
assertAppErrorCode(t, err, "user_recharged")
|
|
||||||
if len(fake.punishRequests) != 0 {
|
|
||||||
t.Fatalf("punish requests = %+v, want none", fake.punishRequests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBanUserFailsWhenRechargeQueryUnavailable(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"})
|
|
||||||
fake.rechargeErr = errors.New("order service unavailable")
|
|
||||||
|
|
||||||
_, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)})
|
|
||||||
assertAppErrorCode(t, err, "recharge_query_failed")
|
|
||||||
if len(fake.punishRequests) != 0 {
|
|
||||||
t.Fatalf("punish requests = %+v, want none", fake.punishRequests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBanUserWithoutRechargeCallsPunishment(t *testing.T) {
|
|
||||||
service, fake := newManagerCenterTestService(t)
|
|
||||||
db := service.db.(*gorm.DB)
|
|
||||||
seedManager(t, db, 1)
|
|
||||||
seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"})
|
|
||||||
|
|
||||||
resp, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{
|
|
||||||
UserID: ID(2),
|
|
||||||
Reason: "spam",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("BanUser() error = %v", err)
|
|
||||||
}
|
|
||||||
if !resp.Success || resp.AccountStatus != accountStatusArchive {
|
|
||||||
t.Fatalf("resp = %+v, want archived success", resp)
|
|
||||||
}
|
|
||||||
if len(fake.punishRequests) != 1 {
|
|
||||||
t.Fatalf("punish requests = %+v, want one", fake.punishRequests)
|
|
||||||
}
|
|
||||||
got := fake.punishRequests[0]
|
|
||||||
if got.BeOptUserID != 2 || got.OptUserID != 1 || got.Status != accountStatusArchive {
|
|
||||||
t.Fatalf("punish request = %+v, want target 2 manager 1 archive", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newManagerCenterTestService(t *testing.T) (*Service, *fakeManagerGateway) {
|
|
||||||
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(
|
|
||||||
&administratorRow{},
|
|
||||||
&userBaseInfoRow{},
|
|
||||||
&propsSourceRecordRow{},
|
|
||||||
&propsNobleVIPAbilityRow{},
|
|
||||||
&model.VipLevelConfig{},
|
|
||||||
&model.UserVipState{},
|
|
||||||
&model.VipOrderRecord{},
|
|
||||||
&gmVIPGiftRecordRow{},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatalf("auto migrate: %v", err)
|
|
||||||
}
|
|
||||||
fake := &fakeManagerGateway{
|
|
||||||
recharges: map[int64][]integration.UserTotalRecharge{},
|
|
||||||
abilities: map[int64]integration.PropsNobleVIPAbility{},
|
|
||||||
}
|
|
||||||
return NewService(db, fake), fake
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedManager(t *testing.T, db *gorm.DB, userID int64) {
|
|
||||||
t.Helper()
|
|
||||||
if err := db.Create(&administratorRow{ID: userID, UserID: userID, Status: true}).Error; err != nil {
|
|
||||||
t.Fatalf("seed manager: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedUsers(t *testing.T, db *gorm.DB, rows ...userBaseInfoRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.AccountStatus == "" {
|
|
||||||
row.AccountStatus = "ACTIVE"
|
|
||||||
}
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed user: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedProps(t *testing.T, db *gorm.DB, rows ...propsSourceRecordRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed props: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedAbilities(t *testing.T, db *gorm.DB, rows ...propsNobleVIPAbilityRow) {
|
|
||||||
t.Helper()
|
|
||||||
for _, row := range rows {
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed ability: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedVIPConfigs(t *testing.T, db *gorm.DB, rows ...model.VipLevelConfig) {
|
|
||||||
t.Helper()
|
|
||||||
now := time.Now()
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.CreateTime.IsZero() {
|
|
||||||
row.CreateTime = now
|
|
||||||
}
|
|
||||||
if row.UpdateTime.IsZero() {
|
|
||||||
row.UpdateTime = now
|
|
||||||
}
|
|
||||||
if err := db.Create(&row).Error; err != nil {
|
|
||||||
t.Fatalf("seed vip config: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertAppErrorCode(t *testing.T, err error, code string) {
|
|
||||||
t.Helper()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("error is nil, want code %s", code)
|
|
||||||
}
|
|
||||||
var appErr *AppError
|
|
||||||
if !errors.As(err, &appErr) {
|
|
||||||
t.Fatalf("error = %T %v, want AppError", err, err)
|
|
||||||
}
|
|
||||||
if appErr.Code != code {
|
|
||||||
t.Fatalf("error code = %s, want %s", appErr.Code, code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func equalInt64Slice(a, b []int64) bool {
|
|
||||||
if len(a) != len(b) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := range a {
|
|
||||||
if a[i] != b[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
package managercenter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
propsTypeNobleVIP = "NOBLE_VIP"
|
|
||||||
propsTypeRide = "RIDE"
|
|
||||||
propsTypeAvatarFrame = "AVATAR_FRAME"
|
|
||||||
propsTypeChatBubble = "CHAT_BUBBLE"
|
|
||||||
propsTypeDataCard = "DATA_CARD"
|
|
||||||
propsTypeBadge = "BADGE"
|
|
||||||
propsTypeFloatPicture = "FLOAT_PICTURE"
|
|
||||||
|
|
||||||
adminFreeOrigin = "ADMIN_FREE"
|
|
||||||
adminFreeOriginDesc = "Admin free"
|
|
||||||
vipGiftOrigin = "VIP_PURCHASE"
|
|
||||||
vipGiftOriginDesc = "VIP purchase"
|
|
||||||
|
|
||||||
defaultGiftDays = 7
|
|
||||||
defaultVIPGiftDays = 30
|
|
||||||
|
|
||||||
accountStatusArchive = "ARCHIVE"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppError = common.AppError
|
|
||||||
type AuthUser = common.AuthUser
|
|
||||||
|
|
||||||
var NewAppError = common.NewAppError
|
|
||||||
|
|
||||||
type managerDB interface {
|
|
||||||
WithContext(ctx context.Context) *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
type managerGateway interface {
|
|
||||||
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
|
||||||
GetUserProfileByAccountOne(ctx context.Context, account string) (integration.UserProfile, error)
|
|
||||||
MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]integration.UserTotalRecharge, error)
|
|
||||||
GivePropsBackpack(ctx context.Context, req integration.GivePropsBackpackRequest) error
|
|
||||||
GetNobleVIPAbility(ctx context.Context, sourceID int64) (integration.PropsNobleVIPAbility, error)
|
|
||||||
GetUserMaxNobleVIPAbility(ctx context.Context, userID int64) (integration.PropsNobleVIPAbility, error)
|
|
||||||
SwitchUseProps(ctx context.Context, userID int64, propsID int64) error
|
|
||||||
ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error
|
|
||||||
RemoveUserProfileCacheAll(ctx context.Context, userID int64) error
|
|
||||||
PunishAccount(ctx context.Context, req integration.PunishAccountRequest) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
db managerDB
|
|
||||||
java managerGateway
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(db managerDB, java managerGateway) *Service {
|
|
||||||
return &Service{db: db, java: java}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ID int64
|
|
||||||
|
|
||||||
func (id ID) Int64() int64 {
|
|
||||||
return int64(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (id ID) MarshalJSON() ([]byte, error) {
|
|
||||||
return []byte(strconv.Quote(strconv.FormatInt(int64(id), 10))), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (id *ID) UnmarshalJSON(data []byte) error {
|
|
||||||
raw := strings.TrimSpace(string(data))
|
|
||||||
if raw == "" || raw == "null" {
|
|
||||||
*id = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(raw, "\"") {
|
|
||||||
var parsed string
|
|
||||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
raw = strings.TrimSpace(parsed)
|
|
||||||
}
|
|
||||||
if raw == "" {
|
|
||||||
*id = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
parsed, err := strconv.ParseInt(raw, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*id = ID(parsed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfileResponse struct {
|
|
||||||
Manager UserView `json:"manager"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSearchResponse struct {
|
|
||||||
User UserView `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserView struct {
|
|
||||||
UserID ID `json:"userId"`
|
|
||||||
Account string `json:"account"`
|
|
||||||
UserAvatar string `json:"userAvatar"`
|
|
||||||
UserNickname string `json:"userNickname"`
|
|
||||||
CountryCode string `json:"countryCode,omitempty"`
|
|
||||||
CountryName string `json:"countryName,omitempty"`
|
|
||||||
OriginSys string `json:"originSys,omitempty"`
|
|
||||||
AccountStatus string `json:"accountStatus"`
|
|
||||||
HasRecharge bool `json:"hasRecharge"`
|
|
||||||
CanBan bool `json:"canBan"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PropsListResponse struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Records []PropsView `json:"records"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PropsView struct {
|
|
||||||
ID ID `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Code string `json:"code,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Cover string `json:"cover,omitempty"`
|
|
||||||
SourceURL string `json:"sourceUrl,omitempty"`
|
|
||||||
Expand string `json:"expand,omitempty"`
|
|
||||||
Days int `json:"days"`
|
|
||||||
VIPLevel int `json:"vipLevel,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendPropsRequest struct {
|
|
||||||
AcceptUserID ID `json:"acceptUserId"`
|
|
||||||
PropsID ID `json:"propsId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendPropsResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
AcceptUserID ID `json:"acceptUserId"`
|
|
||||||
PropsID ID `json:"propsId"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Days int `json:"days"`
|
|
||||||
VIPLevel int `json:"vipLevel,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BanUserRequest struct {
|
|
||||||
UserID ID `json:"userId"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BanUserResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
UserID ID `json:"userId"`
|
|
||||||
AccountStatus string `json:"accountStatus"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func badRequest(code, message string) error {
|
|
||||||
return NewAppError(http.StatusBadRequest, code, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unauthorized(code, message string) error {
|
|
||||||
return NewAppError(http.StatusUnauthorized, code, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func forbidden(code, message string) error {
|
|
||||||
return NewAppError(http.StatusForbidden, code, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notFound(code, message string) error {
|
|
||||||
return NewAppError(http.StatusNotFound, code, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func serviceUnavailable(code, message string) error {
|
|
||||||
return NewAppError(http.StatusServiceUnavailable, code, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func serverError(code, message string) error {
|
|
||||||
return NewAppError(http.StatusInternalServerError, code, message)
|
|
||||||
}
|
|
||||||
@ -1,425 +0,0 @@
|
|||||||
package managercenter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"chatapp3-golang/internal/integration"
|
|
||||||
"chatapp3-golang/internal/model"
|
|
||||||
"chatapp3-golang/internal/utils"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
vipStatusActive = "ACTIVE"
|
|
||||||
vipStatusSuccess = "SUCCESS"
|
|
||||||
vipOrderTypeGMGift = "GM_GIFT"
|
|
||||||
vipGiftEventPrefix = "MANAGER-VIP-GIFT-"
|
|
||||||
mysqlTimestampMaxYear = 2038
|
|
||||||
)
|
|
||||||
|
|
||||||
type gmVIPGiftRecordRow struct {
|
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
|
||||||
UserID int64 `gorm:"column:user_id"`
|
|
||||||
SysOrigin string `gorm:"column:sys_origin"`
|
|
||||||
FromLevel int `gorm:"column:from_level"`
|
|
||||||
VIPLevel int `gorm:"column:vip_level"`
|
|
||||||
LevelCode string `gorm:"column:level_code"`
|
|
||||||
DisplayName string `gorm:"column:display_name"`
|
|
||||||
DurationDays int `gorm:"column:duration_days"`
|
|
||||||
PriceGold int64 `gorm:"column:price_gold"`
|
|
||||||
StartAt time.Time `gorm:"column:start_at"`
|
|
||||||
ExpireAt time.Time `gorm:"column:expire_at"`
|
|
||||||
OrderID int64 `gorm:"column:order_id"`
|
|
||||||
EventID string `gorm:"column:event_id"`
|
|
||||||
Status string `gorm:"column:status"`
|
|
||||||
ApplicantID int64 `gorm:"column:applicant_id"`
|
|
||||||
ApplicantName string `gorm:"column:applicant_name"`
|
|
||||||
Remarks string `gorm:"column:remarks"`
|
|
||||||
ExecutedTime time.Time `gorm:"column:executed_time"`
|
|
||||||
CreateTime time.Time `gorm:"column:create_time"`
|
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
|
||||||
CreateUser int64 `gorm:"column:create_user"`
|
|
||||||
UpdateUser int64 `gorm:"column:update_user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gmVIPGiftRecordRow) TableName() string {
|
|
||||||
return "gm_vip_gift_record"
|
|
||||||
}
|
|
||||||
|
|
||||||
type vipGiftResource struct {
|
|
||||||
resourceID int64
|
|
||||||
propsType string
|
|
||||||
badge bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) listVIPProps(ctx context.Context, user AuthUser) (*PropsListResponse, error) {
|
|
||||||
sysOrigin := normalizeSysOrigin(user.SysOrigin)
|
|
||||||
var rows []model.VipLevelConfig
|
|
||||||
if err := s.db.WithContext(ctx).
|
|
||||||
Where("sys_origin = ? AND enabled = ?", sysOrigin, true).
|
|
||||||
Order("level ASC").
|
|
||||||
Find(&rows).Error; err != nil {
|
|
||||||
return nil, serverError("vip_config_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
records := make([]PropsView, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
records = append(records, vipPropsViewFromConfig(row))
|
|
||||||
}
|
|
||||||
return &PropsListResponse{
|
|
||||||
Type: propsTypeNobleVIP,
|
|
||||||
Records: records,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) sendVIPGift(ctx context.Context, manager AuthUser, target userBaseInfoRow, configID int64) (*SendPropsResponse, error) {
|
|
||||||
sysOrigin := normalizeSysOrigin(target.OriginSys)
|
|
||||||
config, found, err := s.loadVIPConfigByID(ctx, sysOrigin, configID)
|
|
||||||
if err != nil || !found {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !config.Enabled {
|
|
||||||
return nil, badRequest("vip_level_disabled", "vip level is disabled")
|
|
||||||
}
|
|
||||||
if config.Level < 1 || config.Level > 5 {
|
|
||||||
return nil, badRequest("vip_not_giftable", "vip level is invalid")
|
|
||||||
}
|
|
||||||
if config.DurationDays <= 0 {
|
|
||||||
config.DurationDays = defaultVIPGiftDays
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
orderID, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, serverError("id_generate_failed", err.Error())
|
|
||||||
}
|
|
||||||
recordID, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, serverError("id_generate_failed", err.Error())
|
|
||||||
}
|
|
||||||
stateID, err := utils.NextID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, serverError("id_generate_failed", err.Error())
|
|
||||||
}
|
|
||||||
eventID := fmt.Sprintf("%s%d", vipGiftEventPrefix, orderID)
|
|
||||||
applicantName := s.managerApplicantName(ctx, manager.UserID)
|
|
||||||
|
|
||||||
tx := s.db.WithContext(ctx).Begin()
|
|
||||||
if tx.Error != nil {
|
|
||||||
return nil, serverError("vip_gift_failed", tx.Error.Error())
|
|
||||||
}
|
|
||||||
committed := false
|
|
||||||
defer func() {
|
|
||||||
if !committed {
|
|
||||||
_ = tx.Rollback().Error
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
state, err := s.loadLockedVIPState(tx, sysOrigin, target.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, serverError("vip_state_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
currentActive := isActiveVIPState(state, now)
|
|
||||||
fromLevel := 0
|
|
||||||
if currentActive {
|
|
||||||
fromLevel = state.Level
|
|
||||||
}
|
|
||||||
if currentActive && fromLevel > config.Level {
|
|
||||||
return nil, badRequest("vip_downgrade_not_allowed", "vip downgrade is not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
startAt := now
|
|
||||||
if currentActive && !state.StartAt.IsZero() {
|
|
||||||
startAt = state.StartAt
|
|
||||||
}
|
|
||||||
expireAt := resolveVIPGiftExpireAt(state, config, currentActive, now)
|
|
||||||
|
|
||||||
order := model.VipOrderRecord{
|
|
||||||
ID: orderID,
|
|
||||||
EventID: eventID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
UserID: target.ID,
|
|
||||||
OrderType: vipOrderTypeGMGift,
|
|
||||||
FromLevel: fromLevel,
|
|
||||||
ToLevel: config.Level,
|
|
||||||
DurationDays: config.DurationDays,
|
|
||||||
OriginalPriceGold: config.PriceGold,
|
|
||||||
PaidGold: 0,
|
|
||||||
Status: vipStatusSuccess,
|
|
||||||
StartAt: startAt,
|
|
||||||
ExpireAt: expireAt,
|
|
||||||
ErrorMessage: "",
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&order).Error; err != nil {
|
|
||||||
return nil, serverError("vip_order_create_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if state == nil {
|
|
||||||
state = &model.UserVipState{
|
|
||||||
ID: stateID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
UserID: target.ID,
|
|
||||||
CreateTime: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applyVIPGiftConfigToState(state, config, startAt, expireAt, now)
|
|
||||||
if err := tx.Save(state).Error; err != nil {
|
|
||||||
return nil, serverError("vip_state_save_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
record := gmVIPGiftRecordRow{
|
|
||||||
ID: recordID,
|
|
||||||
UserID: target.ID,
|
|
||||||
SysOrigin: sysOrigin,
|
|
||||||
FromLevel: fromLevel,
|
|
||||||
VIPLevel: config.Level,
|
|
||||||
LevelCode: vipLevelCode(config),
|
|
||||||
DisplayName: vipDisplayName(config),
|
|
||||||
DurationDays: config.DurationDays,
|
|
||||||
PriceGold: config.PriceGold,
|
|
||||||
StartAt: startAt,
|
|
||||||
ExpireAt: expireAt,
|
|
||||||
OrderID: orderID,
|
|
||||||
EventID: eventID,
|
|
||||||
Status: vipStatusSuccess,
|
|
||||||
ApplicantID: manager.UserID,
|
|
||||||
ApplicantName: applicantName,
|
|
||||||
Remarks: fmt.Sprintf("manager center gift, manager=%d", manager.UserID),
|
|
||||||
ExecutedTime: now,
|
|
||||||
CreateTime: now,
|
|
||||||
UpdateTime: now,
|
|
||||||
CreateUser: manager.UserID,
|
|
||||||
UpdateUser: manager.UserID,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&record).Error; err != nil {
|
|
||||||
return nil, serverError("vip_gift_record_create_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
return nil, serverError("vip_gift_failed", err.Error())
|
|
||||||
}
|
|
||||||
committed = true
|
|
||||||
|
|
||||||
if err := s.grantVIPGiftResources(ctx, target.ID, manager.UserID, config, expireAt, now); err != nil {
|
|
||||||
return nil, serverError("vip_resource_grant_failed", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SendPropsResponse{
|
|
||||||
Success: true,
|
|
||||||
AcceptUserID: ID(target.ID),
|
|
||||||
PropsID: ID(config.ID),
|
|
||||||
Type: propsTypeNobleVIP,
|
|
||||||
Days: config.DurationDays,
|
|
||||||
VIPLevel: config.Level,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadVIPConfigByID(ctx context.Context, sysOrigin string, configID int64) (model.VipLevelConfig, bool, error) {
|
|
||||||
var row model.VipLevelConfig
|
|
||||||
err := s.db.WithContext(ctx).
|
|
||||||
Where("id = ? AND sys_origin = ?", configID, sysOrigin).
|
|
||||||
First(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return model.VipLevelConfig{}, false, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return model.VipLevelConfig{}, false, serverError("vip_config_query_failed", err.Error())
|
|
||||||
}
|
|
||||||
return row, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) loadLockedVIPState(tx *gorm.DB, sysOrigin string, userID int64) (*model.UserVipState, error) {
|
|
||||||
var row model.UserVipState
|
|
||||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
Where("sys_origin = ? AND user_id = ?", sysOrigin, userID).
|
|
||||||
First(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &row, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isActiveVIPState(state *model.UserVipState, now time.Time) bool {
|
|
||||||
return state != nil && state.Status == vipStatusActive && state.Level > 0 && state.ExpireAt.After(now)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveVIPGiftExpireAt(state *model.UserVipState, config model.VipLevelConfig, currentActive bool, now time.Time) time.Time {
|
|
||||||
durationDays := config.DurationDays
|
|
||||||
if durationDays <= 0 {
|
|
||||||
durationDays = defaultVIPGiftDays
|
|
||||||
}
|
|
||||||
plusDuration := now.AddDate(0, 0, durationDays)
|
|
||||||
if !currentActive {
|
|
||||||
return plusDuration
|
|
||||||
}
|
|
||||||
if state.Level == config.Level {
|
|
||||||
return maxTime(state.ExpireAt, now).AddDate(0, 0, durationDays)
|
|
||||||
}
|
|
||||||
return maxTime(state.ExpireAt, plusDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyVIPGiftConfigToState(state *model.UserVipState, config model.VipLevelConfig, startAt time.Time, expireAt time.Time, now time.Time) {
|
|
||||||
state.Level = config.Level
|
|
||||||
state.LevelCode = vipLevelCode(config)
|
|
||||||
state.DisplayName = vipDisplayName(config)
|
|
||||||
state.Status = vipStatusActive
|
|
||||||
state.StartAt = startAt
|
|
||||||
state.ExpireAt = expireAt
|
|
||||||
state.BadgeResourceID = config.BadgeResourceID
|
|
||||||
state.BadgeName = config.BadgeName
|
|
||||||
state.BadgeURL = config.BadgeURL
|
|
||||||
state.AvatarFrameResourceID = config.AvatarFrameResourceID
|
|
||||||
state.AvatarFrameName = config.AvatarFrameName
|
|
||||||
state.AvatarFrameURL = config.AvatarFrameURL
|
|
||||||
state.EntryEffectResourceID = config.EntryEffectResourceID
|
|
||||||
state.EntryEffectName = config.EntryEffectName
|
|
||||||
state.EntryEffectURL = config.EntryEffectURL
|
|
||||||
state.ChatBubbleResourceID = config.ChatBubbleResourceID
|
|
||||||
state.ChatBubbleName = config.ChatBubbleName
|
|
||||||
state.ChatBubbleURL = config.ChatBubbleURL
|
|
||||||
state.FloatPictureResourceID = config.FloatPictureResourceID
|
|
||||||
state.FloatPictureName = config.FloatPictureName
|
|
||||||
state.FloatPictureURL = config.FloatPictureURL
|
|
||||||
state.UpdateTime = now
|
|
||||||
if state.CreateTime.IsZero() {
|
|
||||||
state.CreateTime = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) grantVIPGiftResources(ctx context.Context, userID int64, managerID int64, config model.VipLevelConfig, expireAt time.Time, now time.Time) error {
|
|
||||||
resources := vipGiftResourcesFromConfig(config)
|
|
||||||
if len(resources) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
days := config.DurationDays
|
|
||||||
if days <= 0 {
|
|
||||||
days = defaultVIPGiftDays
|
|
||||||
}
|
|
||||||
for _, resource := range resources {
|
|
||||||
if resource.badge {
|
|
||||||
if err := s.java.ActivateTemporaryBadge(ctx, userID, resource.resourceID, days); err != nil {
|
|
||||||
return fmt.Errorf("grant vip badge %d: %w", resource.resourceID, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := s.java.GivePropsBackpack(ctx, integration.GivePropsBackpackRequest{
|
|
||||||
AcceptUserID: userID,
|
|
||||||
PropsID: resource.resourceID,
|
|
||||||
Type: resource.propsType,
|
|
||||||
Origin: vipGiftOrigin,
|
|
||||||
OriginDesc: vipGiftOriginDesc,
|
|
||||||
Days: days,
|
|
||||||
UseProps: true,
|
|
||||||
AllowGive: false,
|
|
||||||
OpUser: managerID,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("grant vip resource %s:%d: %w", resource.propsType, resource.resourceID, err)
|
|
||||||
}
|
|
||||||
if err := s.java.SwitchUseProps(ctx, userID, resource.resourceID); err != nil {
|
|
||||||
return fmt.Errorf("switch vip resource %s:%d: %w", resource.propsType, resource.resourceID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.alignVIPGiftResourceExpireAt(ctx, userID, resource, expireAt, now); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil {
|
|
||||||
return fmt.Errorf("remove vip user profile cache: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) alignVIPGiftResourceExpireAt(ctx context.Context, userID int64, resource vipGiftResource, expireAt time.Time, now time.Time) error {
|
|
||||||
expireAt = capMySQLTimestamp(expireAt)
|
|
||||||
var result *gorm.DB
|
|
||||||
if resource.badge {
|
|
||||||
result = s.db.WithContext(ctx).Exec(
|
|
||||||
"UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id = ?",
|
|
||||||
"TEMPORARY", expireAt, true, now, userID, resource.resourceID,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
result = s.db.WithContext(ctx).Exec(
|
|
||||||
"UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id = ? AND type = ?",
|
|
||||||
expireAt, true, false, now, userID, resource.resourceID, resource.propsType,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return fmt.Errorf("vip resource backpack row not found after grant: %d", resource.resourceID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipGiftResourcesFromConfig(config model.VipLevelConfig) []vipGiftResource {
|
|
||||||
resources := []vipGiftResource{
|
|
||||||
{resourceID: config.BadgeResourceID, propsType: propsTypeBadge, badge: true},
|
|
||||||
{resourceID: config.AvatarFrameResourceID, propsType: propsTypeAvatarFrame},
|
|
||||||
{resourceID: config.EntryEffectResourceID, propsType: propsTypeRide},
|
|
||||||
{resourceID: config.ChatBubbleResourceID, propsType: propsTypeChatBubble},
|
|
||||||
{resourceID: config.FloatPictureResourceID, propsType: propsTypeFloatPicture},
|
|
||||||
}
|
|
||||||
out := make([]vipGiftResource, 0, len(resources))
|
|
||||||
for _, resource := range resources {
|
|
||||||
if resource.resourceID > 0 {
|
|
||||||
out = append(out, resource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) managerApplicantName(ctx context.Context, managerID int64) string {
|
|
||||||
row, err := s.loadUserRowByID(ctx, managerID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Sprintf("%d", managerID)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(row.UserNickname) != "" {
|
|
||||||
return row.UserNickname
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(row.Account) != "" {
|
|
||||||
return row.Account
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d", managerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipLevelCode(config model.VipLevelConfig) string {
|
|
||||||
code := strings.TrimSpace(config.LevelCode)
|
|
||||||
if code == "" {
|
|
||||||
return fmt.Sprintf("VIP%d", config.Level)
|
|
||||||
}
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
func vipDisplayName(config model.VipLevelConfig) string {
|
|
||||||
name := strings.TrimSpace(config.DisplayName)
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Sprintf("VIP %d", config.Level)
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
func maxTime(a time.Time, b time.Time) time.Time {
|
|
||||||
if a.After(b) {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func capMySQLTimestamp(value time.Time) time.Time {
|
|
||||||
maxValue := time.Date(mysqlTimestampMaxYear, time.January, 19, 0, 0, 0, 0, time.Local)
|
|
||||||
if value.After(maxValue) {
|
|
||||||
return maxValue
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS `gm_vip_gift_record` (
|
|
||||||
`id` bigint NOT NULL COMMENT '记录ID',
|
|
||||||
`user_id` bigint NOT NULL COMMENT 'APP用户ID',
|
|
||||||
`sys_origin` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '系统来源',
|
|
||||||
`from_level` int NOT NULL DEFAULT '0' COMMENT '赠送前VIP等级',
|
|
||||||
`vip_level` int NOT NULL COMMENT '赠送VIP等级',
|
|
||||||
`level_code` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '等级编码',
|
|
||||||
`display_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '显示名称',
|
|
||||||
`duration_days` int NOT NULL DEFAULT '30' COMMENT '赠送天数',
|
|
||||||
`price_gold` bigint NOT NULL DEFAULT '0' COMMENT '对应购买价格,金币',
|
|
||||||
`start_at` datetime(3) DEFAULT NULL COMMENT '权益开始时间',
|
|
||||||
`expire_at` datetime(3) DEFAULT NULL COMMENT '权益到期时间',
|
|
||||||
`order_id` bigint NOT NULL COMMENT '关联VIP订单ID',
|
|
||||||
`event_id` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '赠送幂等事件ID',
|
|
||||||
`status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'SUCCESS' COMMENT '状态',
|
|
||||||
`applicant_id` bigint NOT NULL COMMENT '操作人ID',
|
|
||||||
`applicant_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作人名称',
|
|
||||||
`remarks` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
|
|
||||||
`executed_time` datetime(3) DEFAULT NULL COMMENT '执行时间',
|
|
||||||
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
|
||||||
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
|
||||||
`create_user` bigint DEFAULT NULL COMMENT '创建用户',
|
|
||||||
`update_user` bigint DEFAULT NULL COMMENT '更新用户',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uk_gm_vip_gift_event` (`event_id`),
|
|
||||||
KEY `idx_gm_vip_gift_user` (`user_id`),
|
|
||||||
KEY `idx_gm_vip_gift_applicant` (`applicant_id`),
|
|
||||||
KEY `idx_gm_vip_gift_time` (`create_time`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='GM VIP赠送记录';
|
|
||||||
Loading…
x
Reference in New Issue
Block a user