Compare commits
115 Commits
codex/gola
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48242f0ade | ||
|
|
b3f5b570ca | ||
|
|
d52263da52 | ||
|
|
3c9f10f21a | ||
|
|
fb43e3e9fa | ||
|
|
5296562391 | ||
|
|
ca72ed3f97 | ||
|
|
a1114fbbf5 | ||
|
|
523cfe961f | ||
|
|
90ab87393e | ||
|
|
714c1c5c68 | ||
|
|
4bb8613570 | ||
|
|
53c6f361f9 | ||
|
|
e370c3ca32 | ||
|
|
995268fd70 | ||
|
|
e086a518bc | ||
|
|
4219117721 | ||
|
|
a71dd0fbae | ||
|
|
81b1ff88bf | ||
|
|
4e3c9d01aa | ||
|
|
cbe388b071 | ||
|
|
8147b5981f | ||
|
|
81cdba4096 | ||
|
|
ecb2fcba4e | ||
|
|
a9ee549e37 | ||
|
|
c7c40fce77 | ||
|
|
b53313f62d | ||
|
|
e738fbc44a | ||
|
|
c09433bf29 | ||
|
|
675f45bf44 | ||
|
|
f1b781e3be | ||
|
|
ee5b4f5eaf | ||
|
|
72c0d1d633 | ||
|
|
5b92fef113 | ||
|
|
9aecdf34a3 | ||
|
|
4cf4eba5ff | ||
|
|
adcc722284 | ||
|
|
3ec7cc6a62 | ||
|
|
3639cad484 | ||
|
|
0a11372d96 | ||
|
|
e4911e7732 | ||
|
|
747d4700df | ||
|
|
8e8878a038 | ||
|
|
a02e22ac74 | ||
|
|
74063d0908 | ||
|
|
810dc0345b | ||
|
|
4926d5dfd2 | ||
|
|
c4d4f03ddf | ||
|
|
78bddf406d | ||
|
|
165863c279 | ||
|
|
886bc8998b | ||
|
|
6195544950 | ||
|
|
8c53a64416 | ||
|
|
c0d58525fe | ||
|
|
6d90ddeee0 | ||
|
|
dbd073b97f | ||
|
|
43c44692ec | ||
|
|
106033f6a9 | ||
|
|
31811d5cf4 | ||
|
|
c1a0049b1d | ||
|
|
8ace95f78f | ||
|
|
0d8e6f599b | ||
|
|
6a0ae4793b | ||
|
|
463d7374af | ||
|
|
ffd3eeddfd | ||
|
|
2c5c247f12 | ||
|
|
39744caf2e | ||
|
|
bbf50367e2 | ||
|
|
1abc344d05 | ||
|
|
fc214f9991 | ||
|
|
de605d9ccd | ||
|
|
019ffa9a4a | ||
|
|
41c1d326ee | ||
|
|
31433b0a1b | ||
|
|
51b8b52c57 | ||
|
|
dd9020a542 | ||
|
|
2bba927458 | ||
|
|
171ba88a2e | ||
|
|
47a7725b33 | ||
|
|
ab8f711169 | ||
|
|
946c3789b1 | ||
|
|
df7446fb8c | ||
|
|
d39840a6ed | ||
|
|
dd2f4ea6c5 | ||
|
|
fbe9e922f8 | ||
|
|
c6ba03c407 | ||
|
|
c4549b16af | ||
|
|
72b66e26b0 | ||
|
|
25aab0d6a2 | ||
|
|
d0007b3319 | ||
|
|
ba8ed01b99 | ||
|
|
157307a474 | ||
|
|
dc6f15369c | ||
|
|
d76a11f62f | ||
|
|
3c8358b6ca | ||
|
|
a2ef52c8d9 | ||
|
|
f8a8d4d65f | ||
|
|
bf07c84dd0 | ||
|
|
65b0558e03 | ||
|
|
c16c6a7bd9 | ||
|
|
a9ab2b8589 | ||
|
|
24525bb4f8 | ||
|
|
4fc2ddebf1 | ||
|
|
7c1ecb804e | ||
|
|
362069e2d4 | ||
|
|
b7c0225dc3 | ||
|
|
86c8e0c9ec | ||
|
|
c4f75b4846 | ||
|
|
42b3a4b595 | ||
|
|
fa564ef985 | ||
|
|
8256fdf7e6 | ||
|
|
5e81478c8b | ||
|
|
1d2bc6eafd | ||
|
|
3e2719f682 | ||
|
|
8f8ae40947 |
@ -29,6 +29,7 @@ internal/utils
|
|||||||
### 1. `cmd/api`
|
### 1. `cmd/api`
|
||||||
- 只做启动和依赖装配。
|
- 只做启动和依赖装配。
|
||||||
- 不承担业务逻辑。
|
- 不承担业务逻辑。
|
||||||
|
- 当前线上只部署 `golang` API 进程,所以这里会同时启动 `registerreward` 的 Redis Stream consumer 和 `weekstar` 的 RocketMQ consumer,确保注册成功与周星送礼事件能被消费。
|
||||||
- 不启动任何 `weekstar` 结算 goroutine。
|
- 不启动任何 `weekstar` 结算 goroutine。
|
||||||
|
|
||||||
### 2. `cmd/consumer`
|
### 2. `cmd/consumer`
|
||||||
@ -36,6 +37,7 @@ internal/utils
|
|||||||
- 当前承接:
|
- 当前承接:
|
||||||
- `registerreward` 的 Redis Stream 消费
|
- `registerreward` 的 Redis Stream 消费
|
||||||
- `weekstar` 的 RocketMQ 消费
|
- `weekstar` 的 RocketMQ 消费
|
||||||
|
- 如果后续单独部署 consumer,`registerreward` 使用同一个 Redis consumer group,重复启动不会重复发奖。
|
||||||
|
|
||||||
### 3. `internal/bootstrap`
|
### 3. `internal/bootstrap`
|
||||||
- 统一完成配置加载、存储初始化、依赖探活、Java 网关装配。
|
- 统一完成配置加载、存储初始化、依赖探活、Java 网关装配。
|
||||||
|
|||||||
133
cmd/api/main.go
133
cmd/api/main.go
@ -1,16 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chatapp3-golang/internal/bootstrap"
|
|
||||||
"chatapp3-golang/internal/router"
|
|
||||||
"chatapp3-golang/internal/service/baishun"
|
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
|
||||||
"chatapp3-golang/internal/service/invite"
|
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
|
||||||
"chatapp3-golang/internal/service/signinreward"
|
|
||||||
"chatapp3-golang/internal/service/weekstar"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
@ -19,6 +9,40 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
_ "time/tzdata"
|
_ "time/tzdata"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/bootstrap"
|
||||||
|
"chatapp3-golang/internal/router"
|
||||||
|
"chatapp3-golang/internal/service/apppopup"
|
||||||
|
"chatapp3-golang/internal/service/baishun"
|
||||||
|
"chatapp3-golang/internal/service/binancerecharge"
|
||||||
|
"chatapp3-golang/internal/service/cprelationbroadcast"
|
||||||
|
"chatapp3-golang/internal/service/errorlog"
|
||||||
|
"chatapp3-golang/internal/service/firstrechargereward"
|
||||||
|
"chatapp3-golang/internal/service/gameking"
|
||||||
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
|
"chatapp3-golang/internal/service/hotgame"
|
||||||
|
"chatapp3-golang/internal/service/invite"
|
||||||
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
|
"chatapp3-golang/internal/service/managercenter"
|
||||||
|
"chatapp3-golang/internal/service/propsstore"
|
||||||
|
"chatapp3-golang/internal/service/rechargeagency"
|
||||||
|
"chatapp3-golang/internal/service/rechargereward"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
|
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||||
|
"chatapp3-golang/internal/service/signinreward"
|
||||||
|
"chatapp3-golang/internal/service/smashegg"
|
||||||
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
|
"chatapp3-golang/internal/service/userbadge"
|
||||||
|
"chatapp3-golang/internal/service/vip"
|
||||||
|
"chatapp3-golang/internal/service/voiceroomredpacket"
|
||||||
|
"chatapp3-golang/internal/service/voiceroomrocket"
|
||||||
|
"chatapp3-golang/internal/service/weekstar"
|
||||||
|
"chatapp3-golang/internal/service/wheel"
|
||||||
|
"chatapp3-golang/internal/service/yumigiftchallenge"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -30,25 +54,113 @@ func main() {
|
|||||||
log.Fatalf("bootstrap api failed: %v", err)
|
log.Fatalf("bootstrap api failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appPopupService := apppopup.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
||||||
|
errorLogService := errorlog.NewService(app.Repository.DB)
|
||||||
inviteService := invite.NewInviteService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
inviteService := invite.NewInviteService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
baishunService := baishun.NewBaishunService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
baishunService := baishun.NewBaishunService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
|
binanceRechargeService := binancerecharge.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
lingxianService := lingxian.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
||||||
|
hotgameService := hotgame.NewService(app.Config, app.Repository.DB, app.Repository.Redis)
|
||||||
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
|
gameOpenService := gameopen.NewGameOpenService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
luckyGiftService := luckygift.NewLuckyGiftService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet)
|
luckyGiftService := luckygift.NewLuckyGiftService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet)
|
||||||
|
rechargeAgencyService := rechargeagency.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
|
rechargeRewardService := rechargereward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
firstRechargeRewardService := firstrechargereward.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)
|
||||||
|
gameKingService := gameking.NewService(app.Repository.DB, &app.Gateways)
|
||||||
|
userBadgeService := userbadge.NewService(app.Repository.DB)
|
||||||
|
vipService := vip.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)
|
||||||
|
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
baishunService.SetHighWinBroadcaster(regionIMGroupService)
|
||||||
|
luckyGiftService.SetHighWinBroadcaster(regionIMGroupService)
|
||||||
|
cpRelationBroadcastService := cprelationbroadcast.NewService(app.Config, app.Repository.DB, regionIMGroupService)
|
||||||
|
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
||||||
|
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
||||||
|
voiceRoomRocketService := voiceroomrocket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
|
voiceRoomRocketService.SetRegionBroadcaster(regionIMGroupService)
|
||||||
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)
|
||||||
|
yumiGiftChallengeService := yumigiftchallenge.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)
|
||||||
|
managerCenterService := managercenter.NewService(app.Repository.DB, &app.Gateways)
|
||||||
|
propsStoreService := propsstore.NewService(app.Repository.DB, app.Repository.Redis)
|
||||||
|
|
||||||
|
baishunService.SetTaskEventReporter(taskCenterService)
|
||||||
|
gameOpenService.SetTaskEventReporter(taskCenterService)
|
||||||
|
weekStarService.SetTaskEventReporter(taskCenterService)
|
||||||
|
// GAME_CONSUME_GOLD 在任务中心完成统一校验后扇出;HTTP 与 MQ 重投由活动账本幂等。
|
||||||
|
taskCenterService.SetGameConsumeSink(gameKingService)
|
||||||
|
|
||||||
|
workerCtx, workerCancel := context.WithCancel(context.Background())
|
||||||
|
defer workerCancel()
|
||||||
|
if err := registerRewardService.Start(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start register reward consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := weekStarService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start week star message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := yumiGiftChallengeService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start yumi gift challenge message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := taskCenterService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start task center message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := firstRechargeRewardService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start first recharge reward message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := cpRelationBroadcastService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start cp relation broadcast message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := voiceRoomRocketService.Start(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start voice room rocket workers failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := taskCenterService.StartArchiveWorker(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start task center archive worker failed: %v", err)
|
||||||
|
}
|
||||||
|
voiceRoomRedPacketService.StartRefundWorker(workerCtx)
|
||||||
|
|
||||||
gameProviders := gameprovider.NewRegistry(
|
gameProviders := gameprovider.NewRegistry(
|
||||||
baishun.NewAppProvider(baishunService),
|
baishun.NewAppProvider(baishunService),
|
||||||
|
lingxian.NewAppProvider(lingxianService),
|
||||||
|
hotgame.NewAppProvider(hotgameService),
|
||||||
)
|
)
|
||||||
|
gameVisibility := gameprovider.NewVisibilityPolicy(&app.Gateways)
|
||||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||||
|
AppPopup: appPopupService,
|
||||||
|
ErrorLog: errorLogService,
|
||||||
Invite: inviteService,
|
Invite: inviteService,
|
||||||
Baishun: baishunService,
|
Baishun: baishunService,
|
||||||
|
BinanceRecharge: binanceRechargeService,
|
||||||
|
FirstRechargeReward: firstRechargeRewardService,
|
||||||
|
Lingxian: lingxianService,
|
||||||
GameOpen: gameOpenService,
|
GameOpen: gameOpenService,
|
||||||
|
GameKing: gameKingService,
|
||||||
GameProviders: gameProviders,
|
GameProviders: gameProviders,
|
||||||
|
GameVisibility: gameVisibility,
|
||||||
LuckyGift: luckyGiftService,
|
LuckyGift: luckyGiftService,
|
||||||
|
ManagerCenter: managerCenterService,
|
||||||
|
PropsStore: propsStoreService,
|
||||||
|
HostCenter: hostCenterService,
|
||||||
|
Hotgame: hotgameService,
|
||||||
|
RechargeAgency: rechargeAgencyService,
|
||||||
|
RechargeReward: rechargeRewardService,
|
||||||
|
RegionIMGroup: regionIMGroupService,
|
||||||
RegisterReward: registerRewardService,
|
RegisterReward: registerRewardService,
|
||||||
|
RoomTurnoverReward: roomTurnoverRewardService,
|
||||||
SignInReward: signInRewardService,
|
SignInReward: signInRewardService,
|
||||||
|
SmashEgg: smashEggService,
|
||||||
|
TaskCenter: taskCenterService,
|
||||||
|
UserBadge: userBadgeService,
|
||||||
|
VIP: vipService,
|
||||||
|
Wheel: wheelService,
|
||||||
|
VoiceRoomRedPacket: voiceRoomRedPacketService,
|
||||||
|
VoiceRoomRocket: voiceRoomRocketService,
|
||||||
WeekStar: weekStarService,
|
WeekStar: weekStarService,
|
||||||
|
YumiGiftChallenge: yumiGiftChallengeService,
|
||||||
})
|
})
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: app.Config.HTTP.ListenAddr,
|
Addr: app.Config.HTTP.ListenAddr,
|
||||||
@ -76,6 +188,7 @@ func main() {
|
|||||||
return
|
return
|
||||||
case <-shutdownSignal.Done():
|
case <-shutdownSignal.Done():
|
||||||
log.Printf("shutdown signal received, draining http traffic")
|
log.Printf("shutdown signal received, draining http traffic")
|
||||||
|
workerCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), app.Config.HTTP.ShutdownTimeout)
|
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), app.Config.HTTP.ShutdownTimeout)
|
||||||
|
|||||||
@ -2,8 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"chatapp3-golang/internal/bootstrap"
|
"chatapp3-golang/internal/bootstrap"
|
||||||
|
"chatapp3-golang/internal/service/cprelationbroadcast"
|
||||||
|
"chatapp3-golang/internal/service/firstrechargereward"
|
||||||
|
"chatapp3-golang/internal/service/gameking"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
|
"chatapp3-golang/internal/service/voiceroomredpacket"
|
||||||
|
"chatapp3-golang/internal/service/voiceroomrocket"
|
||||||
"chatapp3-golang/internal/service/weekstar"
|
"chatapp3-golang/internal/service/weekstar"
|
||||||
|
"chatapp3-golang/internal/service/yumigiftchallenge"
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@ -22,6 +30,18 @@ func main() {
|
|||||||
|
|
||||||
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
registerRewardService := registerreward.NewRegisterRewardService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
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)
|
||||||
|
yumiGiftChallengeService := yumigiftchallenge.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
taskCenterService := taskcenter.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
gameKingService := gameking.NewService(app.Repository.DB, &app.Gateways)
|
||||||
|
// consumer 是生产 MQ 的主要入口,必须与 API 直报共用同一个活动幂等账本。
|
||||||
|
taskCenterService.SetGameConsumeSink(gameKingService)
|
||||||
|
firstRechargeRewardService := firstrechargereward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
cpRelationBroadcastService := cprelationbroadcast.NewService(app.Config, app.Repository.DB, regionIMGroupService)
|
||||||
|
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
||||||
|
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
||||||
|
voiceRoomRocketService := voiceroomrocket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
|
voiceRoomRocketService.SetRegionBroadcaster(regionIMGroupService)
|
||||||
|
|
||||||
workerCtx, workerCancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
workerCtx, workerCancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer workerCancel()
|
defer workerCancel()
|
||||||
@ -32,6 +52,25 @@ func main() {
|
|||||||
if err := weekStarService.StartMessageConsumer(workerCtx); err != nil {
|
if err := weekStarService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
log.Fatalf("start week star message consumer failed: %v", err)
|
log.Fatalf("start week star message consumer failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := yumiGiftChallengeService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start yumi gift challenge message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := taskCenterService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start task center message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := firstRechargeRewardService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start first recharge reward message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := cpRelationBroadcastService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start cp relation broadcast message consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := voiceRoomRocketService.Start(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start voice room rocket workers failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := taskCenterService.StartArchiveWorker(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start task center archive worker failed: %v", err)
|
||||||
|
}
|
||||||
|
voiceRoomRedPacketService.StartRefundWorker(workerCtx)
|
||||||
|
|
||||||
log.Printf("chatapp3-golang consumer started")
|
log.Printf("chatapp3-golang consumer started")
|
||||||
<-workerCtx.Done()
|
<-workerCtx.Done()
|
||||||
|
|||||||
303
docs/Yumi游戏王活动接口.md
Normal file
303
docs/Yumi游戏王活动接口.md
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
# Yumi 游戏王活动接口
|
||||||
|
|
||||||
|
本文对应 `chatapp3-golang` 的 Yumi Game King 实现。业务只接受 `LIKEI` 租户的权威游戏金币消耗事件,不提供事件白名单输入框,也不会统计通用钱包支出。
|
||||||
|
|
||||||
|
## 1. 通用约定
|
||||||
|
|
||||||
|
- H5 外部网关地址:`/go/app/h5/game-king/yumi/*`;Go 服务内路由不带 `/go`。
|
||||||
|
- 后台外部网关地址:`/go/resident-activity/game-king/*`;Go 服务内路由不带 `/go`。
|
||||||
|
- H5 和后台均传 `Authorization`。H5 支持 `Bearer <token>`;后台沿用 Console 完整 Authorization。
|
||||||
|
- 内部任务传 `X-Internal-Token: <HTTP_INTERNAL_CALLBACK_SECRET>`。
|
||||||
|
- 时间字段均为毫秒时间戳;`timeZone` 使用 IANA 时区,默认 `Asia/Riyadh`。
|
||||||
|
- 所有业务 ID 在 JSON 响应中均为字符串,避免 JavaScript 丢失 Snowflake 精度。请求中的 ID 可传字符串或数字,建议统一传字符串。
|
||||||
|
- 成功响应的数据同时位于 `data` 和 `body`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": true,
|
||||||
|
"errorCode": 0,
|
||||||
|
"errorMsg": "success",
|
||||||
|
"code": "success",
|
||||||
|
"message": "success",
|
||||||
|
"data": {},
|
||||||
|
"body": {},
|
||||||
|
"time": 1784300000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 错误时 HTTP 状态码与 `errorCode` 一致,稳定业务码位于 `code` / `errorCodeName`。
|
||||||
|
|
||||||
|
## 2. H5 接口
|
||||||
|
|
||||||
|
### 2.1 活动详情
|
||||||
|
|
||||||
|
`GET /go/app/h5/game-king/yumi/detail?activityId={activityId}`
|
||||||
|
|
||||||
|
`activityId` 可不传;不传时按登录用户 `sysOrigin=LIKEI` 解析当前可见活动。返回活动、固定七个奖项、六个排名奖励档、服务器时间和活动状态。
|
||||||
|
|
||||||
|
关键返回字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activity": {
|
||||||
|
"id": "190000000000000001",
|
||||||
|
"activityCode": "YUMI_GAME_KING_S1",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"startTime": 1784300000000,
|
||||||
|
"endTime": 1784900000000,
|
||||||
|
"coinPerDraw": 1000,
|
||||||
|
"eventTypes": ["GAME_CONSUME_GOLD"],
|
||||||
|
"rankingType": "TYCOON",
|
||||||
|
"rankingPeriod": "OVERALL",
|
||||||
|
"settlementStatus": "NOT_STARTED"
|
||||||
|
},
|
||||||
|
"prizes": [],
|
||||||
|
"rankRewards": [],
|
||||||
|
"supportedRankingTypes": ["TYCOON"],
|
||||||
|
"supportedRankingPeriods": ["DAILY", "OVERALL"],
|
||||||
|
"activityStatus": "UPCOMING",
|
||||||
|
"serverTime": 1784299900000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`activityStatus`:`UPCOMING`、`ACTIVE`、`ENDED`、`DISABLED`。
|
||||||
|
|
||||||
|
### 2.2 我的进度
|
||||||
|
|
||||||
|
`GET /go/app/h5/game-king/yumi/me?activityId={activityId}`
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- `totalConsumed`:活动时间窗内累计游戏消耗金币。
|
||||||
|
- `earnedChances`:`floor(totalConsumed / coinPerDraw)`。
|
||||||
|
- `usedChances` / `availableChances`:已用和可用抽奖次数。
|
||||||
|
- `nextChanceRemaining`:距离下一次机会仍需消耗的金币。
|
||||||
|
- `rank`:总榜名次;无消费时为 `null`。
|
||||||
|
|
||||||
|
### 2.3 抽奖
|
||||||
|
|
||||||
|
`POST /go/app/h5/game-king/yumi/draw`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activityId": "190000000000000001",
|
||||||
|
"requestId": "9af73ee9-65ca-4cf5-b23c-f4c222c3ee6e"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `activityId` 和 `requestId` 均必填;`requestId` 最长 64 字符。客户端一次点击生成一个稳定值;超时重试必须复用原值。
|
||||||
|
- 幂等范围是 `activityId + userId + requestId`。已经提交的结果即使活动随后结束,也会原样返回,且不会再次扣次数或发奖。
|
||||||
|
- 权重仅表示相对概率;库存 `-1` 为无限,`0` 为耗尽,正数为剩余库存。有限库存使用条件更新防止超发。
|
||||||
|
- 返回 `record`、`prize`、`remainingChances`、`idempotentReplay`。
|
||||||
|
|
||||||
|
### 2.4 排行榜
|
||||||
|
|
||||||
|
`GET /go/app/h5/game-king/yumi/ranking?activityId={activityId}&rankingType=TYCOON&period=OVERALL&limit=30`
|
||||||
|
|
||||||
|
- `rankingType`:正式口径仅 `TYCOON`。旧 H5 请求 `VICTORIOUS` 时返回空榜,避免兼容页面报错。
|
||||||
|
- `period`:`DAILY` 或 `OVERALL`;配置页固定保存 `OVERALL`,H5 仍可切日榜。
|
||||||
|
- `limit` 默认 30,最大 100。
|
||||||
|
- 排序固定为:消费金币降序、达到当前消费值的事件时间升序、用户 ID 升序。
|
||||||
|
- `DAILY` 按活动时区自然日统计;活动结束结算只使用 `OVERALL` Top30。
|
||||||
|
- 用户资料通过 Java 批量接口补齐;资料服务异常时榜单仍返回 `userId` 和分数。
|
||||||
|
|
||||||
|
### 2.5 我的抽奖记录
|
||||||
|
|
||||||
|
`GET /go/app/h5/game-king/yumi/draw-records?activityId={activityId}&limit=20`
|
||||||
|
|
||||||
|
`limit` 最大 100。返回奖品快照及 `PENDING / PROCESSING / SUCCESS / FAILED / UNKNOWN` 发放状态。
|
||||||
|
|
||||||
|
## 3. 后台管理接口
|
||||||
|
|
||||||
|
所有接口要求菜单别名 `ResidentGameKingActivity`。写接口还要求对应按钮权限:
|
||||||
|
|
||||||
|
| 权限别名 | 接口用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `resident-activity:yumi-game-king:edit` | 保存活动、奖项、排名奖励 |
|
||||||
|
| `resident-activity:yumi-game-king:enable` | 启停活动 |
|
||||||
|
| `resident-activity:yumi-game-king:settle` | 人工触发到期结算 |
|
||||||
|
| `resident-activity:yumi-game-king:reconcile` | FAILED 重试、UNKNOWN 核账 |
|
||||||
|
| `resident-activity:yumi-game-king:backfill` | 权威事件补数 |
|
||||||
|
|
||||||
|
### 3.1 活动配置
|
||||||
|
|
||||||
|
| 方法 | 地址 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET | `/go/resident-activity/game-king/list` | 活动列表 |
|
||||||
|
| GET | `/go/resident-activity/game-king/detail/{id}` | 完整详情 |
|
||||||
|
| POST | `/go/resident-activity/game-king/save` | 新增或修改 |
|
||||||
|
| PUT | `/go/resident-activity/game-king/enable/{id}?enabled=true` | 启停;布尔值只接受 `true/false/1/0` |
|
||||||
|
| GET | `/go/resident-activity/game-king/prizes/{id}` | 七个奖项 |
|
||||||
|
| PUT | `/go/resident-activity/game-king/prizes/{id}` | 保存七个奖项 |
|
||||||
|
| GET | `/go/resident-activity/game-king/rank-rewards/{id}` | 六个排名奖励档 |
|
||||||
|
| PUT | `/go/resident-activity/game-king/rank-rewards/{id}` | 保存六个排名奖励档 |
|
||||||
|
|
||||||
|
`POST /save` 示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"activityCode": "YUMI_GAME_KING_S1",
|
||||||
|
"activityName": "Yumi Game King",
|
||||||
|
"activityDesc": "",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"timeZone": "Asia/Riyadh",
|
||||||
|
"startTime": 1784300000000,
|
||||||
|
"endTime": 1784900000000,
|
||||||
|
"settlementDelayMinutes": 30,
|
||||||
|
"coinPerDraw": 1000,
|
||||||
|
"rankingType": "TYCOON",
|
||||||
|
"rankingPeriod": "OVERALL",
|
||||||
|
"enabled": false,
|
||||||
|
"prizes": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"prizeName": "Prize 1",
|
||||||
|
"prizeImage": "https://example.com/prize-1.png",
|
||||||
|
"weight": 100,
|
||||||
|
"stock": -1,
|
||||||
|
"resourceGroupId": "190000000000000101",
|
||||||
|
"enabled": true,
|
||||||
|
"sortOrder": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rankRewards": [
|
||||||
|
{
|
||||||
|
"id": "0",
|
||||||
|
"startRank": 1,
|
||||||
|
"endRank": 1,
|
||||||
|
"resourceGroupId": "190000000000000201",
|
||||||
|
"rewardName": "Top 1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例数组为节省篇幅只展示一项,真实启用配置必须满足:
|
||||||
|
|
||||||
|
- 恰好 7 个奖项,全部启用,`sortOrder` 完整覆盖 1..7 且不重复;权重必须大于 0。
|
||||||
|
- 恰好 6 个排名档:`1`、`2`、`3`、`4-7`、`8-10`、`11-30`。
|
||||||
|
- `sysOrigin` 固定 `LIKEI`;`rankingType` 固定 `TYCOON`;配置 `rankingPeriod` 固定 `OVERALL`。
|
||||||
|
- 首次启用时 `startTime` 必须严格晚于当前时间;同租户启用活动时间窗不能重叠。
|
||||||
|
- 计数窗口严格为 `[startTime, endTime)`。活动开始后锁定时间、口径、奖项、排名奖励和启停状态,只允许修改名称、说明等展示信息。
|
||||||
|
- `settlementDelayMinutes` 默认 30,可配置 0..1440。
|
||||||
|
- 每次保存/启用及每次真实发奖前都会重新校验奖励资源组:必须存在、已上架、明确属于 `LIKEI` 且非空,并比对冻结的完整奖励项快照。
|
||||||
|
|
||||||
|
单独保存奖项请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activityId": "190000000000000001",
|
||||||
|
"prizes": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
单独保存排名奖励请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activityId": "190000000000000001",
|
||||||
|
"rankRewards": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
路径 ID 与 body `activityId` 必须一致。
|
||||||
|
|
||||||
|
### 3.2 抽奖发放管理
|
||||||
|
|
||||||
|
| 方法 | 地址 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET | `/go/resident-activity/game-king/draw-records/{id}?deliveryStatus=&limit=100` | 查看抽奖和发放明细 |
|
||||||
|
| POST | `/go/resident-activity/game-king/draw/retry/{recordId}` | 只重试 `FAILED` |
|
||||||
|
| POST | `/go/resident-activity/game-king/delivery-item/resolve/{itemId}?delivered=true` | 人工核销 `UNKNOWN` |
|
||||||
|
|
||||||
|
`delivered` 只接受 `true/false/1/0`:
|
||||||
|
|
||||||
|
- `true`:已通过 `trackId/businessNo` 确认下游到账,只记为成功,不再发送。
|
||||||
|
- `false`:已确认下游未到账,先转为 `FAILED`,再执行一次显式重试。
|
||||||
|
- 非法或缺失值返回 400,绝不默认为 `false`。
|
||||||
|
|
||||||
|
### 3.3 排名结算
|
||||||
|
|
||||||
|
| 方法 | 地址 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET | `/go/resident-activity/game-king/settlement-records/{id}` | 查看冻结名次与发放状态 |
|
||||||
|
| POST | `/go/resident-activity/game-king/settlement/{id}` | 到结算时间后人工冻结并派发一小批 |
|
||||||
|
| POST | `/go/resident-activity/game-king/settlement/retry/{recordId}` | 只重试 `FAILED` |
|
||||||
|
|
||||||
|
结算只冻结一次 `OVERALL` Top30。记录和发奖任务在同一事务生成;人工接口每次最多派发该活动 4 个 `PENDING`,其余由内部定时任务继续。单项硬超时 20 秒,因此最坏外部调用约 80 秒,并为数据库处理留出余量。
|
||||||
|
|
||||||
|
### 3.4 事件补数
|
||||||
|
|
||||||
|
`POST /go/resident-activity/game-king/backfill/{id}?lastId=0&limit=200`
|
||||||
|
|
||||||
|
- 只读取 `task_center_event_archive_outbox` 的 `LIKEI + GAME_CONSUME_GOLD + 活动时间窗` 数据。
|
||||||
|
- 在任何归档扫描前都会检查迁移 058 的四列在线索引;索引未完成、不可见或列序不符时返回 `503 backfill_index_required`,不会退化为全表扫描。
|
||||||
|
- 仅允许对已启用且结算尚未冻结的活动补数;活动结束后的结算等待期仍可补,进入结算后返回 409,游标不会静默推进。
|
||||||
|
- 调用方只能传不透明游标 `lastId` 和 `limit`,不能传 eventType 或钱包口径;`limit` 最大 500。
|
||||||
|
- 返回 `eventType`、`scanned`、`accepted`、`nextLastId`、`finished`。未完成时用返回的 `nextLastId` 继续。
|
||||||
|
- 若结算恰好在一页补数中途冻结,接口返回 409 且不返回新的游标;调用方保留原 `lastId`,已成功写入的前序事件会由活动账本幂等跳过。
|
||||||
|
- 活动账本以 `activityId + eventId` 幂等,实时事件和补数重复不会重复累计。
|
||||||
|
|
||||||
|
## 4. 游戏消耗事件接入
|
||||||
|
|
||||||
|
唯一统计事件是任务中心的 `GAME_CONSUME_GOLD`。事件经过任务中心统一校验后、在任务中心自身业务去重之前同步扇出给 Game King;Game King 使用独立账本保证幂等。
|
||||||
|
|
||||||
|
已接入事件 ID 口径:
|
||||||
|
|
||||||
|
- Java 游戏:`GAME_CONSUME:<provider>:<orderId>`,已覆盖 `YOMI`、`LINGXIAN`、`HOTGAME`、`BAISHUN_LEGACY`、`LUDO`、`TURNTABLE`。
|
||||||
|
- Go GameOpen:`GAME_OPEN_CONSUME:<orderId>`。
|
||||||
|
- Go HotGame:`GAME_HOTGAME_CONSUME:<orderId>`。
|
||||||
|
- Go Baishun:`BAISHUN_GAME_CONSUME:<sysOrigin>:<orderId>`。
|
||||||
|
|
||||||
|
RocketMQ 默认配置:
|
||||||
|
|
||||||
|
- topic:`RC_DEFAULT_APP_ORDINARY`
|
||||||
|
- tag:`task_center_event`
|
||||||
|
- consumer group:`task-center-progress`
|
||||||
|
- 开关:`CHATAPP_TASK_CENTER_MQ_ENABLED`(兼容 `TASK_CENTER_EVENT_MQ_ENABLED`)
|
||||||
|
|
||||||
|
活动累计使用事件的 `occurredAt` 判断 `[startTime,endTime)`,因此活动开始前的历史游戏消耗不会计入。只接受正数 `deltaValue`;退款、胜利收入和非游戏钱包消耗不计入。
|
||||||
|
|
||||||
|
## 5. 发奖和异常状态
|
||||||
|
|
||||||
|
发奖以“奖励资源组”为一个下游调用单元,使用 Java `SendActivityReward`,`origin=GAME_KING_AWARD`,`trackId=deliveryItem.id`。后台明细同时返回稳定 `trackId`、`businessNo`、用户 ID、资源组 ID 和冻结奖励项。
|
||||||
|
|
||||||
|
- `PENDING`:本地事务已提交,尚未领取。
|
||||||
|
- `PROCESSING`:已领取,正在校验/调用下游。
|
||||||
|
- `SUCCESS`:Java 明确成功。
|
||||||
|
- `FAILED`:在发奖请求发出前就已明确失败,例如资源组不存在、下架、跨租户或与冻结快照不一致;只允许运营显式重试。
|
||||||
|
- `UNKNOWN`:发奖请求已尝试但结果不能证明,或 `PROCESSING` 超过 10 分钟;禁止自动重试,必须先核账再 resolve。
|
||||||
|
|
||||||
|
一旦活动启用,奖励组内的实际奖励项内容必须保持与冻结快照一致。若运营修改了组内容,发放会在调用 Java 前进入 `FAILED`;应先恢复原快照或完成明确处置,再重试。
|
||||||
|
|
||||||
|
## 6. 内部结算任务
|
||||||
|
|
||||||
|
`POST /internal/game-king/settle-due`
|
||||||
|
|
||||||
|
Header:`X-Internal-Token`。
|
||||||
|
|
||||||
|
本 Go 服务不启动 ticker。由 `chatapp-cron` 周期调用:
|
||||||
|
|
||||||
|
- 每次最多修复 20 个过期 `PROCESSING`,并扫描、冻结 10 个到期活动。
|
||||||
|
- 每次最多派发全局 4 个 `PENDING`;单项硬超时 20 秒,调用方超时应配置为至少 120 秒。
|
||||||
|
- 只自动恢复 `PENDING`。`FAILED` / `UNKNOWN` 永不自动重试;超过 10 分钟的 `PROCESSING` 只转为 `UNKNOWN`。
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scanned": 2,
|
||||||
|
"settled": 2,
|
||||||
|
"failedIds": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 数据库迁移与性能
|
||||||
|
|
||||||
|
迁移文件:`migrations/058_yumi_game_king.sql`。
|
||||||
|
|
||||||
|
- 新建活动、七奖项、六档奖励、幂等消费账本、总榜/日榜聚合、抽奖、结算和发放表。
|
||||||
|
- 排行读取物化聚合并使用复合排序索引,不对消费流水做运行时 `GROUP BY`。
|
||||||
|
- 补数给 `task_center_event_archive_outbox` 增加 `(sys_origin,event_type,occurred_at,id)` 复合索引,以范围 + keyset 扫描,避免全表排序。
|
||||||
|
- 迁移不执行历史 `UPDATE/DELETE/backfill`。新增复合索引仍属于线上 DDL,执行前应按目标 MySQL 版本、表数据量和 DDL 策略确认锁影响,并在低峰发布。
|
||||||
62
docs/yumi_gift_challenge_api.md
Normal file
62
docs/yumi_gift_challenge_api.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Yumi 礼物挑战接口
|
||||||
|
|
||||||
|
Yumi 的 Java `sysOrigin` 固定为 `LIKEI`。网关外部地址在以下路径前加 `/go`;Go 服务内部路径不加。所有 Snowflake ID 都使用十进制字符串,积分/任务金额使用精确两位小数字符串(如 `"100.50"`)。统一响应业务数据位于 `body`/`data`。
|
||||||
|
|
||||||
|
## H5 用户接口
|
||||||
|
|
||||||
|
鉴权:`Authorization: Bearer <app token>`。
|
||||||
|
|
||||||
|
| 方法 | 地址 | 参数 | 主要返回 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/app/h5/gift-challenge/yumi/detail` | query `activityId` 可选 | `activity`、固定三任务 `tasks`、日/总榜奖励 `rankRewards` |
|
||||||
|
| POST | `/app/h5/gift-challenge/yumi/enter` | JSON `{ "activityId": "..." }` | 当日 `totalScore`、`dailyScore`、名次和任务状态;幂等完成进入页面任务 |
|
||||||
|
| GET | `/app/h5/gift-challenge/yumi/ranking` | query `activityId` 可选、`periodType=DAILY\|OVERALL`、DAILY 可传 `statDate=yyyy-MM-dd`、`limit`(默认/最大 30) | 原始 Top N 过滤神秘人后的 `entries`、本人真实 `my`、`settled` |
|
||||||
|
| POST | `/app/h5/gift-challenge/yumi/tasks/:taskCode/claim` | JSON `{ "activityId": "...", "requestId": "..." }` | 更新后的当日用户状态;`requestId` 最长 128 |
|
||||||
|
|
||||||
|
只有 `GOLD`、非背包、非 `LUCKY_GIFT`/`MAGIC` 的普通礼物计分。积分按 `giftCandy × quantity × 去重有效收礼人数` 精确计算,支持最多两位小数。
|
||||||
|
|
||||||
|
## Webconsole 接口
|
||||||
|
|
||||||
|
鉴权:Console `Authorization`。所有 GET 还要求页面菜单 `ResidentYumiGiftChallenge`;写操作分别校验按钮权限。
|
||||||
|
|
||||||
|
基础地址:`/resident-activity/yumi-gift-challenge`。
|
||||||
|
|
||||||
|
| 方法 | 地址 | 参数/返回 | 权限 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/list` | query `sysOrigin=LIKEI&page&size`,返回 `{records,total,current,size}` | 页面菜单 |
|
||||||
|
| GET | `/detail/:id` | 完整活动、任务、奖励 | 页面菜单 |
|
||||||
|
| GET | `/tasks/:id` | 返回任务数组 | 页面菜单 |
|
||||||
|
| GET | `/rank-rewards/:id` | 返回日榜/总榜奖励数组 | 页面菜单 |
|
||||||
|
| GET | `/ranking/:id` | query `periodType,statDate,limit` | 页面菜单 |
|
||||||
|
| GET | `/settlement-records/:id` | query `periodType,statDate,deliveryStatus,page,size` | 页面菜单 |
|
||||||
|
| POST | `/save` | 完整活动 JSON;新活动固定 3 个启用任务,榜奖区间可配置 | `resident-activity:yumi-gift-challenge:edit` |
|
||||||
|
| PUT | `/enable/:id` | query `enabled=true\|false` | `resident-activity:yumi-gift-challenge:enable` |
|
||||||
|
| PUT | `/tasks/:id` | JSON `{ "activityId":"...", "tasks":[...] }` | `...:edit` |
|
||||||
|
| PUT | `/rank-rewards/:id` | JSON `{ "activityId":"...", "rankRewards":[...] }` | `...:edit` |
|
||||||
|
| POST | `/settlement/:id` | JSON `{ "periodType":"DAILY\|OVERALL", "statDate":"yyyy-MM-dd" }`;OVERALL 不传日期 | `...:settle` |
|
||||||
|
| POST | `/delivery-item/resolve/:itemId` | JSON `{ "delivered": true\|false }`;仅人工核账 UNKNOWN | `...:reconcile` |
|
||||||
|
|
||||||
|
新建并启用活动、或重新启用已停用活动时,`startTime`、`endTime` 均须晚于服务端当前时间;同时校验奖励组已上架且属于 LIKEI,并冻结完整奖励项。活动即使已经进入时间窗,只要 `overallSettlementStatus=NOT_STARTED`,仍可通过 `/save`、`/tasks/:id`、`/rank-rewards/:id` 修改完整配置。只有总榜结算进入 `PROCESSING/COMPLETED/RECONCILIATION_REQUIRED` 后,才仅允许修改活动名称和说明。
|
||||||
|
|
||||||
|
进行中编辑按提交时点向后生效,不回算已经入账的积分。已物化的用户日任务、已冻结日榜、榜奖 parent 和 delivery item 保留原目标及奖励;尚未物化的用户/日期和未冻结榜奖使用新配置。旧任务或榜奖引用的资源组快照始终保留;同一已引用资源组不会刷新,若要更换奖励内容应选择新的资源组。修改时间窗或时区后,事件事务会在活动共享锁内重新读取窗口和时区;已产生数据的历史日期门闩保留,其余 `NOT_STARTED` 门闩按新配置重建。
|
||||||
|
日榜、总榜结算延迟均允许 `0-1440` 分钟,且 `overallSettlementDelayMinutes` 必须大于或等于 `dailySettlementDelayMinutes`,保证最终日榜不会晚于总榜关门。
|
||||||
|
|
||||||
|
## chatapp-cron 触发
|
||||||
|
|
||||||
|
`POST /internal/yumi-gift-challenge/settle-due?batch=20`
|
||||||
|
|
||||||
|
- 鉴权头:`X-Internal-Token`,值与 `CHATAPP_HTTP_INTERNAL_CALLBACK_SECRET` 一致。
|
||||||
|
- Go 服务或 cron 任一侧未配置该密钥时均 fail-closed,不发送/不执行结算请求。
|
||||||
|
- 单轮最多处理 `batch` 个到期 `NOT_STARTED/PROCESSING` 日榜或总榜,并各恢复最多 `batch` 个任务/榜奖 owner;`batch` 默认 20、最大 100。
|
||||||
|
- 返回 `periodsScanned`、`periodsFrozen`、`taskOwnersScanned`、`settlementOwnersScanned`、`ownersProcessed`、`failedKeys`。
|
||||||
|
- HTTP 200 仍可能表示部分 owner 已成功、部分待恢复;cron 会把非空 `failedKeys` 提升为失败告警,下轮依靠数据库状态幂等续跑。
|
||||||
|
- 该端点可重复、可多实例并发调用;周期门闩、业务唯一键和逐奖励项 trackId 保证幂等。过租期 `PROCESSING` 会保守转为 `UNKNOWN`,不会自动盲目补发。
|
||||||
|
|
||||||
|
建议 chatapp-cron 每 10 秒至 1 分钟触发一次,单次 HTTP 超时至少覆盖每项 30 秒的下游上限和配置的 batch。
|
||||||
|
|
||||||
|
## RocketMQ 与 IM
|
||||||
|
|
||||||
|
- 独立消费组:`YUMI_GIFT_CHALLENGE_GIFT`。
|
||||||
|
- 默认 topic/tag:`RC_DEFAULT_APP_ORDINARY` / `give_gift_v3`。
|
||||||
|
- 环境变量前缀:`CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_*`;缺少 endpoint/密钥时消费者不会伪造凭证启动。
|
||||||
|
- 本活动没有新增 IM 消息、房间广播或客户端 IM 协议;H5 通过上述 HTTP 接口刷新任务和排行榜。
|
||||||
5
go.mod
5
go.mod
@ -18,6 +18,7 @@ require (
|
|||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/clbanning/mxj v1.8.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dchest/siphash v1.2.3 // indirect
|
github.com/dchest/siphash v1.2.3 // indirect
|
||||||
@ -32,6 +33,7 @@ require (
|
|||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/google/go-querystring v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
@ -42,12 +44,15 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
||||||
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
github.com/tencentyun/cos-go-sdk-v5 v0.7.73 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/valyala/fastrand v1.1.0 // indirect
|
github.com/valyala/fastrand v1.1.0 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@ -49,6 +49,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
|||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||||
|
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
@ -94,6 +96,7 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
|
|||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@ -132,6 +135,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||||
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
@ -140,6 +145,7 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@ -179,11 +185,15 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
|
||||||
|
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
|
||||||
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
||||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
@ -201,6 +211,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
|||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@ -214,6 +225,11 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||||
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
|
||||||
|
github.com/tencentyun/cos-go-sdk-v5 v0.7.73 h1:uFfgp1A7cQaAGR6QP9DsIkoEQ67b8ewj5r1RV6XB540=
|
||||||
|
github.com/tencentyun/cos-go-sdk-v5 v0.7.73/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg=
|
||||||
|
github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20250515025012-e0eec8a5d123/go.mod h1:b18KQa4IxHbxeseW1GcZox53d7J0z39VNONTxvvlkXw=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
|||||||
@ -6,4 +6,6 @@ type AuthUser struct {
|
|||||||
SysOrigin string
|
SysOrigin string
|
||||||
Token string
|
Token string
|
||||||
Authorization string
|
Authorization string
|
||||||
|
RegionID string
|
||||||
|
RegionCode string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cpRelationBroadcastMQConsumerGroup = "cp-relation-broadcast"
|
||||||
|
cpRelationBroadcastMQTopic = "RC_DEFAULT_APP_ORDINARY"
|
||||||
|
cpRelationBroadcastMQTag = "cp_relation_broadcast"
|
||||||
|
)
|
||||||
|
|
||||||
// Config 是按运行层和业务模块分段后的应用配置。
|
// Config 是按运行层和业务模块分段后的应用配置。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HTTP HTTPConfig
|
HTTP HTTPConfig
|
||||||
@ -15,10 +21,22 @@ type Config struct {
|
|||||||
Java JavaConfig
|
Java JavaConfig
|
||||||
Worker WorkerConfig
|
Worker WorkerConfig
|
||||||
Invite InviteConfig
|
Invite InviteConfig
|
||||||
|
RechargeReward RechargeRewardConfig
|
||||||
|
FirstRechargeReward FirstRechargeRewardConfig
|
||||||
|
TaskCenter TaskCenterConfig
|
||||||
WeekStar WeekStarConfig
|
WeekStar WeekStarConfig
|
||||||
|
YumiGiftChallenge YumiGiftChallengeConfig
|
||||||
|
RoomTurnoverReward RoomTurnoverRewardConfig
|
||||||
|
VoiceRoomRedPacket VoiceRoomRedPacketConfig
|
||||||
|
VoiceRoomRocket VoiceRoomRocketConfig
|
||||||
|
CPRelationBroadcast CPRelationBroadcastConfig
|
||||||
|
TencentIM TencentIMConfig
|
||||||
Baishun BaishunConfig
|
Baishun BaishunConfig
|
||||||
|
Lingxian LingxianConfig
|
||||||
|
Hotgame HotgameConfig
|
||||||
GameOpen GameOpenConfig
|
GameOpen GameOpenConfig
|
||||||
LuckyGift LuckyGiftConfig
|
LuckyGift LuckyGiftConfig
|
||||||
|
BinanceRecharge BinanceRechargeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig 保存 HTTP 服务运行配置。
|
// HTTPConfig 保存 HTTP 服务运行配置。
|
||||||
@ -46,6 +64,7 @@ type JavaConfig struct {
|
|||||||
BridgeBaseURL string
|
BridgeBaseURL string
|
||||||
ExternalBaseURL string
|
ExternalBaseURL string
|
||||||
OtherBaseURL string
|
OtherBaseURL string
|
||||||
|
OrderBaseURL string
|
||||||
WalletBaseURL string
|
WalletBaseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +86,77 @@ type RegisterRewardWorkerConfig struct {
|
|||||||
// InviteConfig 保存邀请活动相关配置。
|
// InviteConfig 保存邀请活动相关配置。
|
||||||
type InviteConfig struct {
|
type InviteConfig struct {
|
||||||
PublicBaseURL string
|
PublicBaseURL string
|
||||||
|
PublicH5Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RechargeRewardConfig 保存充值奖励活动配置。
|
||||||
|
type RechargeRewardConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
Timezone string
|
||||||
|
StorageTimezone string
|
||||||
|
StartDate string
|
||||||
|
CoinSellerGoldPerUSD int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstRechargeRewardConfig 保存首冲奖励模块配置。
|
||||||
|
type FirstRechargeRewardConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
MQ FirstRechargeRewardMQConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstRechargeRewardMQConfig 保存首冲奖励订阅通用充值成功 MQ 的消费配置。
|
||||||
|
type FirstRechargeRewardMQConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Endpoint string
|
||||||
|
Namespace string
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
SecurityToken string
|
||||||
|
ConsumerGroup string
|
||||||
|
Topic string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCenterConfig 保存任务中心模块配置。
|
||||||
|
type TaskCenterConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
Timezone string
|
||||||
|
InternalEventTTL time.Duration
|
||||||
|
MQ TaskCenterMQConfig
|
||||||
|
Archive TaskCenterArchiveConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCenterMQConfig 保存任务中心事件 MQ 消费配置。
|
||||||
|
type TaskCenterMQConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Endpoint string
|
||||||
|
Namespace string
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
SecurityToken string
|
||||||
|
ConsumerGroup string
|
||||||
|
Topic string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCenterArchiveConfig 保存任务中心原始事件 COS 归档配置。
|
||||||
|
type TaskCenterArchiveConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Bucket string
|
||||||
|
Region string
|
||||||
|
SecretID string
|
||||||
|
SecretKey string
|
||||||
|
Prefix string
|
||||||
|
BatchSize int
|
||||||
|
FlushSeconds int
|
||||||
|
Workers int
|
||||||
|
GC TaskCenterArchiveGCConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCenterArchiveGCConfig 只保存清理总开关。cutoff 和批量必须由一次管理预检冻结,
|
||||||
|
// 不能由多节点各自读取易漂移的 NOW()/环境变量后直接删数据。
|
||||||
|
type TaskCenterArchiveGCConfig struct {
|
||||||
|
Enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WeekStarConfig 保存周榜模块配置。
|
// WeekStarConfig 保存周榜模块配置。
|
||||||
@ -90,9 +180,95 @@ type WeekStarRocketMQConfig struct {
|
|||||||
Tag string
|
Tag string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// YumiGiftChallengeConfig 保存 Yumi 礼物挑战的独立送礼消费者和发奖租期配置。
|
||||||
|
type YumiGiftChallengeConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
DeliveryProcessingLeaseSeconds int
|
||||||
|
RocketMQ WeekStarRocketMQConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomTurnoverRewardConfig 保存房间流水奖励模块配置。
|
||||||
|
type RoomTurnoverRewardConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
Timezone string
|
||||||
|
CycleDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoiceRoomRedPacketConfig 保存语音房红包 Go 侧运行配置。
|
||||||
|
type VoiceRoomRedPacketConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
Timezone string
|
||||||
|
PresenceTTLSeconds int
|
||||||
|
RefundScanIntervalSeconds int
|
||||||
|
RefundBatchSize int
|
||||||
|
NotifyStreamKey string
|
||||||
|
ExpireZSetKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoiceRoomRocketConfig 保存语音房火箭 Go 侧运行配置。
|
||||||
|
type VoiceRoomRocketConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
Timezone string
|
||||||
|
RewardRetryStreamKey string
|
||||||
|
InRoomRewardDueZSetKey string
|
||||||
|
RoomLockTTLSeconds int
|
||||||
|
BroadcastStreamKey string
|
||||||
|
BroadcastStreamMaxLen int64
|
||||||
|
InRoomRewardScanIntervalSeconds int
|
||||||
|
InRoomRewardBatchSize int
|
||||||
|
InRoomRewardUserLimit int
|
||||||
|
RewardGrantScanIntervalSeconds int
|
||||||
|
RewardGrantBatchSize int
|
||||||
|
MQ VoiceRoomRocketMQConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoiceRoomRocketMQConfig 保存语音房火箭送礼 MQ 消费配置。
|
||||||
|
type VoiceRoomRocketMQConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Endpoint string
|
||||||
|
Namespace string
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
SecurityToken string
|
||||||
|
ConsumerGroup string
|
||||||
|
Topic string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPRelationBroadcastConfig 保存 CP/兄弟/姐妹关系区域飘屏配置。
|
||||||
|
type CPRelationBroadcastConfig struct {
|
||||||
|
DefaultSysOrigin string
|
||||||
|
MQ CPRelationBroadcastMQConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPRelationBroadcastMQConfig 保存 CP 关系成功 MQ 消费配置。
|
||||||
|
type CPRelationBroadcastMQConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
Endpoint string
|
||||||
|
Namespace string
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
SecurityToken string
|
||||||
|
ConsumerGroup string
|
||||||
|
Topic string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TencentIMConfig 保存腾讯 IM REST API 配置。
|
||||||
|
type TencentIMConfig struct {
|
||||||
|
AppID int64
|
||||||
|
Key string
|
||||||
|
Identifier string
|
||||||
|
BaseEndpoint string
|
||||||
|
UserSigExpireSeconds int64
|
||||||
|
RequestTimeoutSeconds int
|
||||||
|
}
|
||||||
|
|
||||||
// BaishunConfig 保存百顺模块配置。
|
// BaishunConfig 保存百顺模块配置。
|
||||||
type BaishunConfig struct {
|
type BaishunConfig struct {
|
||||||
PlatformBaseURL string
|
PlatformBaseURL string
|
||||||
|
GameListURL string
|
||||||
|
LuckyGiftURL string
|
||||||
AppID int64
|
AppID int64
|
||||||
AppName string
|
AppName string
|
||||||
AppChannel string
|
AppChannel string
|
||||||
@ -102,6 +278,18 @@ type BaishunConfig struct {
|
|||||||
SSTokenTTLSeconds int
|
SSTokenTTLSeconds int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LingxianConfig 保存灵仙模块配置。
|
||||||
|
type LingxianConfig struct {
|
||||||
|
GameListURL string
|
||||||
|
AppKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HotgameConfig 保存热游模块的环境兜底配置。
|
||||||
|
type HotgameConfig struct {
|
||||||
|
AppKey string
|
||||||
|
CallbackBaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
// GameOpenConfig 保存统一三方游戏回调配置。
|
// GameOpenConfig 保存统一三方游戏回调配置。
|
||||||
type GameOpenConfig struct {
|
type GameOpenConfig struct {
|
||||||
PublicBaseURL string
|
PublicBaseURL string
|
||||||
@ -116,9 +304,15 @@ type LuckyGiftConfig struct {
|
|||||||
Enabled bool
|
Enabled bool
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
RewardStreamKey string
|
RewardStreamKey string
|
||||||
|
RewardStreamMaxLen int64
|
||||||
Providers []LuckyGiftProviderConfig
|
Providers []LuckyGiftProviderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BinanceRechargeConfig 保存币安自动充值验证的运行配置。
|
||||||
|
type BinanceRechargeConfig struct {
|
||||||
|
BaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
type LuckyGiftProviderConfig struct {
|
type LuckyGiftProviderConfig struct {
|
||||||
StandardID int64 `json:"standardId"`
|
StandardID int64 `json:"standardId"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
@ -130,6 +324,33 @@ type LuckyGiftProviderConfig struct {
|
|||||||
func Load() Config {
|
func Load() Config {
|
||||||
javaAppBaseURL := getEnvAny([]string{"CHATAPP_JAVA_APP_BASE_URL", "INVITE_JAVA_APP_BASE_URL"}, "http://127.0.0.1:2400")
|
javaAppBaseURL := getEnvAny([]string{"CHATAPP_JAVA_APP_BASE_URL", "INVITE_JAVA_APP_BASE_URL"}, "http://127.0.0.1:2400")
|
||||||
consoleBaseURL := getEnvAny([]string{"CHATAPP_JAVA_CONSOLE_BASE_URL", "JAVA_CONSOLE_BASE_URL"}, "http://127.0.0.1:2700/console")
|
consoleBaseURL := getEnvAny([]string{"CHATAPP_JAVA_CONSOLE_BASE_URL", "JAVA_CONSOLE_BASE_URL"}, "http://127.0.0.1:2700/console")
|
||||||
|
weekStarRocketMQEndpoint := getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT", "WEEK_STAR_ROCKETMQ_ENDPOINT"}, "")
|
||||||
|
weekStarRocketMQAccessKey := getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY", "WEEK_STAR_ROCKETMQ_ACCESS_KEY"}, "")
|
||||||
|
weekStarRocketMQAccessSecret := getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_SECRET", "WEEK_STAR_ROCKETMQ_ACCESS_SECRET"}, "")
|
||||||
|
weekStarRocketMQEnabledDefault := strings.TrimSpace(weekStarRocketMQEndpoint) != "" &&
|
||||||
|
strings.TrimSpace(weekStarRocketMQAccessKey) != "" &&
|
||||||
|
strings.TrimSpace(weekStarRocketMQAccessSecret) != ""
|
||||||
|
yumiGiftRocketMQEndpoint := getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENDPOINT", "LIKEI_ROCKETMQ_ENDPOINT", "CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT"}, "")
|
||||||
|
yumiGiftRocketMQAccessKey := getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ACCESS_KEY", "LIKEI_ROCKETMQ_ACCESS_KEY", "CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY"}, "")
|
||||||
|
yumiGiftRocketMQAccessSecret := getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ACCESS_SECRET", "LIKEI_ROCKETMQ_SECRET_KEY", "CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_SECRET"}, "")
|
||||||
|
yumiGiftRocketMQEnabledDefault := strings.TrimSpace(yumiGiftRocketMQEndpoint) != "" &&
|
||||||
|
strings.TrimSpace(yumiGiftRocketMQAccessKey) != "" &&
|
||||||
|
strings.TrimSpace(yumiGiftRocketMQAccessSecret) != ""
|
||||||
|
taskCenterMQEndpoint := getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_ENDPOINT", "TASK_CENTER_EVENT_MQ_ENDPOINT", "LIKEI_ROCKETMQ_ENDPOINT"}, "")
|
||||||
|
taskCenterMQAccessKey := getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_ACCESS_KEY", "TASK_CENTER_EVENT_MQ_ACCESS_KEY", "LIKEI_ROCKETMQ_ACCESS_KEY"}, "")
|
||||||
|
taskCenterMQAccessSecret := getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_ACCESS_SECRET", "TASK_CENTER_EVENT_MQ_SECRET_KEY", "LIKEI_ROCKETMQ_SECRET_KEY"}, "")
|
||||||
|
firstRechargeRewardMQEndpoint := getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_ENDPOINT", "FIRST_RECHARGE_REWARD_MQ_ENDPOINT", "CHATAPP_RECHARGE_SUCCESS_MQ_ENDPOINT", "RECHARGE_SUCCESS_MQ_ENDPOINT", "LIKEI_ROCKETMQ_ENDPOINT"}, "")
|
||||||
|
firstRechargeRewardMQAccessKey := getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_ACCESS_KEY", "FIRST_RECHARGE_REWARD_MQ_ACCESS_KEY", "CHATAPP_RECHARGE_SUCCESS_MQ_ACCESS_KEY", "RECHARGE_SUCCESS_MQ_ACCESS_KEY", "LIKEI_ROCKETMQ_ACCESS_KEY"}, "")
|
||||||
|
firstRechargeRewardMQAccessSecret := getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_ACCESS_SECRET", "FIRST_RECHARGE_REWARD_MQ_SECRET_KEY", "CHATAPP_RECHARGE_SUCCESS_MQ_ACCESS_SECRET", "RECHARGE_SUCCESS_MQ_SECRET_KEY", "LIKEI_ROCKETMQ_SECRET_KEY"}, "")
|
||||||
|
voiceRoomRocketMQEndpoint := getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_ENDPOINT", "LIKEI_ROCKETMQ_ENDPOINT"}, "")
|
||||||
|
voiceRoomRocketMQAccessKey := getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_ACCESS_KEY", "LIKEI_ROCKETMQ_ACCESS_KEY"}, "")
|
||||||
|
voiceRoomRocketMQAccessSecret := getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_ACCESS_SECRET", "LIKEI_ROCKETMQ_SECRET_KEY"}, "")
|
||||||
|
cpRelationBroadcastMQEndpoint := getEnvAny([]string{"LIKEI_ROCKETMQ_ENDPOINT", "CHATAPP_TASK_CENTER_MQ_ENDPOINT", "TASK_CENTER_EVENT_MQ_ENDPOINT"}, "")
|
||||||
|
cpRelationBroadcastMQAccessKey := getEnvAny([]string{"LIKEI_ROCKETMQ_ACCESS_KEY", "CHATAPP_TASK_CENTER_MQ_ACCESS_KEY", "TASK_CENTER_EVENT_MQ_ACCESS_KEY"}, "")
|
||||||
|
cpRelationBroadcastMQAccessSecret := getEnvAny([]string{"LIKEI_ROCKETMQ_SECRET_KEY", "CHATAPP_TASK_CENTER_MQ_ACCESS_SECRET", "TASK_CENTER_EVENT_MQ_SECRET_KEY"}, "")
|
||||||
|
cpRelationBroadcastMQEnabled := strings.TrimSpace(cpRelationBroadcastMQEndpoint) != "" &&
|
||||||
|
strings.TrimSpace(cpRelationBroadcastMQAccessKey) != "" &&
|
||||||
|
strings.TrimSpace(cpRelationBroadcastMQAccessSecret) != ""
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
HTTP: HTTPConfig{
|
HTTP: HTTPConfig{
|
||||||
@ -152,6 +373,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"),
|
||||||
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{
|
||||||
@ -166,25 +388,271 @@ func Load() Config {
|
|||||||
},
|
},
|
||||||
Invite: InviteConfig{
|
Invite: InviteConfig{
|
||||||
PublicBaseURL: getEnvAny([]string{"CHATAPP_INVITE_PUBLIC_BASE_URL", "INVITE_PUBLIC_BASE_URL"}, "http://localhost:2900"),
|
PublicBaseURL: getEnvAny([]string{"CHATAPP_INVITE_PUBLIC_BASE_URL", "INVITE_PUBLIC_BASE_URL"}, "http://localhost:2900"),
|
||||||
|
PublicH5Path: getEnvAny([]string{"CHATAPP_INVITE_PUBLIC_H5_PATH", "INVITE_PUBLIC_H5_PATH"}, "/h5/app-invite/landing.html"),
|
||||||
|
},
|
||||||
|
RechargeReward: RechargeRewardConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_DEFAULT_SYS_ORIGIN", "RECHARGE_REWARD_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
Timezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_TIMEZONE", "RECHARGE_REWARD_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||||
|
"Asia/Riyadh",
|
||||||
|
),
|
||||||
|
StorageTimezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_STORAGE_TIMEZONE", "RECHARGE_REWARD_STORAGE_TIMEZONE"},
|
||||||
|
"Asia/Shanghai",
|
||||||
|
),
|
||||||
|
StartDate: getEnvAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_START_DATE", "RECHARGE_REWARD_START_DATE"},
|
||||||
|
"2026-05-21",
|
||||||
|
),
|
||||||
|
CoinSellerGoldPerUSD: int64(getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_RECHARGE_REWARD_COIN_SELLER_GOLD_PER_USD", "RECHARGE_REWARD_COIN_SELLER_GOLD_PER_USD"},
|
||||||
|
100000,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
FirstRechargeReward: FirstRechargeRewardConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_FIRST_RECHARGE_REWARD_DEFAULT_SYS_ORIGIN", "FIRST_RECHARGE_REWARD_DEFAULT_SYS_ORIGIN", "CHATAPP_RECHARGE_REWARD_DEFAULT_SYS_ORIGIN", "RECHARGE_REWARD_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
MQ: FirstRechargeRewardMQConfig{
|
||||||
|
Enabled: getEnvBoolAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_ENABLED", "FIRST_RECHARGE_REWARD_MQ_ENABLED", "CHATAPP_RECHARGE_SUCCESS_MQ_ENABLED", "RECHARGE_SUCCESS_MQ_ENABLED"}, false),
|
||||||
|
Endpoint: firstRechargeRewardMQEndpoint,
|
||||||
|
Namespace: getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_NAMESPACE", "FIRST_RECHARGE_REWARD_MQ_NAMESPACE", "CHATAPP_RECHARGE_SUCCESS_MQ_NAMESPACE", "RECHARGE_SUCCESS_MQ_NAMESPACE"}, ""),
|
||||||
|
AccessKey: firstRechargeRewardMQAccessKey,
|
||||||
|
AccessSecret: firstRechargeRewardMQAccessSecret,
|
||||||
|
SecurityToken: getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_SECURITY_TOKEN", "FIRST_RECHARGE_REWARD_MQ_SECURITY_TOKEN", "CHATAPP_RECHARGE_SUCCESS_MQ_SECURITY_TOKEN", "RECHARGE_SUCCESS_MQ_SECURITY_TOKEN"}, ""),
|
||||||
|
ConsumerGroup: getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_CONSUMER_GROUP", "FIRST_RECHARGE_REWARD_MQ_CONSUMER_GROUP"}, "first-recharge-reward"),
|
||||||
|
Topic: getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_TOPIC", "FIRST_RECHARGE_REWARD_MQ_TOPIC", "CHATAPP_RECHARGE_SUCCESS_MQ_TOPIC", "RECHARGE_SUCCESS_MQ_TOPIC"}, "RC_DEFAULT_APP_ORDINARY"),
|
||||||
|
Tag: getEnvAny([]string{"CHATAPP_FIRST_RECHARGE_REWARD_MQ_TAG", "FIRST_RECHARGE_REWARD_MQ_TAG", "CHATAPP_RECHARGE_SUCCESS_MQ_TAG", "RECHARGE_SUCCESS_MQ_TAG"}, "recharge_success_v1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TaskCenter: TaskCenterConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_TASK_CENTER_DEFAULT_SYS_ORIGIN", "TASK_CENTER_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
Timezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_TASK_CENTER_TIMEZONE", "TASK_CENTER_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||||
|
"Asia/Riyadh",
|
||||||
|
),
|
||||||
|
InternalEventTTL: time.Duration(getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_TASK_CENTER_EVENT_TTL_HOURS", "TASK_CENTER_EVENT_TTL_HOURS"},
|
||||||
|
72,
|
||||||
|
)) * time.Hour,
|
||||||
|
MQ: TaskCenterMQConfig{
|
||||||
|
Enabled: getEnvBoolAny([]string{"CHATAPP_TASK_CENTER_MQ_ENABLED", "TASK_CENTER_EVENT_MQ_ENABLED"}, false),
|
||||||
|
Endpoint: taskCenterMQEndpoint,
|
||||||
|
Namespace: getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_NAMESPACE", "TASK_CENTER_EVENT_MQ_NAMESPACE"}, ""),
|
||||||
|
AccessKey: taskCenterMQAccessKey,
|
||||||
|
AccessSecret: taskCenterMQAccessSecret,
|
||||||
|
SecurityToken: getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_SECURITY_TOKEN", "TASK_CENTER_EVENT_MQ_SECURITY_TOKEN"}, ""),
|
||||||
|
ConsumerGroup: getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_CONSUMER_GROUP", "TASK_CENTER_EVENT_MQ_CONSUMER_GROUP"}, "task-center-progress"),
|
||||||
|
Topic: getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_TOPIC", "TASK_CENTER_EVENT_MQ_TOPIC"}, "RC_DEFAULT_APP_ORDINARY"),
|
||||||
|
Tag: getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_TAG", "TASK_CENTER_EVENT_MQ_TAG"}, "task_center_event"),
|
||||||
|
},
|
||||||
|
Archive: TaskCenterArchiveConfig{
|
||||||
|
Enabled: getEnvBoolAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_ENABLED", "TASK_CENTER_ARCHIVE_ENABLED"}, false),
|
||||||
|
Bucket: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_BUCKET", "TASK_CENTER_ARCHIVE_COS_BUCKET", "LIKEI_TENCENT_COS_BUCKET"}, ""),
|
||||||
|
Region: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_REGION", "TASK_CENTER_ARCHIVE_COS_REGION", "LIKEI_TENCENT_COS_REGION"}, ""),
|
||||||
|
SecretID: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_ID", "TASK_CENTER_ARCHIVE_COS_SECRET_ID", "LIKEI_TENCENT_COS_SECRET_ID"}, ""),
|
||||||
|
SecretKey: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "TASK_CENTER_ARCHIVE_COS_SECRET_KEY", "LIKEI_TENCENT_COS_SECRET_KEY"}, ""),
|
||||||
|
Prefix: getEnvAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_COS_PREFIX", "TASK_CENTER_ARCHIVE_COS_PREFIX"}, "task-center-events/v1"),
|
||||||
|
BatchSize: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_BATCH_SIZE", "TASK_CENTER_ARCHIVE_BATCH_SIZE"}, 500),
|
||||||
|
FlushSeconds: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_FLUSH_SECONDS", "TASK_CENTER_ARCHIVE_FLUSH_SECONDS"}, 60),
|
||||||
|
Workers: getEnvIntAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_WORKERS", "TASK_CENTER_ARCHIVE_WORKERS"}, 2),
|
||||||
|
GC: TaskCenterArchiveGCConfig{
|
||||||
|
// 清理涉及不可逆数据库删除;新环境必须显式开启,避免仅部署代码就开始工作。
|
||||||
|
Enabled: getEnvBoolAny([]string{"CHATAPP_TASK_CENTER_ARCHIVE_GC_ENABLED", "TASK_CENTER_ARCHIVE_GC_ENABLED"}, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
WeekStar: WeekStarConfig{
|
WeekStar: WeekStarConfig{
|
||||||
DefaultSysOrigin: strings.ToUpper(getEnvAny([]string{"CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"}, "LIKEI")),
|
DefaultSysOrigin: strings.ToUpper(getEnvAny([]string{"CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"}, "LIKEI")),
|
||||||
Timezone: getEnvAny([]string{"CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"}, "Asia/Riyadh"),
|
Timezone: getEnvAny([]string{"CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"}, "Asia/Riyadh"),
|
||||||
RetryStreamKey: getEnvAny([]string{"CHATAPP_WEEK_STAR_RETRY_STREAM_KEY", "WEEK_STAR_RETRY_STREAM_KEY"}, "week-star:reward-retry"),
|
RetryStreamKey: getEnvAny([]string{"CHATAPP_WEEK_STAR_RETRY_STREAM_KEY", "WEEK_STAR_RETRY_STREAM_KEY"}, "week-star:reward-retry"),
|
||||||
RocketMQ: WeekStarRocketMQConfig{
|
RocketMQ: WeekStarRocketMQConfig{
|
||||||
Enabled: getEnvBoolAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ENABLED", "WEEK_STAR_ROCKETMQ_ENABLED"}, false),
|
Enabled: getEnvBoolAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ENABLED", "WEEK_STAR_ROCKETMQ_ENABLED"}, weekStarRocketMQEnabledDefault),
|
||||||
Endpoint: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT", "WEEK_STAR_ROCKETMQ_ENDPOINT"}, ""),
|
Endpoint: weekStarRocketMQEndpoint,
|
||||||
Namespace: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_NAMESPACE", "WEEK_STAR_ROCKETMQ_NAMESPACE"}, ""),
|
Namespace: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_NAMESPACE", "WEEK_STAR_ROCKETMQ_NAMESPACE"}, ""),
|
||||||
AccessKey: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY", "WEEK_STAR_ROCKETMQ_ACCESS_KEY"}, ""),
|
AccessKey: weekStarRocketMQAccessKey,
|
||||||
AccessSecret: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_SECRET", "WEEK_STAR_ROCKETMQ_ACCESS_SECRET"}, ""),
|
AccessSecret: weekStarRocketMQAccessSecret,
|
||||||
SecurityToken: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_SECURITY_TOKEN", "WEEK_STAR_ROCKETMQ_SECURITY_TOKEN"}, ""),
|
SecurityToken: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_SECURITY_TOKEN", "WEEK_STAR_ROCKETMQ_SECURITY_TOKEN"}, ""),
|
||||||
ConsumerGroup: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_CONSUMER_GROUP", "WEEK_STAR_ROCKETMQ_CONSUMER_GROUP"}, "week-star-activity"),
|
ConsumerGroup: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_CONSUMER_GROUP", "WEEK_STAR_ROCKETMQ_CONSUMER_GROUP"}, "week-star-activity"),
|
||||||
Topic: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_TOPIC", "WEEK_STAR_ROCKETMQ_TOPIC"}, "GIVE_GIFTS"),
|
Topic: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_TOPIC", "WEEK_STAR_ROCKETMQ_TOPIC"}, "RC_DEFAULT_APP_ORDINARY"),
|
||||||
Tag: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_TAG", "WEEK_STAR_ROCKETMQ_TAG"}, "give_gift_v3"),
|
Tag: getEnvAny([]string{"CHATAPP_WEEK_STAR_ROCKETMQ_TAG", "WEEK_STAR_ROCKETMQ_TAG"}, "give_gift_v3"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
YumiGiftChallenge: YumiGiftChallengeConfig{
|
||||||
|
// Java 的 Yumi 业务来源枚举是 LIKEI;此默认值不能使用仅供品牌识别的 YUMI。
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_DEFAULT_SYS_ORIGIN"}, "LIKEI")),
|
||||||
|
DeliveryProcessingLeaseSeconds: getEnvIntAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_DELIVERY_LEASE_SECONDS"}, 300),
|
||||||
|
RocketMQ: WeekStarRocketMQConfig{
|
||||||
|
Enabled: getEnvBoolAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_ENABLED"}, yumiGiftRocketMQEnabledDefault),
|
||||||
|
Endpoint: yumiGiftRocketMQEndpoint,
|
||||||
|
Namespace: getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_NAMESPACE", "LIKEI_ROCKETMQ_NAMESPACE"}, ""),
|
||||||
|
AccessKey: yumiGiftRocketMQAccessKey,
|
||||||
|
AccessSecret: yumiGiftRocketMQAccessSecret,
|
||||||
|
SecurityToken: getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_SECURITY_TOKEN"}, ""),
|
||||||
|
ConsumerGroup: getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_CONSUMER_GROUP"}, "YUMI_GIFT_CHALLENGE_GIFT"),
|
||||||
|
Topic: getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_TOPIC", "CHATAPP_WEEK_STAR_ROCKETMQ_TOPIC"}, "RC_DEFAULT_APP_ORDINARY"),
|
||||||
|
Tag: getEnvAny([]string{"CHATAPP_YUMI_GIFT_CHALLENGE_ROCKETMQ_TAG", "CHATAPP_WEEK_STAR_ROCKETMQ_TAG"}, "give_gift_v3"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoomTurnoverReward: RoomTurnoverRewardConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_ROOM_TURNOVER_REWARD_DEFAULT_SYS_ORIGIN", "ROOM_TURNOVER_REWARD_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
Timezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_ROOM_TURNOVER_REWARD_TIMEZONE", "ROOM_TURNOVER_REWARD_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||||
|
"Asia/Riyadh",
|
||||||
|
),
|
||||||
|
CycleDuration: time.Duration(getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_ROOM_TURNOVER_REWARD_CYCLE_SECONDS", "ROOM_TURNOVER_REWARD_CYCLE_SECONDS"},
|
||||||
|
10,
|
||||||
|
)) * time.Second,
|
||||||
|
},
|
||||||
|
VoiceRoomRedPacket: VoiceRoomRedPacketConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_RED_PACKET_DEFAULT_SYS_ORIGIN", "VOICE_ROOM_RED_PACKET_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
Timezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_RED_PACKET_TIMEZONE", "VOICE_ROOM_RED_PACKET_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||||
|
"Asia/Riyadh",
|
||||||
|
),
|
||||||
|
PresenceTTLSeconds: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_RED_PACKET_PRESENCE_TTL_SECONDS", "VOICE_ROOM_RED_PACKET_PRESENCE_TTL_SECONDS"},
|
||||||
|
60,
|
||||||
|
),
|
||||||
|
RefundScanIntervalSeconds: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_RED_PACKET_REFUND_SCAN_INTERVAL_SECONDS", "VOICE_ROOM_RED_PACKET_REFUND_SCAN_INTERVAL_SECONDS"},
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
RefundBatchSize: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_RED_PACKET_REFUND_BATCH_SIZE", "VOICE_ROOM_RED_PACKET_REFUND_BATCH_SIZE"},
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
NotifyStreamKey: getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_RED_PACKET_NOTIFY_STREAM_KEY", "VOICE_ROOM_RED_PACKET_NOTIFY_STREAM_KEY"},
|
||||||
|
"voice_room:red_packet:notify",
|
||||||
|
),
|
||||||
|
ExpireZSetKey: getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_RED_PACKET_EXPIRE_ZSET_KEY", "VOICE_ROOM_RED_PACKET_EXPIRE_ZSET_KEY"},
|
||||||
|
"voice_room:red_packet:expire_zset",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
VoiceRoomRocket: VoiceRoomRocketConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_DEFAULT_SYS_ORIGIN", "VOICE_ROOM_ROCKET_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
Timezone: getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_TIMEZONE", "VOICE_ROOM_ROCKET_TIMEZONE", "CHATAPP_WEEK_STAR_TIMEZONE", "WEEK_STAR_TIMEZONE"},
|
||||||
|
"Asia/Riyadh",
|
||||||
|
),
|
||||||
|
RewardRetryStreamKey: getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_REWARD_RETRY_STREAM_KEY", "VOICE_ROOM_ROCKET_REWARD_RETRY_STREAM_KEY"},
|
||||||
|
"voice_room:rocket:reward_retry",
|
||||||
|
),
|
||||||
|
InRoomRewardDueZSetKey: getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_IN_ROOM_DUE_ZSET_KEY", "VOICE_ROOM_ROCKET_IN_ROOM_DUE_ZSET_KEY"},
|
||||||
|
"voice_room:rocket:in_room_due",
|
||||||
|
),
|
||||||
|
RoomLockTTLSeconds: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_ROOM_LOCK_TTL_SECONDS", "VOICE_ROOM_ROCKET_ROOM_LOCK_TTL_SECONDS"},
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
BroadcastStreamKey: getEnvAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_BROADCAST_STREAM_KEY", "VOICE_ROOM_ROCKET_BROADCAST_STREAM_KEY"},
|
||||||
|
"voice_room:rocket:broadcast",
|
||||||
|
),
|
||||||
|
BroadcastStreamMaxLen: int64(getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_BROADCAST_STREAM_MAX_LEN", "VOICE_ROOM_ROCKET_BROADCAST_STREAM_MAX_LEN"},
|
||||||
|
10000,
|
||||||
|
)),
|
||||||
|
InRoomRewardScanIntervalSeconds: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_IN_ROOM_SCAN_INTERVAL_SECONDS", "VOICE_ROOM_ROCKET_IN_ROOM_SCAN_INTERVAL_SECONDS"},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
InRoomRewardBatchSize: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_IN_ROOM_BATCH_SIZE", "VOICE_ROOM_ROCKET_IN_ROOM_BATCH_SIZE"},
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
InRoomRewardUserLimit: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_IN_ROOM_USER_LIMIT", "VOICE_ROOM_ROCKET_IN_ROOM_USER_LIMIT"},
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
RewardGrantScanIntervalSeconds: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_REWARD_GRANT_SCAN_INTERVAL_SECONDS", "VOICE_ROOM_ROCKET_REWARD_GRANT_SCAN_INTERVAL_SECONDS"},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
RewardGrantBatchSize: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_VOICE_ROOM_ROCKET_REWARD_GRANT_BATCH_SIZE", "VOICE_ROOM_ROCKET_REWARD_GRANT_BATCH_SIZE"},
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
MQ: VoiceRoomRocketMQConfig{
|
||||||
|
Enabled: getEnvBoolAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_ENABLED", "VOICE_ROOM_ROCKET_MQ_ENABLED"}, false),
|
||||||
|
Endpoint: voiceRoomRocketMQEndpoint,
|
||||||
|
Namespace: getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_NAMESPACE", "VOICE_ROOM_ROCKET_MQ_NAMESPACE"}, ""),
|
||||||
|
AccessKey: voiceRoomRocketMQAccessKey,
|
||||||
|
AccessSecret: voiceRoomRocketMQAccessSecret,
|
||||||
|
SecurityToken: getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_SECURITY_TOKEN", "VOICE_ROOM_ROCKET_MQ_SECURITY_TOKEN"}, ""),
|
||||||
|
ConsumerGroup: getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_CONSUMER_GROUP", "VOICE_ROOM_ROCKET_MQ_CONSUMER_GROUP"}, "voice-room-rocket"),
|
||||||
|
Topic: getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_TOPIC", "VOICE_ROOM_ROCKET_MQ_TOPIC"}, "RC_DEFAULT_APP_ORDINARY"),
|
||||||
|
Tag: getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_TAG", "VOICE_ROOM_ROCKET_MQ_TAG"}, "give_gift_v3"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CPRelationBroadcast: CPRelationBroadcastConfig{
|
||||||
|
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
||||||
|
[]string{"CHATAPP_CP_RELATION_BROADCAST_DEFAULT_SYS_ORIGIN", "CP_RELATION_BROADCAST_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
"LIKEI",
|
||||||
|
)),
|
||||||
|
MQ: CPRelationBroadcastMQConfig{
|
||||||
|
Enabled: cpRelationBroadcastMQEnabled,
|
||||||
|
Endpoint: cpRelationBroadcastMQEndpoint,
|
||||||
|
Namespace: "",
|
||||||
|
AccessKey: cpRelationBroadcastMQAccessKey,
|
||||||
|
AccessSecret: cpRelationBroadcastMQAccessSecret,
|
||||||
|
SecurityToken: "",
|
||||||
|
ConsumerGroup: cpRelationBroadcastMQConsumerGroup,
|
||||||
|
Topic: cpRelationBroadcastMQTopic,
|
||||||
|
Tag: cpRelationBroadcastMQTag,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TencentIM: TencentIMConfig{
|
||||||
|
AppID: getEnvInt64Any(
|
||||||
|
[]string{"CHATAPP_TENCENT_IM_APP_ID", "TENCENT_IM_APP_ID", "LIKEI_IM_APP_ID"},
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
Key: getEnvAny(
|
||||||
|
[]string{"CHATAPP_TENCENT_IM_KEY", "TENCENT_IM_KEY", "LIKEI_IM_KEY"},
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
Identifier: getEnvAny(
|
||||||
|
[]string{"CHATAPP_TENCENT_IM_IDENTIFIER", "TENCENT_IM_IDENTIFIER", "LIKEI_IM_IDENTIFIER"},
|
||||||
|
"administrator",
|
||||||
|
),
|
||||||
|
BaseEndpoint: getEnvAny(
|
||||||
|
[]string{"CHATAPP_TENCENT_IM_BASE_ENDPOINT", "TENCENT_IM_BASE_ENDPOINT", "LIKEI_IM_BASE_ENDPOINT"},
|
||||||
|
"https://console.tim.qq.com",
|
||||||
|
),
|
||||||
|
UserSigExpireSeconds: getEnvInt64Any(
|
||||||
|
[]string{"CHATAPP_TENCENT_IM_USERSIG_EXPIRE_SECONDS", "TENCENT_IM_USERSIG_EXPIRE_SECONDS"},
|
||||||
|
7200000,
|
||||||
|
),
|
||||||
|
RequestTimeoutSeconds: getEnvIntAny(
|
||||||
|
[]string{"CHATAPP_TENCENT_IM_REQUEST_TIMEOUT_SECONDS", "TENCENT_IM_REQUEST_TIMEOUT_SECONDS"},
|
||||||
|
8,
|
||||||
|
),
|
||||||
|
},
|
||||||
Baishun: BaishunConfig{
|
Baishun: BaishunConfig{
|
||||||
PlatformBaseURL: getEnvAny([]string{"CHATAPP_BAISHUN_PLATFORM_BASE_URL", "BAISHUN_PLATFORM_BASE_URL"}, ""),
|
PlatformBaseURL: getEnvAny([]string{"CHATAPP_BAISHUN_PLATFORM_BASE_URL", "BAISHUN_PLATFORM_BASE_URL"}, ""),
|
||||||
|
GameListURL: getEnvAny([]string{"CHATAPP_BAISHUN_GAME_LIST_URL", "BAISHUN_GAME_LIST_URL"}, ""),
|
||||||
|
LuckyGiftURL: getEnvAny([]string{"CHATAPP_BAISHUN_LUCKY_GIFT_URL", "BAISHUN_LUCKY_GIFT_URL"}, ""),
|
||||||
AppID: getEnvInt64Any([]string{"CHATAPP_BAISHUN_APP_ID", "BAISHUN_APP_ID"}, 0),
|
AppID: getEnvInt64Any([]string{"CHATAPP_BAISHUN_APP_ID", "BAISHUN_APP_ID"}, 0),
|
||||||
AppName: getEnvAny([]string{"CHATAPP_BAISHUN_APP_NAME", "BAISHUN_APP_NAME"}, ""),
|
AppName: getEnvAny([]string{"CHATAPP_BAISHUN_APP_NAME", "BAISHUN_APP_NAME"}, ""),
|
||||||
AppChannel: getEnvAny([]string{"CHATAPP_BAISHUN_APP_CHANNEL", "BAISHUN_APP_CHANNEL"}, ""),
|
AppChannel: getEnvAny([]string{"CHATAPP_BAISHUN_APP_CHANNEL", "BAISHUN_APP_CHANNEL"}, ""),
|
||||||
@ -193,6 +661,14 @@ func Load() Config {
|
|||||||
LaunchCodeTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_LAUNCH_CODE_TTL_SECONDS", "BAISHUN_LAUNCH_CODE_TTL_SECONDS"}, 300),
|
LaunchCodeTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_LAUNCH_CODE_TTL_SECONDS", "BAISHUN_LAUNCH_CODE_TTL_SECONDS"}, 300),
|
||||||
SSTokenTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_SS_TOKEN_TTL_SECONDS", "BAISHUN_SS_TOKEN_TTL_SECONDS"}, 86400),
|
SSTokenTTLSeconds: getEnvIntAny([]string{"CHATAPP_BAISHUN_SS_TOKEN_TTL_SECONDS", "BAISHUN_SS_TOKEN_TTL_SECONDS"}, 86400),
|
||||||
},
|
},
|
||||||
|
Lingxian: LingxianConfig{
|
||||||
|
GameListURL: getEnvAny([]string{"CHATAPP_LINGXIAN_GAME_LIST_URL", "LINGXIAN_GAME_LIST_URL"}, "https://sg-test.leadercc.com/yumichat_games/test_game_list.json"),
|
||||||
|
AppKey: getEnvAny([]string{"CHATAPP_LINGXIAN_APP_KEY", "LINGXIAN_APP_KEY", "GAME_HKYS_SIGN_KEY", "LIKEI_GAME_HKYS_SIGN_KEY"}, ""),
|
||||||
|
},
|
||||||
|
Hotgame: HotgameConfig{
|
||||||
|
AppKey: getEnvAny([]string{"CHATAPP_HOTGAME_APP_KEY", "HOTGAME_APP_KEY", "CHATAPP_REYOU_APP_KEY", "REYOU_APP_KEY"}, ""),
|
||||||
|
CallbackBaseURL: getEnvAny([]string{"CHATAPP_HOTGAME_CALLBACK_BASE_URL", "HOTGAME_CALLBACK_BASE_URL", "CHATAPP_REYOU_CALLBACK_BASE_URL", "REYOU_CALLBACK_BASE_URL"}, ""),
|
||||||
|
},
|
||||||
GameOpen: GameOpenConfig{
|
GameOpen: GameOpenConfig{
|
||||||
PublicBaseURL: getEnvAny([]string{"CHATAPP_GAME_OPEN_PUBLIC_BASE_URL", "GAME_OPEN_PUBLIC_BASE_URL"}, ""),
|
PublicBaseURL: getEnvAny([]string{"CHATAPP_GAME_OPEN_PUBLIC_BASE_URL", "GAME_OPEN_PUBLIC_BASE_URL"}, ""),
|
||||||
AppKey: getEnvAny([]string{"CHATAPP_GAME_OPEN_APP_KEY", "GAME_OPEN_APP_KEY"}, "game-open-test-key"),
|
AppKey: getEnvAny([]string{"CHATAPP_GAME_OPEN_APP_KEY", "GAME_OPEN_APP_KEY"}, "game-open-test-key"),
|
||||||
@ -204,8 +680,12 @@ func Load() Config {
|
|||||||
Enabled: getEnvBoolAny([]string{"CHATAPP_LUCKY_GIFT_ENABLED", "LUCKY_GIFT_ENABLED", "LUCKY_GIFT_GO_ENABLED"}, false),
|
Enabled: getEnvBoolAny([]string{"CHATAPP_LUCKY_GIFT_ENABLED", "LUCKY_GIFT_ENABLED", "LUCKY_GIFT_GO_ENABLED"}, false),
|
||||||
Timeout: time.Duration(getEnvIntAny([]string{"CHATAPP_LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_GO_TIMEOUT_MILLIS"}, 8000)) * time.Millisecond,
|
Timeout: time.Duration(getEnvIntAny([]string{"CHATAPP_LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_TIMEOUT_MILLIS", "LUCKY_GIFT_GO_TIMEOUT_MILLIS"}, 8000)) * time.Millisecond,
|
||||||
RewardStreamKey: getEnvAny([]string{"CHATAPP_LUCKY_GIFT_REWARD_STREAM_KEY", "LUCKY_GIFT_REWARD_STREAM_KEY"}, "lucky-gift:reward"),
|
RewardStreamKey: getEnvAny([]string{"CHATAPP_LUCKY_GIFT_REWARD_STREAM_KEY", "LUCKY_GIFT_REWARD_STREAM_KEY"}, "lucky-gift:reward"),
|
||||||
|
RewardStreamMaxLen: int64(getEnvIntAny([]string{"CHATAPP_LUCKY_GIFT_REWARD_STREAM_MAX_LEN", "LUCKY_GIFT_REWARD_STREAM_MAX_LEN"}, 100000)),
|
||||||
Providers: loadLuckyGiftConfigs(),
|
Providers: loadLuckyGiftConfigs(),
|
||||||
},
|
},
|
||||||
|
BinanceRecharge: BinanceRechargeConfig{
|
||||||
|
BaseURL: getEnvAny([]string{"CHATAPP_BINANCE_RECHARGE_BASE_URL", "BINANCE_RECHARGE_BASE_URL"}, "https://api.binance.com"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,13 +787,13 @@ func normalizeLuckyGiftConfigs(
|
|||||||
item.AppID = strings.TrimSpace(item.AppID)
|
item.AppID = strings.TrimSpace(item.AppID)
|
||||||
item.AppChannel = strings.TrimSpace(item.AppChannel)
|
item.AppChannel = strings.TrimSpace(item.AppChannel)
|
||||||
item.AppKey = strings.TrimSpace(item.AppKey)
|
item.AppKey = strings.TrimSpace(item.AppKey)
|
||||||
if sharedBaishunAppID != "" {
|
if item.AppID == "" && sharedBaishunAppID != "" {
|
||||||
item.AppID = sharedBaishunAppID
|
item.AppID = sharedBaishunAppID
|
||||||
}
|
}
|
||||||
if sharedBaishunAppChannel != "" {
|
if item.AppChannel == "" && sharedBaishunAppChannel != "" {
|
||||||
item.AppChannel = sharedBaishunAppChannel
|
item.AppChannel = sharedBaishunAppChannel
|
||||||
}
|
}
|
||||||
if sharedBaishunAppKey != "" {
|
if item.AppKey == "" && sharedBaishunAppKey != "" {
|
||||||
item.AppKey = sharedBaishunAppKey
|
item.AppKey = sharedBaishunAppKey
|
||||||
}
|
}
|
||||||
if item.URL == "" || item.AppID == "" || item.AppChannel == "" || item.AppKey == "" {
|
if item.URL == "" || item.AppID == "" || item.AppChannel == "" || item.AppKey == "" {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeLuckyGiftConfigsUsesSharedBaishunAppKey(t *testing.T) {
|
func TestNormalizeLuckyGiftConfigsKeepsDedicatedAccountWhenSharedPresent(t *testing.T) {
|
||||||
configs := normalizeLuckyGiftConfigs([]LuckyGiftProviderConfig{
|
configs := normalizeLuckyGiftConfigs([]LuckyGiftProviderConfig{
|
||||||
{
|
{
|
||||||
StandardID: 18816,
|
StandardID: 18816,
|
||||||
@ -18,30 +18,6 @@ func TestNormalizeLuckyGiftConfigsUsesSharedBaishunAppKey(t *testing.T) {
|
|||||||
if len(configs) != 1 {
|
if len(configs) != 1 {
|
||||||
t.Fatalf("expected 1 config, got %d", len(configs))
|
t.Fatalf("expected 1 config, got %d", len(configs))
|
||||||
}
|
}
|
||||||
if got := configs[0].AppID; got != "shared-baishun-app-id" {
|
|
||||||
t.Fatalf("expected baishun app id override, got %q", got)
|
|
||||||
}
|
|
||||||
if got := configs[0].AppChannel; got != "shared-baishun-app-channel" {
|
|
||||||
t.Fatalf("expected baishun app channel override, got %q", got)
|
|
||||||
}
|
|
||||||
if got := configs[0].AppKey; got != "shared-baishun-key" {
|
|
||||||
t.Fatalf("expected baishun key override, got %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeLuckyGiftConfigsKeepsDedicatedKeyWhenSharedBlank(t *testing.T) {
|
|
||||||
configs := normalizeLuckyGiftConfigs([]LuckyGiftProviderConfig{
|
|
||||||
{
|
|
||||||
StandardID: 18816,
|
|
||||||
URL: "https://provider.example.com",
|
|
||||||
AppID: "lucky-app-id",
|
|
||||||
AppChannel: "lucky-app-channel",
|
|
||||||
AppKey: "legacy-lucky-key",
|
|
||||||
},
|
|
||||||
}, "", "", "")
|
|
||||||
if len(configs) != 1 {
|
|
||||||
t.Fatalf("expected 1 config, got %d", len(configs))
|
|
||||||
}
|
|
||||||
if got := configs[0].AppID; got != "lucky-app-id" {
|
if got := configs[0].AppID; got != "lucky-app-id" {
|
||||||
t.Fatalf("expected dedicated lucky gift app id, got %q", got)
|
t.Fatalf("expected dedicated lucky gift app id, got %q", got)
|
||||||
}
|
}
|
||||||
@ -53,6 +29,27 @@ func TestNormalizeLuckyGiftConfigsKeepsDedicatedKeyWhenSharedBlank(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeLuckyGiftConfigsFallsBackToSharedAccountWhenDedicatedBlank(t *testing.T) {
|
||||||
|
configs := normalizeLuckyGiftConfigs([]LuckyGiftProviderConfig{
|
||||||
|
{
|
||||||
|
StandardID: 18816,
|
||||||
|
URL: "https://provider.example.com",
|
||||||
|
},
|
||||||
|
}, "shared-baishun-app-id", "shared-baishun-app-channel", "shared-baishun-key")
|
||||||
|
if len(configs) != 1 {
|
||||||
|
t.Fatalf("expected 1 config, got %d", len(configs))
|
||||||
|
}
|
||||||
|
if got := configs[0].AppID; got != "shared-baishun-app-id" {
|
||||||
|
t.Fatalf("expected shared baishun app id fallback, got %q", got)
|
||||||
|
}
|
||||||
|
if got := configs[0].AppChannel; got != "shared-baishun-app-channel" {
|
||||||
|
t.Fatalf("expected shared baishun app channel fallback, got %q", got)
|
||||||
|
}
|
||||||
|
if got := configs[0].AppKey; got != "shared-baishun-key" {
|
||||||
|
t.Fatalf("expected shared baishun key fallback, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetEnvBoolAliasSupportsLegacyLuckyGiftKey(t *testing.T) {
|
func TestGetEnvBoolAliasSupportsLegacyLuckyGiftKey(t *testing.T) {
|
||||||
t.Setenv("LUCKY_GIFT_GO_ENABLED", "true")
|
t.Setenv("LUCKY_GIFT_GO_ENABLED", "true")
|
||||||
if got := getEnvBoolAny([]string{"LUCKY_GIFT_ENABLED", "LUCKY_GIFT_GO_ENABLED"}, false); !got {
|
if got := getEnvBoolAny([]string{"LUCKY_GIFT_ENABLED", "LUCKY_GIFT_GO_ENABLED"}, false); !got {
|
||||||
@ -60,6 +57,125 @@ func TestGetEnvBoolAliasSupportsLegacyLuckyGiftKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadWeekStarRocketMQAutoEnablesWhenCredentialsPresent(t *testing.T) {
|
||||||
|
unsetEnvForTest(t, "CHATAPP_WEEK_STAR_ROCKETMQ_ENABLED", "WEEK_STAR_ROCKETMQ_ENABLED")
|
||||||
|
t.Setenv("CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT", "rmq.example.com:8081")
|
||||||
|
t.Setenv("CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY", "access-key")
|
||||||
|
t.Setenv("CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_SECRET", "access-secret")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if !cfg.WeekStar.RocketMQ.Enabled {
|
||||||
|
t.Fatalf("expected week star rocketmq to auto-enable when endpoint and credentials are present")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWeekStarRocketMQExplicitDisableOverridesCredentials(t *testing.T) {
|
||||||
|
t.Setenv("CHATAPP_WEEK_STAR_ROCKETMQ_ENABLED", "false")
|
||||||
|
t.Setenv("CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT", "rmq.example.com:8081")
|
||||||
|
t.Setenv("CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY", "access-key")
|
||||||
|
t.Setenv("CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_SECRET", "access-secret")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if cfg.WeekStar.RocketMQ.Enabled {
|
||||||
|
t.Fatalf("expected explicit week star rocketmq disable to override credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadTaskCenterMQUsesSharedRocketMQCredentials(t *testing.T) {
|
||||||
|
t.Setenv("CHATAPP_TASK_CENTER_MQ_ENABLED", "true")
|
||||||
|
t.Setenv("LIKEI_ROCKETMQ_ENDPOINT", "rmq.example.com:8081")
|
||||||
|
t.Setenv("LIKEI_ROCKETMQ_ACCESS_KEY", "access-key")
|
||||||
|
t.Setenv("LIKEI_ROCKETMQ_SECRET_KEY", "access-secret")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if !cfg.TaskCenter.MQ.Enabled {
|
||||||
|
t.Fatalf("expected task center mq enabled")
|
||||||
|
}
|
||||||
|
if got := cfg.TaskCenter.MQ.Endpoint; got != "rmq.example.com:8081" {
|
||||||
|
t.Fatalf("task center mq endpoint = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.TaskCenter.MQ.Topic; got != "RC_DEFAULT_APP_ORDINARY" {
|
||||||
|
t.Fatalf("task center mq topic = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.TaskCenter.MQ.Tag; got != "task_center_event" {
|
||||||
|
t.Fatalf("task center mq tag = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadCPRelationBroadcastMQUsesSharedRocketMQCredentials(t *testing.T) {
|
||||||
|
t.Setenv("LIKEI_ROCKETMQ_ENDPOINT", "rmq.example.com:8081")
|
||||||
|
t.Setenv("LIKEI_ROCKETMQ_ACCESS_KEY", "access-key")
|
||||||
|
t.Setenv("LIKEI_ROCKETMQ_SECRET_KEY", "access-secret")
|
||||||
|
t.Setenv("CHATAPP_CP_RELATION_BROADCAST_MQ_ENDPOINT", "should-not-use:8081")
|
||||||
|
t.Setenv("CHATAPP_CP_RELATION_BROADCAST_MQ_TOPIC", "should-not-use")
|
||||||
|
t.Setenv("CHATAPP_CP_RELATION_BROADCAST_MQ_TAG", "should-not-use")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if !cfg.CPRelationBroadcast.MQ.Enabled {
|
||||||
|
t.Fatalf("expected cp relation broadcast mq enabled")
|
||||||
|
}
|
||||||
|
if got := cfg.CPRelationBroadcast.MQ.Endpoint; got != "rmq.example.com:8081" {
|
||||||
|
t.Fatalf("cp relation broadcast mq endpoint = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.CPRelationBroadcast.MQ.Topic; got != "RC_DEFAULT_APP_ORDINARY" {
|
||||||
|
t.Fatalf("cp relation broadcast mq topic = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.CPRelationBroadcast.MQ.Tag; got != "cp_relation_broadcast" {
|
||||||
|
t.Fatalf("cp relation broadcast mq tag = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadTaskCenterArchiveUsesSharedCOSCredentials(t *testing.T) {
|
||||||
|
t.Setenv("CHATAPP_TASK_CENTER_ARCHIVE_ENABLED", "true")
|
||||||
|
t.Setenv("LIKEI_TENCENT_COS_BUCKET", "bucket-123")
|
||||||
|
t.Setenv("LIKEI_TENCENT_COS_REGION", "ap-test")
|
||||||
|
t.Setenv("LIKEI_TENCENT_COS_SECRET_ID", "secret-id")
|
||||||
|
t.Setenv("LIKEI_TENCENT_COS_SECRET_KEY", "secret-key")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if !cfg.TaskCenter.Archive.Enabled {
|
||||||
|
t.Fatalf("expected task center archive enabled")
|
||||||
|
}
|
||||||
|
if got := cfg.TaskCenter.Archive.Bucket; got != "bucket-123" {
|
||||||
|
t.Fatalf("task center archive bucket = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.TaskCenter.Archive.Prefix; got != "task-center-events/v1" {
|
||||||
|
t.Fatalf("task center archive prefix = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadLingxianConfigSupportsLikeiHkysSignKey(t *testing.T) {
|
||||||
|
t.Setenv("LIKEI_GAME_HKYS_SIGN_KEY", "likei-hkys-key")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if got := cfg.Lingxian.AppKey; got != "likei-hkys-key" {
|
||||||
|
t.Fatalf("expected legacy likei hkys sign key, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsetEnvForTest(t *testing.T, keys ...string) {
|
||||||
|
t.Helper()
|
||||||
|
values := make(map[string]string, len(keys))
|
||||||
|
present := make(map[string]bool, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
value, ok := os.LookupEnv(key)
|
||||||
|
values[key] = value
|
||||||
|
present[key] = ok
|
||||||
|
if err := os.Unsetenv(key); err != nil {
|
||||||
|
t.Fatalf("unset %s: %v", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
for _, key := range keys {
|
||||||
|
if present[key] {
|
||||||
|
_ = os.Setenv(key, values[key])
|
||||||
|
} else {
|
||||||
|
_ = os.Unsetenv(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadLuckyGiftConfigsSupportsLegacyDefaultEnv(t *testing.T) {
|
func TestLoadLuckyGiftConfigsSupportsLegacyDefaultEnv(t *testing.T) {
|
||||||
oldJSON, hadJSON := os.LookupEnv("LUCKY_GIFT_CONFIGS_JSON")
|
oldJSON, hadJSON := os.LookupEnv("LUCKY_GIFT_CONFIGS_JSON")
|
||||||
oldList, hadList := os.LookupEnv("LUCKY_GIFT_CONFIGS")
|
oldList, hadList := os.LookupEnv("LUCKY_GIFT_CONFIGS")
|
||||||
|
|||||||
@ -12,10 +12,14 @@ type Gateways struct {
|
|||||||
Device *DeviceGateway
|
Device *DeviceGateway
|
||||||
InviteCode *InviteCodeGateway
|
InviteCode *InviteCodeGateway
|
||||||
Notice *NoticeGateway
|
Notice *NoticeGateway
|
||||||
|
Order *OrderGateway
|
||||||
RewardDispatch *RewardDispatchGateway
|
RewardDispatch *RewardDispatchGateway
|
||||||
RewardQuery *RewardQueryGateway
|
RewardQuery *RewardQueryGateway
|
||||||
Profile *ProfileGateway
|
Profile *ProfileGateway
|
||||||
|
Room *RoomGateway
|
||||||
|
Team *TeamGateway
|
||||||
Wallet *WalletGateway
|
Wallet *WalletGateway
|
||||||
|
Freight *FreightGateway
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGateways 基于共享 HTTP 客户端创建所有 Java 网关。
|
// NewGateways 基于共享 HTTP 客户端创建所有 Java 网关。
|
||||||
@ -26,10 +30,14 @@ func NewGateways(cfg config.Config) Gateways {
|
|||||||
Device: &DeviceGateway{client: client},
|
Device: &DeviceGateway{client: client},
|
||||||
InviteCode: &InviteCodeGateway{client: client},
|
InviteCode: &InviteCodeGateway{client: client},
|
||||||
Notice: &NoticeGateway{client: client},
|
Notice: &NoticeGateway{client: client},
|
||||||
|
Order: &OrderGateway{client: client},
|
||||||
RewardDispatch: &RewardDispatchGateway{client: client},
|
RewardDispatch: &RewardDispatchGateway{client: client},
|
||||||
RewardQuery: &RewardQueryGateway{client: client},
|
RewardQuery: &RewardQueryGateway{client: client},
|
||||||
Profile: &ProfileGateway{client: client},
|
Profile: &ProfileGateway{client: client},
|
||||||
|
Room: &RoomGateway{client: client},
|
||||||
|
Team: &TeamGateway{client: client},
|
||||||
Wallet: &WalletGateway{client: client},
|
Wallet: &WalletGateway{client: client},
|
||||||
|
Freight: &FreightGateway{client: client},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +56,16 @@ func (g *Gateways) AuthenticateConsoleToken(ctx context.Context, authorization s
|
|||||||
return g.Auth.AuthenticateConsoleToken(ctx, authorization)
|
return g.Auth.AuthenticateConsoleToken(ctx, authorization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListConsoleButtonAliases 透传管理员按钮权限查询。
|
||||||
|
func (g *Gateways) ListConsoleButtonAliases(ctx context.Context, authorization string) (map[string]struct{}, error) {
|
||||||
|
return g.Auth.ListConsoleButtonAliases(ctx, authorization)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasConsoleMenuAlias 查询管理员是否拥有指定页面菜单。
|
||||||
|
func (g *Gateways) HasConsoleMenuAlias(ctx context.Context, authorization, requiredAlias string) (bool, error) {
|
||||||
|
return g.Auth.HasConsoleMenuAlias(ctx, authorization, requiredAlias)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDeviceFingerprint 透传到设备网关。
|
// GetDeviceFingerprint 透传到设备网关。
|
||||||
func (g *Gateways) GetDeviceFingerprint(ctx context.Context, userID int64) (string, error) {
|
func (g *Gateways) GetDeviceFingerprint(ctx context.Context, userID int64) (string, error) {
|
||||||
return g.Device.GetDeviceFingerprint(ctx, userID)
|
return g.Device.GetDeviceFingerprint(ctx, userID)
|
||||||
@ -63,6 +81,16 @@ func (g *Gateways) SendOfficialNoticeCustomize(ctx context.Context, req Official
|
|||||||
return g.Notice.SendOfficialNoticeCustomize(ctx, req)
|
return g.Notice.SendOfficialNoticeCustomize(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapUserTotalRecharge 透传到订单充值统计网关。
|
||||||
|
func (g *Gateways) MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]UserTotalRecharge, error) {
|
||||||
|
return g.Order.MapUserTotalRecharge(ctx, userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetThisMonthTotalPersonalRecharge 透传到订单充值统计网关。
|
||||||
|
func (g *Gateways) GetThisMonthTotalPersonalRecharge(ctx context.Context, userID int64) (DecimalString, error) {
|
||||||
|
return g.Order.GetThisMonthTotalPersonalRecharge(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
// GrantGold 透传到奖励发放网关。
|
// GrantGold 透传到奖励发放网关。
|
||||||
func (g *Gateways) GrantGold(ctx context.Context, req GrantGoldRequest) error {
|
func (g *Gateways) GrantGold(ctx context.Context, req GrantGoldRequest) error {
|
||||||
return g.RewardDispatch.GrantGold(ctx, req)
|
return g.RewardDispatch.GrantGold(ctx, req)
|
||||||
@ -73,26 +101,151 @@ func (g *Gateways) GrantProps(ctx context.Context, req GrantPropsRequest) error
|
|||||||
return g.RewardDispatch.GrantProps(ctx, req)
|
return g.RewardDispatch.GrantProps(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GivePropsBackpack 透传到道具背包发放接口。
|
||||||
|
func (g *Gateways) GivePropsBackpack(ctx context.Context, req GivePropsBackpackRequest) error {
|
||||||
|
return g.RewardDispatch.GivePropsBackpack(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrGiftBackpack 透传到礼物背包发放接口。
|
||||||
|
func (g *Gateways) IncrGiftBackpack(ctx context.Context, userID int64, giftID int64, quantity int) error {
|
||||||
|
return g.RewardDispatch.IncrGiftBackpack(ctx, userID, giftID, quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 透传到道具使用切换接口。
|
||||||
|
func (g *Gateways) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
||||||
|
return g.RewardDispatch.SwitchUseProps(ctx, userID, propsID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivateTemporaryBadge 透传到临时徽章激活接口。
|
||||||
|
func (g *Gateways) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
||||||
|
return g.RewardDispatch.ActivateTemporaryBadge(ctx, userID, badgeID, days)
|
||||||
|
}
|
||||||
|
|
||||||
// SendActivityReward 透传到奖励发放网关。
|
// SendActivityReward 透传到奖励发放网关。
|
||||||
func (g *Gateways) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error {
|
func (g *Gateways) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error {
|
||||||
return g.RewardDispatch.SendActivityReward(ctx, req)
|
return g.RewardDispatch.SendActivityReward(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendFrozenActivityReward 透传活动冻结奖励项发放网关。
|
||||||
|
func (g *Gateways) SendFrozenActivityReward(ctx context.Context, req SendFrozenActivityRewardRequest) error {
|
||||||
|
return g.RewardDispatch.SendFrozenActivityReward(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
// GetRewardGroupDetail 透传到奖励查询网关。
|
// GetRewardGroupDetail 透传到奖励查询网关。
|
||||||
func (g *Gateways) GetRewardGroupDetail(ctx context.Context, groupID int64) (RewardGroupDetail, error) {
|
func (g *Gateways) GetRewardGroupDetail(ctx context.Context, groupID int64) (RewardGroupDetail, error) {
|
||||||
return g.RewardQuery.GetRewardGroupDetail(ctx, groupID)
|
return g.RewardQuery.GetRewardGroupDetail(ctx, groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindRewardGroupDetail 透传到奖励查询网关,按名称查找奖励组详情。
|
||||||
|
func (g *Gateways) FindRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (RewardGroupDetail, error) {
|
||||||
|
return g.RewardQuery.FindRewardGroupDetail(ctx, sysOrigin, name)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserProfile 透传到资料网关。
|
// GetUserProfile 透传到资料网关。
|
||||||
func (g *Gateways) GetUserProfile(ctx context.Context, userID int64) (UserProfile, error) {
|
func (g *Gateways) GetUserProfile(ctx context.Context, userID int64) (UserProfile, error) {
|
||||||
return g.Profile.GetUserProfile(ctx, userID)
|
return g.Profile.GetUserProfile(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapUserProfiles 透传批量用户资料查询。
|
||||||
|
func (g *Gateways) MapUserProfiles(ctx context.Context, userIDs []int64) (map[int64]UserProfile, error) {
|
||||||
|
return g.Profile.MapUserProfiles(ctx, userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserMysteriousInvisibility 查询用户是否开启排行榜隐身能力。
|
||||||
|
func (g *Gateways) GetUserMysteriousInvisibility(ctx context.Context, sysOrigin string, userID int64) (bool, error) {
|
||||||
|
return g.Profile.GetUserMysteriousInvisibility(ctx, sysOrigin, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserCountry 透传到 Java 用户国家更新接口。
|
||||||
|
func (g *Gateways) UpdateUserCountry(ctx context.Context, userID int64, countryID int64) error {
|
||||||
|
return g.Profile.UpdateUserCountry(ctx, userID, countryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserRegion 透传到用户地区网关。
|
||||||
|
func (g *Gateways) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
|
||||||
|
return g.Profile.GetUserRegion(ctx, userID, authorization)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserLevel 透传到用户等级网关。
|
||||||
|
func (g *Gateways) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
||||||
|
return g.Profile.GetUserLevel(ctx, sysOrigin, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveRegionCodeByCountryCode 透传到 Java 区域配置网关。
|
||||||
|
func (g *Gateways) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||||
|
return g.Profile.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProfileByAccount 透传到资料网关。
|
||||||
|
func (g *Gateways) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
||||||
|
return g.Profile.GetUserProfileByAccount(ctx, sysOrigin, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProfileByAccountOne 透传到资料网关。
|
||||||
|
func (g *Gateways) GetUserProfileByAccountOne(ctx context.Context, account string) (UserProfile, error) {
|
||||||
|
return g.Profile.GetUserProfileByAccountOne(ctx, account)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserLanguage 透传到资料网关。
|
// GetUserLanguage 透传到资料网关。
|
||||||
func (g *Gateways) GetUserLanguage(ctx context.Context, userID int64) (string, error) {
|
func (g *Gateways) GetUserLanguage(ctx context.Context, userID int64) (string, error) {
|
||||||
return g.Profile.GetUserLanguage(ctx, userID)
|
return g.Profile.GetUserLanguage(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveUserProfileCacheAll 透传到用户资料缓存清理接口。
|
||||||
|
func (g *Gateways) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
|
||||||
|
return g.Profile.RemoveUserProfileCacheAll(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PunishAccount 透传到账号处罚接口。
|
||||||
|
func (g *Gateways) PunishAccount(ctx context.Context, req PunishAccountRequest) error {
|
||||||
|
return g.Profile.PunishAccount(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnblockAccount 透传到账号解封接口。
|
||||||
|
func (g *Gateways) UnblockAccount(ctx context.Context, req UnblockAccountRequest) error {
|
||||||
|
return g.Profile.UnblockAccount(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCurrentWeekRoomContribution 透传到房间流水网关。
|
||||||
|
func (g *Gateways) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
||||||
|
return g.Room.ListCurrentWeekRoomContribution(ctx, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentWeekRoomContribution 透传到房间流水网关。
|
||||||
|
func (g *Gateways) GetCurrentWeekRoomContribution(ctx context.Context, roomID int64) (RoomContributionActivityCount, error) {
|
||||||
|
return g.Room.GetCurrentWeekRoomContribution(ctx, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoomProfileByUserID 透传到房间资料网关。
|
||||||
|
func (g *Gateways) GetRoomProfileByUserID(ctx context.Context, userID int64) (RoomProfile, error) {
|
||||||
|
return g.Room.GetRoomProfileByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapRoomProfiles 透传到房间资料网关。
|
||||||
|
func (g *Gateways) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]RoomProfile, error) {
|
||||||
|
return g.Room.MapRoomProfiles(ctx, roomIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRoomTurnoverRewardCoinsSent 透传到房间流水奖励标记接口。
|
||||||
|
func (g *Gateways) MarkRoomTurnoverRewardCoinsSent(ctx context.Context, id string) (bool, error) {
|
||||||
|
return g.Room.MarkRoomTurnoverRewardCoinsSent(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamPolicyRelease 透传到团队政策网关。
|
||||||
|
func (g *Gateways) GetTeamPolicyRelease(ctx context.Context, query TeamPolicyReleaseQuery) (TeamPolicyRelease, error) {
|
||||||
|
return g.Team.GetTeamPolicyRelease(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
// MapGoldBalance 透传到钱包网关。
|
// MapGoldBalance 透传到钱包网关。
|
||||||
func (g *Gateways) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
func (g *Gateways) MapGoldBalance(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
||||||
return g.Wallet.MapGoldBalance(ctx, userIDs)
|
return g.Wallet.MapGoldBalance(ctx, userIDs)
|
||||||
@ -108,6 +261,31 @@ func (g *Gateways) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand
|
|||||||
return g.Wallet.ChangeGoldBalance(ctx, cmd)
|
return g.Wallet.ChangeGoldBalance(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageFreightSellers 透传到 H5 币商列表接口。
|
||||||
|
func (g *Gateways) PageFreightSellers(ctx context.Context, authorization string, sysOrigin string, cursor int, limit int) (FreightSellerPage, error) {
|
||||||
|
return g.Freight.PageFreightSellers(ctx, authorization, sysOrigin, cursor, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateways) ExistsFreightBalance(ctx context.Context, userID int64) (bool, error) {
|
||||||
|
return g.Freight.ExistsFreightBalance(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateways) ExistsFreightSeller(ctx context.Context, userID int64) (bool, error) {
|
||||||
|
return g.Freight.ExistsFreightSeller(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateways) ChangeFreightBalance(ctx context.Context, req FreightBalanceChangeRequest) error {
|
||||||
|
return g.Freight.ChangeFreightBalance(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateways) AddFreightAgentReview(ctx context.Context, req FreightAgentReviewRequest) error {
|
||||||
|
return g.Freight.AddFreightAgentReview(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gateways) IncreaseFreightRechargeRecord(ctx context.Context, userID int64, amount string) error {
|
||||||
|
return g.Order.IncreaseFreightRechargeRecord(ctx, userID, amount)
|
||||||
|
}
|
||||||
|
|
||||||
// AuthGateway 负责认证相关接口。
|
// AuthGateway 负责认证相关接口。
|
||||||
type AuthGateway struct {
|
type AuthGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
@ -128,6 +306,16 @@ func (g *AuthGateway) AuthenticateConsoleToken(ctx context.Context, authorizatio
|
|||||||
return g.client.AuthenticateConsoleToken(ctx, authorization)
|
return g.client.AuthenticateConsoleToken(ctx, authorization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListConsoleButtonAliases 查询管理员按钮权限码。
|
||||||
|
func (g *AuthGateway) ListConsoleButtonAliases(ctx context.Context, authorization string) (map[string]struct{}, error) {
|
||||||
|
return g.client.ListConsoleButtonAliases(ctx, authorization)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasConsoleMenuAlias 查询管理员页面菜单权限。
|
||||||
|
func (g *AuthGateway) HasConsoleMenuAlias(ctx context.Context, authorization, requiredAlias string) (bool, error) {
|
||||||
|
return g.client.HasConsoleMenuAlias(ctx, authorization, requiredAlias)
|
||||||
|
}
|
||||||
|
|
||||||
// DeviceGateway 负责设备指纹相关接口。
|
// DeviceGateway 负责设备指纹相关接口。
|
||||||
type DeviceGateway struct {
|
type DeviceGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
@ -158,6 +346,25 @@ func (g *NoticeGateway) SendOfficialNoticeCustomize(ctx context.Context, req Off
|
|||||||
return g.client.SendOfficialNoticeCustomize(ctx, req)
|
return g.client.SendOfficialNoticeCustomize(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OrderGateway 负责订单充值统计接口。
|
||||||
|
type OrderGateway struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapUserTotalRecharge 查询用户累计充值分组。
|
||||||
|
func (g *OrderGateway) MapUserTotalRecharge(ctx context.Context, userIDs []int64) (map[int64][]UserTotalRecharge, error) {
|
||||||
|
return g.client.MapUserTotalRecharge(ctx, userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetThisMonthTotalPersonalRecharge 查询用户本月个人充值。
|
||||||
|
func (g *OrderGateway) GetThisMonthTotalPersonalRecharge(ctx context.Context, userID int64) (DecimalString, error) {
|
||||||
|
return g.client.GetThisMonthTotalPersonalRecharge(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *OrderGateway) IncreaseFreightRechargeRecord(ctx context.Context, userID int64, amount string) error {
|
||||||
|
return g.client.IncreaseFreightRechargeRecord(ctx, userID, amount)
|
||||||
|
}
|
||||||
|
|
||||||
// RewardDispatchGateway 负责活动奖励发放接口。
|
// RewardDispatchGateway 负责活动奖励发放接口。
|
||||||
type RewardDispatchGateway struct {
|
type RewardDispatchGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
@ -173,11 +380,36 @@ func (g *RewardDispatchGateway) GrantProps(ctx context.Context, req GrantPropsRe
|
|||||||
return g.client.GrantProps(ctx, req)
|
return g.client.GrantProps(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GivePropsBackpack 发放单个道具到用户背包。
|
||||||
|
func (g *RewardDispatchGateway) GivePropsBackpack(ctx context.Context, req GivePropsBackpackRequest) error {
|
||||||
|
return g.client.GivePropsBackpack(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrGiftBackpack 发放礼物到用户礼物背包。
|
||||||
|
func (g *RewardDispatchGateway) IncrGiftBackpack(ctx context.Context, userID int64, giftID int64, quantity int) error {
|
||||||
|
return g.client.IncrGiftBackpack(ctx, userID, giftID, quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchUseProps 切换用户当前使用道具。
|
||||||
|
func (g *RewardDispatchGateway) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
||||||
|
return g.client.SwitchUseProps(ctx, userID, propsID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivateTemporaryBadge 激活临时徽章并佩戴。
|
||||||
|
func (g *RewardDispatchGateway) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
||||||
|
return g.client.ActivateTemporaryBadge(ctx, userID, badgeID, days)
|
||||||
|
}
|
||||||
|
|
||||||
// SendActivityReward 发放活动奖励组。
|
// SendActivityReward 发放活动奖励组。
|
||||||
func (g *RewardDispatchGateway) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error {
|
func (g *RewardDispatchGateway) SendActivityReward(ctx context.Context, req SendActivityRewardRequest) error {
|
||||||
return g.client.SendActivityReward(ctx, req)
|
return g.client.SendActivityReward(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendFrozenActivityReward 发放一个已经冻结的活动奖励项。
|
||||||
|
func (g *RewardDispatchGateway) SendFrozenActivityReward(ctx context.Context, req SendFrozenActivityRewardRequest) error {
|
||||||
|
return g.client.SendFrozenActivityReward(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
// RewardQueryGateway 负责奖励配置查询接口。
|
// RewardQueryGateway 负责奖励配置查询接口。
|
||||||
type RewardQueryGateway struct {
|
type RewardQueryGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
@ -188,6 +420,21 @@ func (g *RewardQueryGateway) GetRewardGroupDetail(ctx context.Context, groupID i
|
|||||||
return g.client.GetRewardGroupDetail(ctx, groupID)
|
return g.client.GetRewardGroupDetail(ctx, groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindRewardGroupDetail 按名称查询奖励组详情。
|
||||||
|
func (g *RewardQueryGateway) FindRewardGroupDetail(ctx context.Context, sysOrigin string, name string) (RewardGroupDetail, error) {
|
||||||
|
return g.client.FindRewardGroupDetail(ctx, sysOrigin, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
@ -198,11 +445,106 @@ func (g *ProfileGateway) GetUserProfile(ctx context.Context, userID int64) (User
|
|||||||
return g.client.GetUserProfile(ctx, userID)
|
return g.client.GetUserProfile(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapUserProfiles 批量查询用户资料。
|
||||||
|
func (g *ProfileGateway) MapUserProfiles(ctx context.Context, userIDs []int64) (map[int64]UserProfile, error) {
|
||||||
|
return g.client.MapUserProfiles(ctx, userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserMysteriousInvisibility 查询单个用户的排行榜隐身能力。
|
||||||
|
func (g *ProfileGateway) GetUserMysteriousInvisibility(ctx context.Context, sysOrigin string, userID int64) (bool, error) {
|
||||||
|
return g.client.GetUserMysteriousInvisibility(ctx, sysOrigin, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserCountry 通过 Java other 更新用户国家。
|
||||||
|
func (g *ProfileGateway) UpdateUserCountry(ctx context.Context, userID int64, countryID int64) error {
|
||||||
|
return g.client.UpdateUserCountry(ctx, userID, countryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserRegion 查询用户当前地区。
|
||||||
|
func (g *ProfileGateway) GetUserRegion(ctx context.Context, userID int64, authorization string) (UserRegion, error) {
|
||||||
|
return g.client.GetUserRegion(ctx, userID, authorization)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserLevel 查询用户财富/魅力等级。
|
||||||
|
func (g *ProfileGateway) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
||||||
|
return g.client.GetUserLevel(ctx, sysOrigin, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveRegionCodeByCountryCode 根据 Java 区域配置把国家码解析成区域编码。
|
||||||
|
func (g *ProfileGateway) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||||
|
return g.client.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProfileByAccount 根据账号查询用户资料。
|
||||||
|
func (g *ProfileGateway) GetUserProfileByAccount(ctx context.Context, sysOrigin, account string) (UserProfile, error) {
|
||||||
|
return g.client.GetUserProfileByAccount(ctx, sysOrigin, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProfileByAccountOne 根据账号查询单个用户资料。
|
||||||
|
func (g *ProfileGateway) GetUserProfileByAccountOne(ctx context.Context, account string) (UserProfile, error) {
|
||||||
|
return g.client.GetUserProfileByAccountOne(ctx, account)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserLanguage 查询用户语言。
|
// GetUserLanguage 查询用户语言。
|
||||||
func (g *ProfileGateway) GetUserLanguage(ctx context.Context, userID int64) (string, error) {
|
func (g *ProfileGateway) GetUserLanguage(ctx context.Context, userID int64) (string, error) {
|
||||||
return g.client.GetUserLanguage(ctx, userID)
|
return g.client.GetUserLanguage(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveUserProfileCacheAll 清理用户资料相关缓存。
|
||||||
|
func (g *ProfileGateway) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
|
||||||
|
return g.client.RemoveUserProfileCacheAll(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PunishAccount 处罚用户账号。
|
||||||
|
func (g *ProfileGateway) PunishAccount(ctx context.Context, req PunishAccountRequest) error {
|
||||||
|
return g.client.PunishAccount(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnblockAccount 解封用户账号。
|
||||||
|
func (g *ProfileGateway) UnblockAccount(ctx context.Context, req UnblockAccountRequest) error {
|
||||||
|
return g.client.UnblockAccount(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomGateway 负责房间资料和房间流水相关接口。
|
||||||
|
type RoomGateway struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCurrentWeekRoomContribution 查询当前周房间流水排行榜。
|
||||||
|
func (g *RoomGateway) ListCurrentWeekRoomContribution(ctx context.Context, limit int) ([]RoomContributionActivityCount, error) {
|
||||||
|
return g.client.ListCurrentWeekRoomContribution(ctx, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentWeekRoomContribution 查询指定房间当前周流水。
|
||||||
|
func (g *RoomGateway) GetCurrentWeekRoomContribution(ctx context.Context, roomID int64) (RoomContributionActivityCount, error) {
|
||||||
|
return g.client.GetCurrentWeekRoomContribution(ctx, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoomProfileByUserID 查询用户当前房间资料。
|
||||||
|
func (g *RoomGateway) GetRoomProfileByUserID(ctx context.Context, userID int64) (RoomProfile, error) {
|
||||||
|
return g.client.GetRoomProfileByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapRoomProfiles 批量查询房间资料。
|
||||||
|
func (g *RoomGateway) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]RoomProfile, error) {
|
||||||
|
return g.client.MapRoomProfiles(ctx, roomIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRoomTurnoverRewardCoinsSent 标记 Java 房间流水统计记录已经发奖。
|
||||||
|
func (g *RoomGateway) MarkRoomTurnoverRewardCoinsSent(ctx context.Context, id string) (bool, error) {
|
||||||
|
return g.client.MarkRoomTurnoverRewardCoinsSent(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamGateway 负责团队政策相关接口。
|
||||||
|
type TeamGateway struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTeamPolicyRelease 查询已发布团队政策。
|
||||||
|
func (g *TeamGateway) GetTeamPolicyRelease(ctx context.Context, query TeamPolicyReleaseQuery) (TeamPolicyRelease, error) {
|
||||||
|
return g.client.GetTeamPolicyRelease(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
// WalletGateway 负责钱包相关接口。
|
// WalletGateway 负责钱包相关接口。
|
||||||
type WalletGateway struct {
|
type WalletGateway struct {
|
||||||
client *Client
|
client *Client
|
||||||
@ -222,3 +564,29 @@ func (g *WalletGateway) ExistsGoldEvent(ctx context.Context, eventID string) (bo
|
|||||||
func (g *WalletGateway) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand) error {
|
func (g *WalletGateway) ChangeGoldBalance(ctx context.Context, cmd GoldReceiptCommand) error {
|
||||||
return g.client.ChangeGoldBalance(ctx, cmd)
|
return g.client.ChangeGoldBalance(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FreightGateway 负责货运代理/币商相关接口。
|
||||||
|
type FreightGateway struct {
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageFreightSellers 查询 H5 可见币商列表。
|
||||||
|
func (g *FreightGateway) PageFreightSellers(ctx context.Context, authorization string, sysOrigin string, cursor int, limit int) (FreightSellerPage, error) {
|
||||||
|
return g.client.PageFreightSellers(ctx, authorization, sysOrigin, cursor, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FreightGateway) ExistsFreightBalance(ctx context.Context, userID int64) (bool, error) {
|
||||||
|
return g.client.ExistsFreightBalance(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FreightGateway) ExistsFreightSeller(ctx context.Context, userID int64) (bool, error) {
|
||||||
|
return g.client.ExistsFreightSeller(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FreightGateway) ChangeFreightBalance(ctx context.Context, req FreightBalanceChangeRequest) error {
|
||||||
|
return g.client.ChangeFreightBalance(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *FreightGateway) AddFreightAgentReview(ctx context.Context, req FreightAgentReviewRequest) error {
|
||||||
|
return g.client.AddFreightAgentReview(ctx, req)
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
207
internal/integration/java_test.go
Normal file
207
internal/integration/java_test.go
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserTotalRechargeAcceptsNumericTimestamps(t *testing.T) {
|
||||||
|
var item UserTotalRecharge
|
||||||
|
raw := []byte(`{
|
||||||
|
"id":"2049097310624870401",
|
||||||
|
"userId":"2042274349343506434",
|
||||||
|
"type":"SHIPPING_AGENT",
|
||||||
|
"amount":1.60,
|
||||||
|
"createTime":1777377844000,
|
||||||
|
"updateTime":1777556451000
|
||||||
|
}`)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(raw, &item); err != nil {
|
||||||
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
if item.CreateTime != "1777377844000" {
|
||||||
|
t.Fatalf("CreateTime = %q, want numeric timestamp string", item.CreateTime)
|
||||||
|
}
|
||||||
|
if item.UpdateTime != "1777556451000" {
|
||||||
|
t.Fatalf("UpdateTime = %q, want numeric timestamp string", item.UpdateTime)
|
||||||
|
}
|
||||||
|
if item.Amount != "1.60" {
|
||||||
|
t.Fatalf("Amount = %q, want 1.60", item.Amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveRegionCodeByCountryCodeUsesRegionConfig(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls++
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != "/region-config/client/listByCondition" {
|
||||||
|
t.Fatalf("request = %s %s, want POST /region-config/client/listByCondition", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
var payload map[string]string
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
t.Fatalf("decode request: %v", err)
|
||||||
|
}
|
||||||
|
if payload["sysOrigin"] != "LIKEI" {
|
||||||
|
t.Fatalf("sysOrigin = %q, want LIKEI", payload["sysOrigin"])
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"body":[{"regionCode":"中东区","sysOrigin":"LIKEI","countryCodes":"JO, EG, KSA","del":false},{"regionCode":"US","sysOrigin":"LIKEI","countryCodes":"US","del":true}]}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := New(config.Config{
|
||||||
|
HTTP: config.HTTPConfig{Timeout: time.Second},
|
||||||
|
Java: config.JavaConfig{OtherBaseURL: server.URL},
|
||||||
|
})
|
||||||
|
got, err := client.ResolveRegionCodeByCountryCode(context.Background(), " likei ", " ksa ")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveRegionCodeByCountryCode() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != "中东区" {
|
||||||
|
t.Fatalf("region = %q, want 中东区", got)
|
||||||
|
}
|
||||||
|
got, err = client.ResolveRegionCodeByCountryCode(context.Background(), "LIKEI", "eg")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveRegionCodeByCountryCode() cached error = %v", err)
|
||||||
|
}
|
||||||
|
if got != "中东区" {
|
||||||
|
t.Fatalf("cached region = %q, want 中东区", got)
|
||||||
|
}
|
||||||
|
if calls != 1 {
|
||||||
|
t.Fatalf("region config calls = %d, want cached 1", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserCountryPostsOnlyStableIDs(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost || r.URL.Path != "/user-profile/client/updateSelectiveById" {
|
||||||
|
t.Fatalf("request = %s %s, want POST /user-profile/client/updateSelectiveById", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
var payload map[string]json.RawMessage
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
t.Fatalf("decode request: %v", err)
|
||||||
|
}
|
||||||
|
if len(payload) != 2 || string(payload["id"]) != "2" || string(payload["countryId"]) != "11" {
|
||||||
|
t.Fatalf("payload = %s, want only id and countryId", payload)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"success":true}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := New(config.Config{
|
||||||
|
HTTP: config.HTTPConfig{Timeout: time.Second},
|
||||||
|
Java: config.JavaConfig{OtherBaseURL: server.URL},
|
||||||
|
})
|
||||||
|
if err := client.UpdateUserCountry(context.Background(), 2, 11); err != nil {
|
||||||
|
t.Fatalf("UpdateUserCountry() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRewardGroupDetailAcceptsDataWrapper(t *testing.T) {
|
||||||
|
groupID := int64(2056981527522242600)
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet || r.URL.Path != "/props-activity/reward/group/client/getByGroupId" {
|
||||||
|
t.Fatalf("request = %s %s, want GET /props-activity/reward/group/client/getByGroupId", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
if r.URL.Query().Get("id") != "2056981527522242600" {
|
||||||
|
t.Fatalf("id query = %q", r.URL.Query().Get("id"))
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"data":{"id":"2056981527522242600","name":"累计充值10美金","rewardConfigList":[{"id":"1","type":"PROPS","detailType":"AVATAR_FRAME","name":"Frame","quantity":"1","sourceUrl":"https://example.com/frame.svga"}]}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := New(config.Config{
|
||||||
|
HTTP: config.HTTPConfig{Timeout: time.Second},
|
||||||
|
Java: config.JavaConfig{OtherBaseURL: server.URL},
|
||||||
|
})
|
||||||
|
got, err := client.GetRewardGroupDetail(context.Background(), groupID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRewardGroupDetail() error = %v", err)
|
||||||
|
}
|
||||||
|
if int64(got.ID) != groupID || len(got.RewardConfigList) != 1 {
|
||||||
|
t.Fatalf("detail = %+v, want group with one reward item", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRewardGroupDetailFallsBackToConsoleEndpoint(t *testing.T) {
|
||||||
|
groupID := int64(2056981527522242600)
|
||||||
|
var innerCalls, consoleCalls int
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/props-activity/reward/group/client/getByGroupId", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
innerCalls++
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"body":{"id":"2056981527522242600","name":"累计充值10美金","rewardConfigList":[]}}`))
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/console/props/activity/reward/group/2056981527522242600", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
consoleCalls++
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"id":"2056981527522242600","name":"累计充值10美金","rewardConfigList":[{"id":"1","type":"GIFT","name":"Gift","quantity":"1","cover":"https://example.com/gift.png"}]}`))
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := New(config.Config{
|
||||||
|
HTTP: config.HTTPConfig{Timeout: time.Second},
|
||||||
|
Java: config.JavaConfig{
|
||||||
|
OtherBaseURL: server.URL,
|
||||||
|
ConsoleBaseURL: server.URL + "/console",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
got, err := client.GetRewardGroupDetail(context.Background(), groupID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRewardGroupDetail() error = %v", err)
|
||||||
|
}
|
||||||
|
if innerCalls != 1 || consoleCalls != 1 {
|
||||||
|
t.Fatalf("calls inner=%d console=%d, want 1/1", innerCalls, consoleCalls)
|
||||||
|
}
|
||||||
|
if len(got.RewardConfigList) != 1 || got.RewardConfigList[0].Name != "Gift" {
|
||||||
|
t.Fatalf("detail = %+v, want fallback reward item", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRewardGroupDetailHydratesPageRecord(t *testing.T) {
|
||||||
|
groupID := int64(2056980937278816399)
|
||||||
|
var pageCalls, detailCalls int
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/props-activity/reward/group/client/page", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pageCalls++
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
t.Fatalf("method = %s, want POST", r.Method)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"body":{"records":[{"id":"2056980937278816399","name":"累充活动$100","rewardConfigList":[]}]}}`))
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/props-activity/reward/group/client/getByGroupId", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
detailCalls++
|
||||||
|
if r.URL.Query().Get("id") != "2056980937278816399" {
|
||||||
|
t.Fatalf("id query = %q", r.URL.Query().Get("id"))
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"body":{"id":"2056980937278816399","name":"累充活动$100","rewardConfigList":[{"id":"1","type":"BADGE","name":"Badge","quantity":"30","cover":"https://example.com/badge.png"}]}}`))
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := New(config.Config{
|
||||||
|
HTTP: config.HTTPConfig{Timeout: time.Second},
|
||||||
|
Java: config.JavaConfig{OtherBaseURL: server.URL},
|
||||||
|
})
|
||||||
|
got, err := client.FindRewardGroupDetail(context.Background(), "LIKEI", "累充活动$100")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindRewardGroupDetail() error = %v", err)
|
||||||
|
}
|
||||||
|
if pageCalls != 1 || detailCalls != 1 {
|
||||||
|
t.Fatalf("calls page=%d detail=%d, want 1/1", pageCalls, detailCalls)
|
||||||
|
}
|
||||||
|
if int64(got.ID) != groupID || len(got.RewardConfigList) != 1 {
|
||||||
|
t.Fatalf("detail = %+v, want hydrated reward group", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
internal/model/activity_idempotency.go
Normal file
23
internal/model/activity_idempotency.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ActivityIdempotency stores client-side idempotency keys for paid activity draw
|
||||||
|
// requests. The same activity/sysOrigin/user/key may only map to one request hash.
|
||||||
|
type ActivityIdempotency struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
Activity string `gorm:"column:activity;size:64;uniqueIndex:uk_activity_idempotency_key,priority:1"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_activity_idempotency_key,priority:2"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_activity_idempotency_key,priority:3"`
|
||||||
|
IdempotencyKey string `gorm:"column:idempotency_key;size:128;uniqueIndex:uk_activity_idempotency_key,priority:4"`
|
||||||
|
RequestHash string `gorm:"column:request_hash;size:64"`
|
||||||
|
BusinessNo string `gorm:"column:business_no;size:64;index:idx_activity_idempotency_business"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_activity_idempotency_status"`
|
||||||
|
ResponseJSON string `gorm:"column:response_json;type:longtext"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:1024"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the activity idempotency table name.
|
||||||
|
func (ActivityIdempotency) TableName() string { return "activity_idempotency" }
|
||||||
28
internal/model/agora_error_log_models.go
Normal file
28
internal/model/agora_error_log_models.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// AgoraErrorLog stores client-reported Agora room connection errors.
|
||||||
|
type AgoraErrorLog struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"` // 主键ID
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32"` // 系统来源
|
||||||
|
UserID int64 `gorm:"column:user_id"` // 上报用户ID
|
||||||
|
RoomID string `gorm:"column:room_id;size:64"` // 房间ID
|
||||||
|
AgoraUID string `gorm:"column:agora_uid;size:64"` // Agora UID
|
||||||
|
ErrorCodeType string `gorm:"column:error_code_type;size:128"` // Agora ErrorCodeType
|
||||||
|
ConnectionChangedReasonType string `gorm:"column:connection_changed_reason_type;size:128"` // Agora ConnectionChangedReasonType
|
||||||
|
BannedByServer bool `gorm:"column:banned_by_server"` // 是否 BannedByServer/kicking-rule
|
||||||
|
IsTimeout bool `gorm:"column:is_timeout"` // 是否超时
|
||||||
|
TokenRequestSuccess *bool `gorm:"column:token_request_success"` // token 请求是否成功
|
||||||
|
NetworkType string `gorm:"column:network_type;size:64"` // 网络类型
|
||||||
|
ReqClient string `gorm:"column:req_client;size:32"` // 请求客户端
|
||||||
|
ReqAppIntel string `gorm:"column:req_app_intel;size:255"` // App 版本信息请求头
|
||||||
|
ClientIP string `gorm:"column:client_ip;size:64"` // 客户端IP
|
||||||
|
UserAgent string `gorm:"column:user_agent;size:255"` // User-Agent
|
||||||
|
RawPayload string `gorm:"column:raw_payload;type:mediumtext"` // 原始上报内容
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the Agora error log table name.
|
||||||
|
func (AgoraErrorLog) TableName() string { return "agora_error_log" }
|
||||||
32
internal/model/app_popup_models.go
Normal file
32
internal/model/app_popup_models.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// AppPopupConfig 保存 APP 进场弹窗配置。
|
||||||
|
type AppPopupConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_app_popup_config_key,priority:1;index:idx_app_popup_config_scene,priority:1"`
|
||||||
|
PopupKey string `gorm:"column:popup_key;size:64;uniqueIndex:uk_app_popup_config_key,priority:3"`
|
||||||
|
Name string `gorm:"column:name;size:128"`
|
||||||
|
Scene string `gorm:"column:scene;size:32;uniqueIndex:uk_app_popup_config_key,priority:2;index:idx_app_popup_config_scene,priority:2"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_app_popup_config_scene,priority:3"`
|
||||||
|
LimitDays int `gorm:"column:limit_days"`
|
||||||
|
Priority int `gorm:"column:priority;index:idx_app_popup_config_scene,priority:4"`
|
||||||
|
Version int `gorm:"column:version"`
|
||||||
|
StartTime *time.Time `gorm:"column:start_time"`
|
||||||
|
EndTime *time.Time `gorm:"column:end_time"`
|
||||||
|
PlatformsJSON string `gorm:"column:platforms_json;type:json"`
|
||||||
|
MinAppVersion string `gorm:"column:min_app_version;size:32"`
|
||||||
|
MaxAppVersion string `gorm:"column:max_app_version;size:32"`
|
||||||
|
UserScopeType string `gorm:"column:user_scope_type;size:32"`
|
||||||
|
UserScopeJSON string `gorm:"column:user_scope_json;type:json"`
|
||||||
|
ContentJSON string `gorm:"column:content_json;type:json"`
|
||||||
|
JumpType string `gorm:"column:jump_type;size:32"`
|
||||||
|
JumpURL string `gorm:"column:jump_url;size:512"`
|
||||||
|
Remark string `gorm:"column:remark;size:255"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 返回 APP 进场弹窗配置表名。
|
||||||
|
func (AppPopupConfig) TableName() string { return "app_popup_config" }
|
||||||
50
internal/model/badge_models.go
Normal file
50
internal/model/badge_models.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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"`
|
||||||
|
DisplayType string `gorm:"column:display_type;size:16"`
|
||||||
|
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" }
|
||||||
@ -30,9 +30,10 @@ func (SysGameListConfig) TableName() string { return "sys_game_list_config" }
|
|||||||
type SysGameListVendorExt struct {
|
type SysGameListVendorExt struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||||
GameListConfigID int64 `gorm:"column:game_list_config_id;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:1"` // 关联系统游戏配置 ID
|
GameListConfigID int64 `gorm:"column:game_list_config_id;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:1"` // 关联系统游戏配置 ID
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_game_vendor_ext_sys_vendor,priority:1"` // 系统标识
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_game_vendor_ext_sys_vendor,priority:1;index:idx_game_vendor_ext_sys_profile_vendor,priority:1"` // 系统标识
|
||||||
VendorType string `gorm:"column:vendor_type;size:32;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:2;index:idx_game_vendor_ext_sys_vendor,priority:2"` // 厂商类型
|
Profile string `gorm:"column:profile;size:32;default:PROD;index:idx_game_vendor_ext_sys_profile_vendor,priority:2"` // 配置档案:PROD/TEST
|
||||||
VendorGameID string `gorm:"column:vendor_game_id;size:64"` // 厂商游戏 ID
|
VendorType string `gorm:"column:vendor_type;size:32;uniqueIndex:uk_game_vendor_ext_cfg_vendor,priority:2;index:idx_game_vendor_ext_sys_vendor,priority:2;index:idx_game_vendor_ext_sys_profile_vendor,priority:3"` // 厂商类型
|
||||||
|
VendorGameID string `gorm:"column:vendor_game_id;size:64;index:idx_game_vendor_ext_sys_profile_vendor,priority:4"` // 厂商游戏 ID
|
||||||
LaunchMode string `gorm:"column:launch_mode;size:32"` // 启动模式
|
LaunchMode string `gorm:"column:launch_mode;size:32"` // 启动模式
|
||||||
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本号
|
PackageVersion string `gorm:"column:package_version;size:32"` // 包版本号
|
||||||
PackageURL string `gorm:"column:package_url;size:1024"` // 包下载地址
|
PackageURL string `gorm:"column:package_url;size:1024"` // 包下载地址
|
||||||
@ -44,7 +45,7 @@ type SysGameListVendorExt struct {
|
|||||||
CurrencyType *int `gorm:"column:currency_type"` // 货币类型
|
CurrencyType *int `gorm:"column:currency_type"` // 货币类型
|
||||||
CurrencyIcon string `gorm:"column:currency_icon;size:1024"` // 货币图标
|
CurrencyIcon string `gorm:"column:currency_icon;size:1024"` // 货币图标
|
||||||
ExtraJSON string `gorm:"column:extra_json;type:text"` // 额外扩展配置
|
ExtraJSON string `gorm:"column:extra_json;type:text"` // 额外扩展配置
|
||||||
Enabled bool `gorm:"column:enabled;index:idx_game_vendor_ext_sys_vendor,priority:3"` // 是否启用
|
Enabled bool `gorm:"column:enabled;index:idx_game_vendor_ext_sys_vendor,priority:3;index:idx_game_vendor_ext_sys_profile_vendor,priority:5"` // 是否启用
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||||
}
|
}
|
||||||
@ -55,8 +56,12 @@ func (SysGameListVendorExt) TableName() string { return "sys_game_list_vendor_ex
|
|||||||
// BaishunProviderConfig 保存每个系统的百顺接入配置。
|
// BaishunProviderConfig 保存每个系统的百顺接入配置。
|
||||||
type BaishunProviderConfig struct {
|
type BaishunProviderConfig struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_provider_sys_origin"` // 系统标识
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_provider_sys_origin_profile,priority:1;index:idx_baishun_provider_active,priority:1"` // 系统标识
|
||||||
|
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_baishun_provider_sys_origin_profile,priority:2"` // 配置档案:PROD/TEST
|
||||||
|
Active bool `gorm:"column:active;index:idx_baishun_provider_active,priority:2"` // 是否当前启用
|
||||||
PlatformBaseURL string `gorm:"column:platform_base_url;size:1024"` // 百顺平台基础地址
|
PlatformBaseURL string `gorm:"column:platform_base_url;size:1024"` // 百顺平台基础地址
|
||||||
|
GameListURL string `gorm:"column:game_list_url;size:1024"` // 百顺游戏列表完整地址
|
||||||
|
LuckyGiftURL string `gorm:"column:lucky_gift_url;size:1024"` // 幸运礼物完整地址
|
||||||
AppID int64 `gorm:"column:app_id;index:idx_baishun_provider_app,priority:1"` // 百顺应用 ID
|
AppID int64 `gorm:"column:app_id;index:idx_baishun_provider_app,priority:1"` // 百顺应用 ID
|
||||||
AppName string `gorm:"column:app_name;size:128"` // 百顺应用名
|
AppName string `gorm:"column:app_name;size:128"` // 百顺应用名
|
||||||
AppChannel string `gorm:"column:app_channel;size:64;index:idx_baishun_provider_app,priority:2"` // 百顺渠道
|
AppChannel string `gorm:"column:app_channel;size:64;index:idx_baishun_provider_app,priority:2"` // 百顺渠道
|
||||||
@ -65,7 +70,7 @@ type BaishunProviderConfig struct {
|
|||||||
LaunchCodeTTLSeconds int `gorm:"column:launch_code_ttl_seconds"` // 启动码过期秒数
|
LaunchCodeTTLSeconds int `gorm:"column:launch_code_ttl_seconds"` // 启动码过期秒数
|
||||||
SSTokenTTLSeconds int `gorm:"column:ss_token_ttl_seconds"` // ss_token 过期秒数
|
SSTokenTTLSeconds int `gorm:"column:ss_token_ttl_seconds"` // ss_token 过期秒数
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
UpdateTime time.Time `gorm:"column:update_time;index:idx_baishun_provider_active,priority:3"` // 更新时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 返回百顺配置表名。
|
// TableName 返回百顺配置表名。
|
||||||
@ -74,9 +79,10 @@ func (BaishunProviderConfig) TableName() string { return "baishun_provider_confi
|
|||||||
// BaishunGameCatalog 是百顺平台同步到本地的游戏目录。
|
// BaishunGameCatalog 是百顺平台同步到本地的游戏目录。
|
||||||
type BaishunGameCatalog struct {
|
type BaishunGameCatalog struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_catalog_sys_vendor_game,priority:1;index:idx_baishun_catalog_internal_game,priority:1"` // 系统标识
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_baishun_catalog_sys_profile_vendor_game,priority:1;index:idx_baishun_catalog_profile_internal_game,priority:1"` // 系统标识
|
||||||
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_baishun_catalog_internal_game,priority:2"` // 内部游戏 ID
|
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_baishun_catalog_sys_profile_vendor_game,priority:2;index:idx_baishun_catalog_profile_internal_game,priority:2"` // 配置档案:PROD/TEST
|
||||||
VendorGameID int `gorm:"column:vendor_game_id;uniqueIndex:uk_baishun_catalog_sys_vendor_game,priority:2"` // 百顺游戏 ID
|
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_baishun_catalog_profile_internal_game,priority:3"` // 内部游戏 ID
|
||||||
|
VendorGameID int `gorm:"column:vendor_game_id;uniqueIndex:uk_baishun_catalog_sys_profile_vendor_game,priority:3"` // 百顺游戏 ID
|
||||||
Name string `gorm:"column:name;size:128"` // 游戏名称
|
Name string `gorm:"column:name;size:128"` // 游戏名称
|
||||||
Cover string `gorm:"column:cover;size:1024"` // 封面图
|
Cover string `gorm:"column:cover;size:1024"` // 封面图
|
||||||
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
|
PreviewURL string `gorm:"column:preview_url;size:1024"` // 预览地址
|
||||||
@ -88,7 +94,7 @@ type BaishunGameCatalog struct {
|
|||||||
VenueLevelJSON string `gorm:"column:venue_level_json;size:64"` // 场馆等级 JSON
|
VenueLevelJSON string `gorm:"column:venue_level_json;size:64"` // 场馆等级 JSON
|
||||||
GSP string `gorm:"column:gsp;size:64"` // GSP 配置
|
GSP string `gorm:"column:gsp;size:64"` // GSP 配置
|
||||||
RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录数据
|
RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录数据
|
||||||
Status string `gorm:"column:status;size:32;index:idx_baishun_catalog_internal_game,priority:3"` // 状态
|
Status string `gorm:"column:status;size:32;index:idx_baishun_catalog_profile_internal_game,priority:4"` // 状态
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||||
}
|
}
|
||||||
@ -96,6 +102,45 @@ type BaishunGameCatalog struct {
|
|||||||
// TableName 返回百顺目录表名。
|
// TableName 返回百顺目录表名。
|
||||||
func (BaishunGameCatalog) TableName() string { return "baishun_game_catalog" }
|
func (BaishunGameCatalog) TableName() string { return "baishun_game_catalog" }
|
||||||
|
|
||||||
|
// LingxianProviderConfig 保存每个系统的灵仙接入配置。
|
||||||
|
type LingxianProviderConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_lingxian_provider_sys_origin_profile,priority:1;index:idx_lingxian_provider_active,priority:1"`
|
||||||
|
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_lingxian_provider_sys_origin_profile,priority:2"`
|
||||||
|
Active bool `gorm:"column:active;index:idx_lingxian_provider_active,priority:2"`
|
||||||
|
GameListURL string `gorm:"column:game_list_url;size:1024"`
|
||||||
|
AppKey string `gorm:"column:app_key;size:255"`
|
||||||
|
TestUID string `gorm:"column:test_uid;size:64"`
|
||||||
|
TestToken string `gorm:"column:test_token;size:1024"`
|
||||||
|
TestRoomID string `gorm:"column:test_room_id;size:64"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time;index:idx_lingxian_provider_active,priority:3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 返回灵仙配置表名。
|
||||||
|
func (LingxianProviderConfig) TableName() string { return "lingxian_provider_config" }
|
||||||
|
|
||||||
|
// LingxianGameCatalog 是灵仙平台同步到本地的游戏目录。
|
||||||
|
type LingxianGameCatalog struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_lingxian_catalog_sys_profile_vendor_game,priority:1;index:idx_lingxian_catalog_profile_internal_game,priority:1"`
|
||||||
|
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_lingxian_catalog_sys_profile_vendor_game,priority:2;index:idx_lingxian_catalog_profile_internal_game,priority:2"`
|
||||||
|
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_lingxian_catalog_profile_internal_game,priority:3"`
|
||||||
|
VendorGameID string `gorm:"column:vendor_game_id;size:64;uniqueIndex:uk_lingxian_catalog_sys_profile_vendor_game,priority:3"`
|
||||||
|
Name string `gorm:"column:name;size:128"`
|
||||||
|
Cover string `gorm:"column:cover;size:1024"`
|
||||||
|
PreviewURL string `gorm:"column:preview_url;size:1024"`
|
||||||
|
DownloadURL string `gorm:"column:download_url;size:1024"`
|
||||||
|
PackageVersion string `gorm:"column:package_version;size:32"`
|
||||||
|
RawJSON string `gorm:"column:raw_json;type:longtext"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_lingxian_catalog_profile_internal_game,priority:4"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 返回灵仙目录表名。
|
||||||
|
func (LingxianGameCatalog) TableName() string { return "lingxian_game_catalog" }
|
||||||
|
|
||||||
// BaishunLaunchSession 保存一次房间内游戏启动会话。
|
// BaishunLaunchSession 保存一次房间内游戏启动会话。
|
||||||
type BaishunLaunchSession struct {
|
type BaishunLaunchSession struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 会话主键
|
ID int64 `gorm:"column:id;primaryKey"` // 会话主键
|
||||||
|
|||||||
60
internal/model/binance_recharge_models.go
Normal file
60
internal/model/binance_recharge_models.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// BinanceRechargeConfig stores Binance API credentials per app origin.
|
||||||
|
type BinanceRechargeConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_binance_recharge_config_origin"`
|
||||||
|
APIKey string `gorm:"column:api_key;size:255"`
|
||||||
|
APISecret string `gorm:"column:api_secret;size:255"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BinanceRechargeConfig) TableName() string {
|
||||||
|
return "binance_recharge_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinanceRechargeRate maps a verified USDT amount range to gold per USD.
|
||||||
|
type BinanceRechargeRate struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_binance_recharge_rate_config"`
|
||||||
|
MinAmount string `gorm:"column:min_amount;type:decimal(20,8)"`
|
||||||
|
MaxAmount string `gorm:"column:max_amount;type:decimal(20,8)"`
|
||||||
|
GoldPerUSD string `gorm:"column:gold_per_usd;type:decimal(20,8)"`
|
||||||
|
SortOrder int `gorm:"column:sort_order"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BinanceRechargeRate) TableName() string {
|
||||||
|
return "binance_recharge_rate"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinanceRechargeVerifyRecord is the local idempotency ledger for auto recharge verification.
|
||||||
|
type BinanceRechargeVerifyRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_binance_recharge_verify_origin;uniqueIndex:uk_binance_recharge_verify_order,priority:1;uniqueIndex:uk_binance_recharge_verify_bill,priority:1"`
|
||||||
|
DealerUserID int64 `gorm:"column:dealer_user_id;index:idx_binance_recharge_verify_dealer"`
|
||||||
|
TargetUserID int64 `gorm:"column:target_user_id;index:idx_binance_recharge_verify_target"`
|
||||||
|
OrderNo string `gorm:"column:order_no;size:128;uniqueIndex:uk_binance_recharge_verify_order,priority:2"`
|
||||||
|
TransferType string `gorm:"column:transfer_type;size:32;uniqueIndex:uk_binance_recharge_verify_bill,priority:2"`
|
||||||
|
BillNo string `gorm:"column:bill_no;size:160;uniqueIndex:uk_binance_recharge_verify_bill,priority:3"`
|
||||||
|
AmountUSDT string `gorm:"column:amount_usdt;type:decimal(20,8)"`
|
||||||
|
GoldAmount int64 `gorm:"column:gold_amount"`
|
||||||
|
RateGoldPerUSD string `gorm:"column:rate_gold_per_usd;type:decimal(20,8)"`
|
||||||
|
BinanceTransactionID string `gorm:"column:binance_transaction_id;size:180"`
|
||||||
|
BinanceOrderType string `gorm:"column:binance_order_type;size:64"`
|
||||||
|
BinanceRawJSON string `gorm:"column:binance_raw_json;type:text"`
|
||||||
|
Status string `gorm:"column:status;size:24;index:idx_binance_recharge_verify_status"`
|
||||||
|
FailReason string `gorm:"column:fail_reason;size:500"`
|
||||||
|
Remark string `gorm:"column:remark;size:500"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (BinanceRechargeVerifyRecord) TableName() string {
|
||||||
|
return "binance_recharge_verify_record"
|
||||||
|
}
|
||||||
169
internal/model/decimal24_2.go
Normal file
169
internal/model/decimal24_2.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decimal24_2 用规范十进制文本承载 MySQL DECIMAL(24,2),避免金额经过 float64 丢精度。
|
||||||
|
// JSON 输出字符串以保护 22 位整数精度;数据库读写通过 Scanner/Valuer 完成。
|
||||||
|
type Decimal24_2 string
|
||||||
|
|
||||||
|
func ParseDecimal24_2(value string) (Decimal24_2, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return "", fmt.Errorf("decimal is blank")
|
||||||
|
}
|
||||||
|
negative := false
|
||||||
|
if value[0] == '-' {
|
||||||
|
negative, value = true, value[1:]
|
||||||
|
} else if value[0] == '+' {
|
||||||
|
return "", fmt.Errorf("decimal plus sign is not supported")
|
||||||
|
}
|
||||||
|
if value == "" || strings.ContainsAny(value, "eE") {
|
||||||
|
return "", fmt.Errorf("invalid decimal")
|
||||||
|
}
|
||||||
|
parts := strings.Split(value, ".")
|
||||||
|
if len(parts) > 2 || parts[0] == "" {
|
||||||
|
return "", fmt.Errorf("invalid decimal")
|
||||||
|
}
|
||||||
|
for _, char := range parts[0] {
|
||||||
|
if char < '0' || char > '9' {
|
||||||
|
return "", fmt.Errorf("invalid decimal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
integer := strings.TrimLeft(parts[0], "0")
|
||||||
|
if integer == "" {
|
||||||
|
integer = "0"
|
||||||
|
}
|
||||||
|
fraction := ""
|
||||||
|
if len(parts) == 2 {
|
||||||
|
fraction = parts[1]
|
||||||
|
for _, char := range fraction {
|
||||||
|
if char < '0' || char > '9' {
|
||||||
|
return "", fmt.Errorf("invalid decimal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// BigDecimal.stripTrailingZeros 后 scale<=2 的值均可接受,例如 1.2300。
|
||||||
|
if len(fraction) > 2 && strings.Trim(fraction[2:], "0") != "" {
|
||||||
|
return "", fmt.Errorf("decimal scale exceeds 2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(integer) > 22 {
|
||||||
|
return "", fmt.Errorf("decimal integer digits exceed 22")
|
||||||
|
}
|
||||||
|
if len(fraction) > 2 {
|
||||||
|
fraction = fraction[:2]
|
||||||
|
}
|
||||||
|
fraction += strings.Repeat("0", 2-len(fraction))
|
||||||
|
if integer == "0" && fraction == "00" {
|
||||||
|
negative = false
|
||||||
|
}
|
||||||
|
prefix := ""
|
||||||
|
if negative {
|
||||||
|
prefix = "-"
|
||||||
|
}
|
||||||
|
return Decimal24_2(prefix + integer + "." + fraction), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decimal24_2) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := strings.TrimSpace(string(data))
|
||||||
|
if raw == "" || raw == "null" || raw == `""` {
|
||||||
|
*d = DecimalZero()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, `"`) && strings.HasSuffix(raw, `"`) {
|
||||||
|
var value string
|
||||||
|
if err := json.Unmarshal(data, &value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw = value
|
||||||
|
}
|
||||||
|
parsed, err := ParseDecimal24_2(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Decimal24_2) MarshalJSON() ([]byte, error) { return json.Marshal(d.String()) }
|
||||||
|
|
||||||
|
func (d *Decimal24_2) Scan(value any) error {
|
||||||
|
var raw string
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case nil:
|
||||||
|
raw = "0"
|
||||||
|
case []byte:
|
||||||
|
raw = string(typed)
|
||||||
|
case string:
|
||||||
|
raw = typed
|
||||||
|
case int64:
|
||||||
|
raw = fmt.Sprintf("%d", typed)
|
||||||
|
default:
|
||||||
|
raw = fmt.Sprint(typed)
|
||||||
|
}
|
||||||
|
parsed, err := ParseDecimal24_2(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Decimal24_2) Value() (driver.Value, error) { return d.String(), nil }
|
||||||
|
|
||||||
|
func (d Decimal24_2) String() string {
|
||||||
|
if strings.TrimSpace(string(d)) == "" {
|
||||||
|
return "0.00"
|
||||||
|
}
|
||||||
|
return string(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecimalZero() Decimal24_2 { return Decimal24_2("0.00") }
|
||||||
|
|
||||||
|
func (d Decimal24_2) Positive() bool { return d.cents().Sign() > 0 }
|
||||||
|
|
||||||
|
func (d Decimal24_2) Compare(other Decimal24_2) int { return d.cents().Cmp(other.cents()) }
|
||||||
|
|
||||||
|
func (d Decimal24_2) MultiplyInt(multiplier int64) (Decimal24_2, error) {
|
||||||
|
if multiplier < 0 {
|
||||||
|
return "", fmt.Errorf("decimal multiplier must not be negative")
|
||||||
|
}
|
||||||
|
value := new(big.Int).Mul(d.cents(), big.NewInt(multiplier))
|
||||||
|
if len(new(big.Int).Abs(new(big.Int).Set(value)).String()) > 24 {
|
||||||
|
return "", fmt.Errorf("decimal exceeds DECIMAL(24,2)")
|
||||||
|
}
|
||||||
|
return decimalFromCents(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Decimal24_2) cents() *big.Int {
|
||||||
|
raw := d.String()
|
||||||
|
negative := strings.HasPrefix(raw, "-")
|
||||||
|
raw = strings.TrimPrefix(raw, "-")
|
||||||
|
digits := strings.ReplaceAll(raw, ".", "")
|
||||||
|
value, ok := new(big.Int).SetString(digits, 10)
|
||||||
|
if !ok {
|
||||||
|
return new(big.Int)
|
||||||
|
}
|
||||||
|
if negative {
|
||||||
|
value.Neg(value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func decimalFromCents(value *big.Int) Decimal24_2 {
|
||||||
|
negative := value.Sign() < 0
|
||||||
|
abs := new(big.Int).Abs(new(big.Int).Set(value)).String()
|
||||||
|
if len(abs) < 3 {
|
||||||
|
abs = strings.Repeat("0", 3-len(abs)) + abs
|
||||||
|
}
|
||||||
|
result := abs[:len(abs)-2] + "." + abs[len(abs)-2:]
|
||||||
|
if negative && result != "0.00" {
|
||||||
|
result = "-" + result
|
||||||
|
}
|
||||||
|
return Decimal24_2(result)
|
||||||
|
}
|
||||||
68
internal/model/first_recharge_reward_models.go
Normal file
68
internal/model/first_recharge_reward_models.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// FirstRechargeRewardConfig stores first recharge reward config per system.
|
||||||
|
type FirstRechargeRewardConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_first_recharge_reward_sys_origin"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_first_recharge_reward_enabled"`
|
||||||
|
MinAppVersion string `gorm:"column:min_app_version;size:32"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (FirstRechargeRewardConfig) TableName() string { return "first_recharge_reward_config" }
|
||||||
|
|
||||||
|
// FirstRechargeRewardLevel stores a recharge threshold and reward resource group.
|
||||||
|
type FirstRechargeRewardLevel struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;uniqueIndex:uk_first_recharge_reward_level,priority:1;uniqueIndex:uk_first_recharge_reward_amount,priority:1;index:idx_first_recharge_reward_level_config"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_first_recharge_reward_level,priority:2"`
|
||||||
|
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents;uniqueIndex:uk_first_recharge_reward_amount,priority:2"`
|
||||||
|
GoogleProductID string `gorm:"column:google_product_id;size:128;index:idx_first_recharge_reward_level_google_product"`
|
||||||
|
RewardGroupID int64 `gorm:"column:reward_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (FirstRechargeRewardLevel) TableName() string { return "first_recharge_reward_level" }
|
||||||
|
|
||||||
|
// FirstRechargeRewardGrantRecord stores the actual first recharge reward grant result.
|
||||||
|
type FirstRechargeRewardGrantRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_first_recharge_reward_event"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id"`
|
||||||
|
LevelID int64 `gorm:"column:level_id"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_first_recharge_reward_user,priority:1;index:idx_first_recharge_reward_status,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_first_recharge_reward_user,priority:2;index:idx_first_recharge_reward_status,priority:2"`
|
||||||
|
Account string `gorm:"column:account;size:64"`
|
||||||
|
UserAvatar string `gorm:"column:user_avatar;size:1024"`
|
||||||
|
UserNickname string `gorm:"column:user_nickname;size:255"`
|
||||||
|
CountryCode string `gorm:"column:country_code;size:32"`
|
||||||
|
CountryName string `gorm:"column:country_name;size:128"`
|
||||||
|
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents"`
|
||||||
|
RechargeThresholdCents int64 `gorm:"column:recharge_threshold_cents"`
|
||||||
|
Level int `gorm:"column:level"`
|
||||||
|
PayPlatform string `gorm:"column:pay_platform;size:64"`
|
||||||
|
PaymentMethod string `gorm:"column:payment_method;size:32"`
|
||||||
|
GoogleProductID string `gorm:"column:google_product_id;size:128;index:idx_first_recharge_reward_grant_google_product"`
|
||||||
|
SourceOrderID string `gorm:"column:source_order_id;size:128;index:idx_first_recharge_reward_order"`
|
||||||
|
RewardGroupID int64 `gorm:"column:reward_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
|
||||||
|
RewardGroupTrackID int64 `gorm:"column:reward_group_track_id"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_first_recharge_reward_status,priority:3"`
|
||||||
|
RewardGroupStatus string `gorm:"column:reward_group_status;size:32"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
LastError string `gorm:"column:last_error;size:1024"`
|
||||||
|
EventTime time.Time `gorm:"column:event_time"`
|
||||||
|
SentAt *time.Time `gorm:"column:sent_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (FirstRechargeRewardGrantRecord) TableName() string {
|
||||||
|
return "first_recharge_reward_grant_record"
|
||||||
|
}
|
||||||
41
internal/model/hotgame_models.go
Normal file
41
internal/model/hotgame_models.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// HotgameProviderConfig 保存每个系统、每个环境的热游回调验签配置。
|
||||||
|
type HotgameProviderConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_hotgame_provider_sys_origin_profile,priority:1;index:idx_hotgame_provider_active,priority:1"` // 系统标识
|
||||||
|
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_hotgame_provider_sys_origin_profile,priority:2"` // 配置环境:PROD/TEST
|
||||||
|
Active bool `gorm:"column:active;index:idx_hotgame_provider_active,priority:2"` // app 当前启用环境
|
||||||
|
AppKey string `gorm:"column:app_key;size:255"` // 热游回调 MD5 key
|
||||||
|
CallbackBaseURL string `gorm:"column:callback_base_url;size:1024"` // 提供给热游配置的回调域名
|
||||||
|
TestUID string `gorm:"column:test_uid;size:64"` // 联调用 UID 覆盖
|
||||||
|
TestToken string `gorm:"column:test_token;size:1024"` // 联调用 token 覆盖
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time;index:idx_hotgame_provider_active,priority:3"` // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 返回热游接入配置表名。
|
||||||
|
func (HotgameProviderConfig) TableName() string { return "hotgame_provider_config" }
|
||||||
|
|
||||||
|
// HotgameGameCatalog 保存从热游对接表导入的游戏目录。
|
||||||
|
type HotgameGameCatalog struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"` // 主键
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_hotgame_catalog_sys_profile_vendor_game,priority:1;index:idx_hotgame_catalog_profile_internal_game,priority:1"` // 系统标识
|
||||||
|
Profile string `gorm:"column:profile;size:32;default:PROD;uniqueIndex:uk_hotgame_catalog_sys_profile_vendor_game,priority:2;index:idx_hotgame_catalog_profile_internal_game,priority:2"` // 配置环境:PROD/TEST
|
||||||
|
InternalGameID string `gorm:"column:internal_game_id;size:64;index:idx_hotgame_catalog_profile_internal_game,priority:3"` // app 内部游戏 ID
|
||||||
|
VendorGameID string `gorm:"column:vendor_game_id;size:64;uniqueIndex:uk_hotgame_catalog_sys_profile_vendor_game,priority:3"` // 热游游戏 ID
|
||||||
|
Name string `gorm:"column:name;size:128"` // 游戏名称
|
||||||
|
Cover string `gorm:"column:cover;size:1024"` // 封面地址
|
||||||
|
PreviewURL string `gorm:"column:preview_url;size:1024"` // 七分屏/预览入口
|
||||||
|
DownloadURL string `gorm:"column:download_url;size:1024"` // 实际启动入口
|
||||||
|
PackageVersion string `gorm:"column:package_version;size:32"` // 版本号
|
||||||
|
RawJSON string `gorm:"column:raw_json;type:longtext"` // 原始目录 JSON
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_hotgame_catalog_profile_internal_game,priority:4"` // 目录状态
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 返回热游目录表名。
|
||||||
|
func (HotgameGameCatalog) TableName() string { return "hotgame_game_catalog" }
|
||||||
@ -79,12 +79,12 @@ type InviteCampaignInviteeProgress struct {
|
|||||||
// TableName 返回被邀请人进度表名。
|
// TableName 返回被邀请人进度表名。
|
||||||
func (InviteCampaignInviteeProgress) TableName() string { return "invite_campaign_invitee_progress" }
|
func (InviteCampaignInviteeProgress) TableName() string { return "invite_campaign_invitee_progress" }
|
||||||
|
|
||||||
// InviteCampaignMonthlyProgress 汇总邀请人月度邀请进展。
|
// InviteCampaignMonthlyProgress 汇总邀请人当前任务周期邀请进展。
|
||||||
type InviteCampaignMonthlyProgress struct {
|
type InviteCampaignMonthlyProgress struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"` // 统计主键
|
ID int64 `gorm:"column:id;primaryKey"` // 统计主键
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_monthly_progress,priority:1"` // 系统标识
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_monthly_progress,priority:1"` // 系统标识
|
||||||
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_monthly_progress,priority:2"` // 邀请人用户 ID
|
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_monthly_progress,priority:2"` // 邀请人用户 ID
|
||||||
MonthKey string `gorm:"column:month_key;size:6;uniqueIndex:uk_invite_monthly_progress,priority:3"` // 月份标识
|
MonthKey string `gorm:"column:month_key;size:16;uniqueIndex:uk_invite_monthly_progress,priority:3"` // 任务周期标识
|
||||||
InviteCount int64 `gorm:"column:invite_count"` // 邀请人数
|
InviteCount int64 `gorm:"column:invite_count"` // 邀请人数
|
||||||
ValidUserCount int64 `gorm:"column:valid_user_count"` // 有效用户数
|
ValidUserCount int64 `gorm:"column:valid_user_count"` // 有效用户数
|
||||||
TotalRecharge int64 `gorm:"column:total_recharge_coins"` // 累计充值金币
|
TotalRecharge int64 `gorm:"column:total_recharge_coins"` // 累计充值金币
|
||||||
@ -93,7 +93,7 @@ type InviteCampaignMonthlyProgress struct {
|
|||||||
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
UpdateTime time.Time `gorm:"column:update_time"` // 更新时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 返回邀请月度进度表名。
|
// TableName 返回邀请任务周期进度表名。
|
||||||
func (InviteCampaignMonthlyProgress) TableName() string { return "invite_campaign_monthly_progress" }
|
func (InviteCampaignMonthlyProgress) TableName() string { return "invite_campaign_monthly_progress" }
|
||||||
|
|
||||||
// InviteCampaignTaskClaim 记录任务奖励领取行为,防止重复领取。
|
// InviteCampaignTaskClaim 记录任务奖励领取行为,防止重复领取。
|
||||||
@ -101,7 +101,7 @@ type InviteCampaignTaskClaim struct {
|
|||||||
ID int64 `gorm:"column:id;primaryKey"` // 领取记录主键
|
ID int64 `gorm:"column:id;primaryKey"` // 领取记录主键
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_task_claim,priority:1"` // 系统标识
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_task_claim,priority:1"` // 系统标识
|
||||||
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_task_claim,priority:2"` // 邀请人用户 ID
|
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_task_claim,priority:2"` // 邀请人用户 ID
|
||||||
MonthKey string `gorm:"column:month_key;size:6;uniqueIndex:uk_invite_task_claim,priority:3"` // 月份标识
|
MonthKey string `gorm:"column:month_key;size:16;uniqueIndex:uk_invite_task_claim,priority:3"` // 任务周期标识
|
||||||
RuleID int64 `gorm:"column:rule_id;uniqueIndex:uk_invite_task_claim,priority:4"` // 规则 ID
|
RuleID int64 `gorm:"column:rule_id;uniqueIndex:uk_invite_task_claim,priority:4"` // 规则 ID
|
||||||
ClaimTime time.Time `gorm:"column:claim_time"` // 领取时间
|
ClaimTime time.Time `gorm:"column:claim_time"` // 领取时间
|
||||||
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
CreateTime time.Time `gorm:"column:create_time"` // 创建时间
|
||||||
|
|||||||
25
internal/model/props_models.go
Normal file
25
internal/model/props_models.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// PropsCommodityStore stores one props store commodity row.
|
||||||
|
type PropsCommodityStore struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_props_store_source,priority:1"`
|
||||||
|
SourceID int64 `gorm:"column:source_id;index:idx_props_store_source,priority:2"`
|
||||||
|
PropsType string `gorm:"column:props_type;size:64"`
|
||||||
|
CurrencyTypes string `gorm:"column:currency_types;size:128"`
|
||||||
|
ValidDays string `gorm:"column:valid_days;size:128"`
|
||||||
|
Discount *float64 `gorm:"column:discount;type:decimal(10,4)"`
|
||||||
|
ShelfStatus *bool `gorm:"column:shelf_status"`
|
||||||
|
Sort int `gorm:"column:sort"`
|
||||||
|
Label string `gorm:"column:label;size:255"`
|
||||||
|
Del *bool `gorm:"column:is_del"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the props store commodity table name.
|
||||||
|
func (PropsCommodityStore) TableName() string { return "props_commodity_store" }
|
||||||
17
internal/model/recharge_agency_models.go
Normal file
17
internal/model/recharge_agency_models.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// RechargeAgencySellerInfo stores H5-only profile fields for recharge agency sellers.
|
||||||
|
type RechargeAgencySellerInfo struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_recharge_agency_seller_info_user,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_recharge_agency_seller_info_user,priority:2"`
|
||||||
|
Whatsapp string `gorm:"column:whatsapp;size:64"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RechargeAgencySellerInfo) TableName() string {
|
||||||
|
return "recharge_agency_seller_info"
|
||||||
|
}
|
||||||
67
internal/model/recharge_reward_models.go
Normal file
67
internal/model/recharge_reward_models.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// RechargeRewardConfig 保存充值奖励活动的系统级配置。
|
||||||
|
type RechargeRewardConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_recharge_reward_sys_origin"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_recharge_reward_enabled"`
|
||||||
|
Timezone string `gorm:"column:timezone;size:64"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RechargeRewardConfig) TableName() string { return "recharge_reward_config" }
|
||||||
|
|
||||||
|
// RechargeRewardLevel 保存达到某个充值门槛后展示的奖励配置。
|
||||||
|
type RechargeRewardLevel struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;uniqueIndex:uk_recharge_reward_level,priority:1;uniqueIndex:uk_recharge_reward_amount,priority:1;index:idx_recharge_reward_level_config"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_recharge_reward_level,priority:2"`
|
||||||
|
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents;uniqueIndex:uk_recharge_reward_amount,priority:2"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
RewardGroupID *int64 `gorm:"column:reward_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RechargeRewardLevel) TableName() string { return "recharge_reward_level" }
|
||||||
|
|
||||||
|
// RechargeRewardGrantRecord 记录某个充值周期内用户各档位的实际发放状态。
|
||||||
|
type RechargeRewardGrantRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_recharge_reward_grant_config_cycle,priority:1"`
|
||||||
|
LevelID int64 `gorm:"column:level_id"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_recharge_reward_grant_level,priority:1;index:idx_recharge_reward_grant_date,priority:1"`
|
||||||
|
CycleKey string `gorm:"column:cycle_key;size:32;uniqueIndex:uk_recharge_reward_grant_level,priority:2;index:idx_recharge_reward_grant_config_cycle,priority:2"`
|
||||||
|
RechargeDate int `gorm:"column:recharge_date;index:idx_recharge_reward_grant_user,priority:2;index:idx_recharge_reward_grant_date,priority:2"`
|
||||||
|
PeriodStartAt time.Time `gorm:"column:period_start_at"`
|
||||||
|
PeriodEndAt time.Time `gorm:"column:period_end_at"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_recharge_reward_grant_level,priority:3;index:idx_recharge_reward_grant_user,priority:1"`
|
||||||
|
Account string `gorm:"column:account;size:64"`
|
||||||
|
UserAvatar string `gorm:"column:user_avatar;size:1024"`
|
||||||
|
UserNickname string `gorm:"column:user_nickname;size:255"`
|
||||||
|
CountryCode string `gorm:"column:country_code;size:32"`
|
||||||
|
CountryName string `gorm:"column:country_name;size:128"`
|
||||||
|
RechargeAmountCents int64 `gorm:"column:recharge_amount_cents"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_recharge_reward_grant_level,priority:4"`
|
||||||
|
RechargeThresholdCents int64 `gorm:"column:recharge_threshold_cents"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
RewardGroupID *int64 `gorm:"column:reward_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
|
||||||
|
RewardGroupTrackID int64 `gorm:"column:reward_group_track_id"`
|
||||||
|
GoldEventID string `gorm:"column:gold_event_id;size:128;index:idx_recharge_reward_gold_event"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_recharge_reward_grant_config_cycle,priority:3;index:idx_recharge_reward_grant_date,priority:3"`
|
||||||
|
RewardGroupStatus string `gorm:"column:reward_group_status;size:32"`
|
||||||
|
RewardGoldStatus string `gorm:"column:reward_gold_status;size:32"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
LastError string `gorm:"column:last_error;size:1024"`
|
||||||
|
SentAt *time.Time `gorm:"column:sent_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RechargeRewardGrantRecord) TableName() string { return "recharge_reward_grant_record" }
|
||||||
19
internal/model/region_im_group_models.go
Normal file
19
internal/model/region_im_group_models.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// VoiceRoomRegionIMGroup 保存区域通用腾讯 IM 群配置。
|
||||||
|
type VoiceRoomRegionIMGroup struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_region_im_group,priority:1"`
|
||||||
|
RegionCode string `gorm:"column:region_code;size:32;uniqueIndex:uk_voice_room_region_im_group,priority:2;index:idx_voice_room_region_im_group_group,priority:2"`
|
||||||
|
RegionName string `gorm:"column:region_name;size:128"`
|
||||||
|
GroupID string `gorm:"column:group_id;size:128;index:idx_voice_room_region_im_group_group,priority:1"`
|
||||||
|
GroupName string `gorm:"column:group_name;size:128"`
|
||||||
|
Status string `gorm:"column:status;size:32"`
|
||||||
|
LastError string `gorm:"column:last_error;size:1024"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRegionIMGroup) TableName() string { return "voice_room_region_im_group" }
|
||||||
61
internal/model/room_turnover_reward_models.go
Normal file
61
internal/model/room_turnover_reward_models.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// RoomTurnoverRewardConfig 保存房间流水奖励的系统级配置。
|
||||||
|
type RoomTurnoverRewardConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_room_turnover_reward_sys_origin"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_room_turnover_reward_enabled"`
|
||||||
|
AutoRewardEnabled bool `gorm:"column:auto_reward_enabled"`
|
||||||
|
Timezone string `gorm:"column:timezone;size:64"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RoomTurnoverRewardConfig) TableName() string { return "room_turnover_reward_config" }
|
||||||
|
|
||||||
|
// RoomTurnoverRewardLevel 保存达到某个流水门槛后发放的金币奖励。
|
||||||
|
type RoomTurnoverRewardLevel struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;uniqueIndex:uk_room_turnover_reward_level,priority:1;uniqueIndex:uk_room_turnover_reward_threshold,priority:1;index:idx_room_turnover_reward_level_config"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_room_turnover_reward_level,priority:2"`
|
||||||
|
TurnoverThreshold int64 `gorm:"column:turnover_threshold;uniqueIndex:uk_room_turnover_reward_threshold,priority:2"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RoomTurnoverRewardLevel) TableName() string { return "room_turnover_reward_level" }
|
||||||
|
|
||||||
|
// RoomTurnoverRewardRecord 记录每周给房主发放的最高档房间流水奖励。
|
||||||
|
type RoomTurnoverRewardRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_room_turnover_reward_record_config"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_room_turnover_reward_record_room,priority:1;index:idx_room_turnover_reward_record_cycle,priority:1"`
|
||||||
|
CycleKey string `gorm:"column:cycle_key;size:32;uniqueIndex:uk_room_turnover_reward_record_room,priority:2;index:idx_room_turnover_reward_record_cycle,priority:2"`
|
||||||
|
PeriodStartAt time.Time `gorm:"column:period_start_at"`
|
||||||
|
PeriodEndAt time.Time `gorm:"column:period_end_at"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;uniqueIndex:uk_room_turnover_reward_record_room,priority:3;index:idx_room_turnover_reward_record_room"`
|
||||||
|
RoomAccount string `gorm:"column:room_account;size:64"`
|
||||||
|
RoomName string `gorm:"column:room_name;size:255"`
|
||||||
|
RoomCover string `gorm:"column:room_cover;size:1024"`
|
||||||
|
OwnerUserID int64 `gorm:"column:owner_user_id;index:idx_room_turnover_reward_record_owner"`
|
||||||
|
CountryCode string `gorm:"column:country_code;size:32"`
|
||||||
|
CountryName string `gorm:"column:country_name;size:128"`
|
||||||
|
TurnoverAmount int64 `gorm:"column:turnover_amount"`
|
||||||
|
Level int `gorm:"column:level"`
|
||||||
|
TurnoverThreshold int64 `gorm:"column:turnover_threshold"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
TrackID string `gorm:"column:track_id;size:128;uniqueIndex:uk_room_turnover_reward_track"`
|
||||||
|
SourceID string `gorm:"column:source_id;size:128"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_room_turnover_reward_record_status"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
LastError string `gorm:"column:last_error;size:1024"`
|
||||||
|
SentAt *time.Time `gorm:"column:sent_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RoomTurnoverRewardRecord) TableName() string { return "room_turnover_reward_record" }
|
||||||
98
internal/model/smash_egg_models.go
Normal file
98
internal/model/smash_egg_models.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SmashEggConfig stores the resident smash golden egg activity master config.
|
||||||
|
type SmashEggConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_smash_egg_config_sys_origin"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_smash_egg_config_enabled"`
|
||||||
|
Timezone string `gorm:"column:timezone;size:64"`
|
||||||
|
RTPBasisPoints int `gorm:"column:rtp_basis_points"`
|
||||||
|
NewbiePoolEnabled bool `gorm:"column:newbie_pool_enabled"`
|
||||||
|
NewbieWindowDays int `gorm:"column:newbie_window_days"`
|
||||||
|
NewbieMaxDrawCount int `gorm:"column:newbie_max_draw_count"`
|
||||||
|
NewbieMinRechargeAmount int64 `gorm:"column:newbie_min_recharge_amount"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the smash egg master configuration table name.
|
||||||
|
func (SmashEggConfig) TableName() string { return "smash_egg_config" }
|
||||||
|
|
||||||
|
// SmashEggDrawOptionConfig stores one draw button option.
|
||||||
|
type SmashEggDrawOptionConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_smash_egg_option_config,priority:1"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_smash_egg_option_enabled,priority:1"`
|
||||||
|
OptionKey string `gorm:"column:option_key;size:32"`
|
||||||
|
Label string `gorm:"column:label;size:64"`
|
||||||
|
Times int `gorm:"column:times"`
|
||||||
|
PriceGold int64 `gorm:"column:price_gold"`
|
||||||
|
Sort int `gorm:"column:sort"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_smash_egg_option_enabled,priority:2"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the smash egg draw option configuration table name.
|
||||||
|
func (SmashEggDrawOptionConfig) TableName() string { return "smash_egg_draw_option_config" }
|
||||||
|
|
||||||
|
// SmashEggRewardConfig stores one prize on the smash egg activity.
|
||||||
|
type SmashEggRewardConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_smash_egg_reward_config,priority:1"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_smash_egg_reward_enabled,priority:1"`
|
||||||
|
PoolType string `gorm:"column:pool_type;size:32;index:idx_smash_egg_reward_pool,priority:2"`
|
||||||
|
RewardType string `gorm:"column:reward_type;size:32"`
|
||||||
|
RewardGroupID *int64 `gorm:"column:reward_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
|
||||||
|
ResourceType string `gorm:"column:resource_type;size:64"`
|
||||||
|
ResourceURL string `gorm:"column:resource_url;size:512"`
|
||||||
|
CoverURL string `gorm:"column:cover_url;size:512"`
|
||||||
|
AnimationURL string `gorm:"column:animation_url;size:512"`
|
||||||
|
DurationDays int `gorm:"column:duration_days"`
|
||||||
|
DisplayGoldAmount int64 `gorm:"column:display_gold_amount"`
|
||||||
|
GoldAmount int64 `gorm:"column:gold_amount"`
|
||||||
|
RewardValueGold int64 `gorm:"column:reward_value_gold"`
|
||||||
|
Probability int `gorm:"column:probability"`
|
||||||
|
Sort int `gorm:"column:sort"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_smash_egg_reward_enabled,priority:2"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the smash egg reward configuration table name.
|
||||||
|
func (SmashEggRewardConfig) TableName() string { return "smash_egg_reward_config" }
|
||||||
|
|
||||||
|
// SmashEggDrawRecord stores each prize item produced by a smash egg draw.
|
||||||
|
type SmashEggDrawRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
DrawNo string `gorm:"column:draw_no;size:64;index:idx_smash_egg_draw_record_draw_no"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_smash_egg_draw_record_event"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_smash_egg_draw_record_user_time,priority:1;index:idx_smash_egg_draw_record_day,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_smash_egg_draw_record_user_time,priority:2"`
|
||||||
|
PoolType string `gorm:"column:pool_type;size:32;index:idx_smash_egg_draw_record_user_pool,priority:3"`
|
||||||
|
DrawOptionID int64 `gorm:"column:draw_option_id"`
|
||||||
|
DrawTimes int `gorm:"column:draw_times"`
|
||||||
|
PaidGold int64 `gorm:"column:paid_gold"`
|
||||||
|
RewardType string `gorm:"column:reward_type;size:32"`
|
||||||
|
RewardGroupID *int64 `gorm:"column:reward_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:255"`
|
||||||
|
ResourceType string `gorm:"column:resource_type;size:64"`
|
||||||
|
ResourceURL string `gorm:"column:resource_url;size:512"`
|
||||||
|
CoverURL string `gorm:"column:cover_url;size:512"`
|
||||||
|
AnimationURL string `gorm:"column:animation_url;size:512"`
|
||||||
|
DurationDays int `gorm:"column:duration_days"`
|
||||||
|
DisplayGoldAmount int64 `gorm:"column:display_gold_amount"`
|
||||||
|
GoldAmount int64 `gorm:"column:gold_amount"`
|
||||||
|
RewardValueGold int64 `gorm:"column:reward_value_gold"`
|
||||||
|
Probability int `gorm:"column:probability"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_smash_egg_draw_record_status"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:1024"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_smash_egg_draw_record_user_time,priority:3;index:idx_smash_egg_draw_record_day,priority:2"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the smash egg draw record table name.
|
||||||
|
func (SmashEggDrawRecord) TableName() string { return "smash_egg_draw_record" }
|
||||||
206
internal/model/taskcenter_models.go
Normal file
206
internal/model/taskcenter_models.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// TaskCenterConfig 保存任务中心的系统级开关和时区配置。
|
||||||
|
type TaskCenterConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_config_sys_origin"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_task_center_config_enabled"`
|
||||||
|
Timezone string `gorm:"column:timezone;size:64"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterConfig) TableName() string { return "task_center_config" }
|
||||||
|
|
||||||
|
// TaskCenterTaskConfig 保存单个任务定义。
|
||||||
|
type TaskCenterTaskConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_task_center_task_config_id"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_task_code,priority:1;index:idx_task_center_task_sys_category"`
|
||||||
|
TaskCode string `gorm:"column:task_code;size:64;uniqueIndex:uk_task_center_task_code,priority:2"`
|
||||||
|
TaskCategory string `gorm:"column:task_category;size:32;index:idx_task_center_task_sys_category"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_task_center_task_enabled"`
|
||||||
|
TaskName string `gorm:"column:task_name;size:255"`
|
||||||
|
TaskDesc string `gorm:"column:task_desc;size:255"`
|
||||||
|
ConditionType string `gorm:"column:condition_type;size:64;index:idx_task_center_task_condition"`
|
||||||
|
TargetValue int64 `gorm:"column:target_value"`
|
||||||
|
TargetUnit string `gorm:"column:target_unit;size:32"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
JumpType string `gorm:"column:jump_type;size:64"`
|
||||||
|
JumpPage string `gorm:"column:jump_page;size:512"`
|
||||||
|
TaskIcon string `gorm:"column:task_icon;size:512"`
|
||||||
|
Cover string `gorm:"column:cover;size:512"`
|
||||||
|
SortOrder int `gorm:"column:sort_order;index:idx_task_center_task_sort"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterTaskConfig) TableName() string { return "task_center_task_config" }
|
||||||
|
|
||||||
|
// TaskCenterConditionJumpConfig 保存任务条件对应的跳转配置。
|
||||||
|
type TaskCenterConditionJumpConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_task_center_condition_jump_config_id"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_condition_jump,priority:1;index:idx_task_center_condition_jump_sys_category"`
|
||||||
|
TaskCategory string `gorm:"column:task_category;size:32;uniqueIndex:uk_task_center_condition_jump,priority:2;index:idx_task_center_condition_jump_sys_category"`
|
||||||
|
ConditionType string `gorm:"column:condition_type;size:64;uniqueIndex:uk_task_center_condition_jump,priority:3;index:idx_task_center_condition_jump_condition"`
|
||||||
|
JumpType string `gorm:"column:jump_type;size:64"`
|
||||||
|
JumpPage string `gorm:"column:jump_page;size:512"`
|
||||||
|
SortOrder int `gorm:"column:sort_order;index:idx_task_center_condition_jump_sort"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterConditionJumpConfig) TableName() string {
|
||||||
|
return "task_center_condition_jump_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCenterUserProgress 保存用户在某个任务周期内的进度。
|
||||||
|
type TaskCenterUserProgress struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_user_progress,priority:1;index:idx_task_center_progress_user"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_task_center_user_progress,priority:2;index:idx_task_center_progress_user"`
|
||||||
|
TaskID int64 `gorm:"column:task_id;uniqueIndex:uk_task_center_user_progress,priority:3;index:idx_task_center_progress_task"`
|
||||||
|
TaskCode string `gorm:"column:task_code;size:64;index:idx_task_center_progress_task_code"`
|
||||||
|
TaskCategory string `gorm:"column:task_category;size:32"`
|
||||||
|
CycleKey string `gorm:"column:cycle_key;size:32;uniqueIndex:uk_task_center_user_progress,priority:4;index:idx_task_center_progress_cycle"`
|
||||||
|
ConditionType string `gorm:"column:condition_type;size:64"`
|
||||||
|
CompletedValue int64 `gorm:"column:completed_value"`
|
||||||
|
TargetValue int64 `gorm:"column:target_value"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_task_center_progress_status"`
|
||||||
|
RewardCollected bool `gorm:"column:reward_collected"`
|
||||||
|
ClaimedAt *time.Time `gorm:"column:claimed_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterUserProgress) TableName() string { return "task_center_user_progress" }
|
||||||
|
|
||||||
|
// TaskCenterEvent 保存内部业务事件和命中的任务流水。
|
||||||
|
type TaskCenterEvent struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_task_center_event_sys"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_task_center_event_task,priority:1;index:idx_task_center_event_id"`
|
||||||
|
EventType string `gorm:"column:event_type;size:64;index:idx_task_center_event_type"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_task_center_event_user"`
|
||||||
|
TaskID int64 `gorm:"column:task_id;uniqueIndex:uk_task_center_event_task,priority:2"`
|
||||||
|
TaskCode string `gorm:"column:task_code;size:64;index:idx_task_center_event_task_code"`
|
||||||
|
TaskCategory string `gorm:"column:task_category;size:32"`
|
||||||
|
CycleKey string `gorm:"column:cycle_key;size:32"`
|
||||||
|
DeltaValue int64 `gorm:"column:delta_value"`
|
||||||
|
CompletedValue int64 `gorm:"column:completed_value"`
|
||||||
|
RawJSON string `gorm:"column:raw_json;type:text"`
|
||||||
|
OccurredAt time.Time `gorm:"column:occurred_at;index:idx_task_center_event_occurred"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterEvent) TableName() string { return "task_center_event" }
|
||||||
|
|
||||||
|
// TaskCenterEventDedup 保存业务事件幂等记录。
|
||||||
|
type TaskCenterEventDedup struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_event_dedup,priority:1"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_task_center_event_dedup,priority:2"`
|
||||||
|
EventType string `gorm:"column:event_type;size:64"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_task_center_event_dedup_status"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason;size:512"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_task_center_event_dedup_create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterEventDedup) TableName() string { return "task_center_event_dedup" }
|
||||||
|
|
||||||
|
// TaskCenterEventArchiveOutbox 保存待归档到 COS 的原始事件。
|
||||||
|
type TaskCenterEventArchiveOutbox struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;index:idx_task_center_archive_claim_create,priority:3;index:idx_task_center_archive_claim_stale,priority:3;index:idx_task_center_game_king_backfill,priority:4"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_task_center_archive_event,priority:1;index:idx_task_center_game_king_backfill,priority:1"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_task_center_archive_event,priority:2"`
|
||||||
|
EventType string `gorm:"column:event_type;size:64;index:idx_task_center_game_king_backfill,priority:2"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
OccurredAt time.Time `gorm:"column:occurred_at;index:idx_task_center_game_king_backfill,priority:3"`
|
||||||
|
RawJSON string `gorm:"column:raw_json;type:text"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_task_center_archive_status_time,priority:1;index:idx_task_center_archive_claim_create,priority:1;index:idx_task_center_archive_claim_stale,priority:1"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
COSKey string `gorm:"column:cos_key;size:512"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason;size:512"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_task_center_archive_status_time,priority:2;index:idx_task_center_archive_claim_create,priority:2"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time;index:idx_task_center_archive_claim_stale,priority:2"`
|
||||||
|
ArchivedAt *time.Time `gorm:"column:archived_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterEventArchiveOutbox) TableName() string {
|
||||||
|
return "task_center_event_archive_outbox"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCenterArchiveManifest 保存 COS 对象的可验证清单。GC 只有在源对象和恢复包都达到
|
||||||
|
// VERIFIED 后才允许删行,避免把“PUT 返回成功”错误地当成可恢复承诺。
|
||||||
|
type TaskCenterArchiveManifest struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
|
ObjectType string `gorm:"column:object_type;size:32;index:idx_task_center_archive_manifest_run,priority:2"`
|
||||||
|
RunID string `gorm:"column:run_id;size:64;index:idx_task_center_archive_manifest_run,priority:1"`
|
||||||
|
COSKey string `gorm:"column:cos_key;size:512;uniqueIndex:uk_task_center_archive_manifest_cos_key"`
|
||||||
|
ParentCOSKey string `gorm:"column:parent_cos_key;size:512"`
|
||||||
|
SHA256 string `gorm:"column:sha256;size:64"`
|
||||||
|
ETag string `gorm:"column:etag;size:128"`
|
||||||
|
CompressedSize int64 `gorm:"column:compressed_size"`
|
||||||
|
RowCount int `gorm:"column:row_count"`
|
||||||
|
MinOutboxID *int64 `gorm:"column:min_outbox_id"`
|
||||||
|
MaxOutboxID *int64 `gorm:"column:max_outbox_id"`
|
||||||
|
MinOccurredAt *time.Time `gorm:"column:min_occurred_at"`
|
||||||
|
MaxOccurredAt *time.Time `gorm:"column:max_occurred_at"`
|
||||||
|
VerificationStatus string `gorm:"column:verification_status;size:32"`
|
||||||
|
VerifiedAt *time.Time `gorm:"column:verified_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterArchiveManifest) TableName() string { return "task_center_archive_manifest" }
|
||||||
|
|
||||||
|
// TaskCenterArchiveGCRun 是固定 id=1 的全局控制行。管理命令、cron tick 和 Game King
|
||||||
|
// 启用门禁都先锁这一行,从数据库层串行化多节点状态变化。
|
||||||
|
type TaskCenterArchiveGCRun struct {
|
||||||
|
ID uint8 `gorm:"column:id;primaryKey"`
|
||||||
|
RunID string `gorm:"column:run_id;size:64"`
|
||||||
|
Status string `gorm:"column:status;size:32"`
|
||||||
|
CutoffTime *time.Time `gorm:"column:cutoff_time"`
|
||||||
|
BatchSize int `gorm:"column:batch_size"`
|
||||||
|
ScannedRows int64 `gorm:"column:scanned_rows"`
|
||||||
|
EligibleRows int64 `gorm:"column:eligible_rows"`
|
||||||
|
ProtectedRows int64 `gorm:"column:protected_rows"`
|
||||||
|
DeletedRows int64 `gorm:"column:deleted_rows"`
|
||||||
|
RecoveryObjectCount int64 `gorm:"column:recovery_object_count"`
|
||||||
|
CursorCreateTime *time.Time `gorm:"column:cursor_create_time"`
|
||||||
|
CursorID int64 `gorm:"column:cursor_id"`
|
||||||
|
LeaseOwner string `gorm:"column:lease_owner;size:160"`
|
||||||
|
LeaseUntil *time.Time `gorm:"column:lease_until"`
|
||||||
|
LastError string `gorm:"column:last_error;size:1000"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterArchiveGCRun) TableName() string { return "task_center_archive_gc_run" }
|
||||||
|
|
||||||
|
// TaskCenterClaimRecord 保存任务奖励领取记录。
|
||||||
|
type TaskCenterClaimRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_task_center_claim_sys"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_task_center_claim_user"`
|
||||||
|
TaskID int64 `gorm:"column:task_id;index:idx_task_center_claim_task"`
|
||||||
|
TaskCode string `gorm:"column:task_code;size:64;index:idx_task_center_claim_task_code"`
|
||||||
|
TaskName string `gorm:"column:task_name;size:255"`
|
||||||
|
TaskCategory string `gorm:"column:task_category;size:32"`
|
||||||
|
CycleKey string `gorm:"column:cycle_key;size:32;index:idx_task_center_claim_cycle"`
|
||||||
|
RewardGold int64 `gorm:"column:reward_gold"`
|
||||||
|
WalletEventID string `gorm:"column:wallet_event_id;size:160;uniqueIndex:uk_task_center_claim_wallet_event"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_task_center_claim_status"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason;size:512"`
|
||||||
|
ClaimedAt *time.Time `gorm:"column:claimed_at"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TaskCenterClaimRecord) TableName() string { return "task_center_claim_record" }
|
||||||
109
internal/model/vip_models.go
Normal file
109
internal/model/vip_models.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// VipLevelConfig stores the operator-managed 30-day VIP level configuration.
|
||||||
|
type VipLevelConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_vip_level_config,priority:1;index:idx_vip_level_config_sys_origin,priority:1"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_vip_level_config,priority:2"`
|
||||||
|
LevelCode string `gorm:"column:level_code;size:16"`
|
||||||
|
DisplayName string `gorm:"column:display_name;size:64"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
DurationDays int `gorm:"column:duration_days"`
|
||||||
|
PriceGold int64 `gorm:"column:price_gold"`
|
||||||
|
BadgeResourceID int64 `gorm:"column:badge_resource_id"`
|
||||||
|
BadgeName string `gorm:"column:badge_name;size:128"`
|
||||||
|
BadgeURL string `gorm:"column:badge_url;size:512"`
|
||||||
|
ShortBadgeResourceID int64 `gorm:"column:short_badge_resource_id"`
|
||||||
|
ShortBadgeName string `gorm:"column:short_badge_name;size:128"`
|
||||||
|
ShortBadgeURL string `gorm:"column:short_badge_url;size:512"`
|
||||||
|
AvatarFrameResourceID int64 `gorm:"column:avatar_frame_resource_id"`
|
||||||
|
AvatarFrameName string `gorm:"column:avatar_frame_name;size:128"`
|
||||||
|
AvatarFrameURL string `gorm:"column:avatar_frame_url;size:512"`
|
||||||
|
EntryEffectResourceID int64 `gorm:"column:entry_effect_resource_id"`
|
||||||
|
EntryEffectName string `gorm:"column:entry_effect_name;size:128"`
|
||||||
|
EntryEffectURL string `gorm:"column:entry_effect_url;size:512"`
|
||||||
|
ChatBubbleResourceID int64 `gorm:"column:chat_bubble_resource_id"`
|
||||||
|
ChatBubbleName string `gorm:"column:chat_bubble_name;size:128"`
|
||||||
|
ChatBubbleURL string `gorm:"column:chat_bubble_url;size:512"`
|
||||||
|
FloatPictureResourceID int64 `gorm:"column:float_picture_resource_id"`
|
||||||
|
FloatPictureName string `gorm:"column:float_picture_name;size:128"`
|
||||||
|
FloatPictureURL string `gorm:"column:float_picture_url;size:512"`
|
||||||
|
BackgroundCardResourceID int64 `gorm:"column:background_card_resource_id"`
|
||||||
|
BackgroundCardName string `gorm:"column:background_card_name;size:128"`
|
||||||
|
BackgroundCardURL string `gorm:"column:background_card_url;size:512"`
|
||||||
|
EffectImageResourceID int64 `gorm:"column:effect_image_resource_id"`
|
||||||
|
EffectImageName string `gorm:"column:effect_image_name;size:128"`
|
||||||
|
EffectImageURL string `gorm:"column:effect_image_url;size:512"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the VIP level config table name.
|
||||||
|
func (VipLevelConfig) TableName() string { return "vip_level_config" }
|
||||||
|
|
||||||
|
// UserVipState stores the single active VIP state for a user.
|
||||||
|
type UserVipState struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_user_vip_state,priority:1;index:idx_user_vip_state_lookup,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_user_vip_state,priority:2;index:idx_user_vip_state_lookup,priority:2"`
|
||||||
|
Level int `gorm:"column:level"`
|
||||||
|
LevelCode string `gorm:"column:level_code;size:16"`
|
||||||
|
DisplayName string `gorm:"column:display_name;size:64"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_user_vip_state_lookup,priority:3"`
|
||||||
|
StartAt time.Time `gorm:"column:start_at"`
|
||||||
|
ExpireAt time.Time `gorm:"column:expire_at;index:idx_user_vip_expire"`
|
||||||
|
BadgeResourceID int64 `gorm:"column:badge_resource_id"`
|
||||||
|
BadgeName string `gorm:"column:badge_name;size:128"`
|
||||||
|
BadgeURL string `gorm:"column:badge_url;size:512"`
|
||||||
|
ShortBadgeResourceID int64 `gorm:"column:short_badge_resource_id"`
|
||||||
|
ShortBadgeName string `gorm:"column:short_badge_name;size:128"`
|
||||||
|
ShortBadgeURL string `gorm:"column:short_badge_url;size:512"`
|
||||||
|
AvatarFrameResourceID int64 `gorm:"column:avatar_frame_resource_id"`
|
||||||
|
AvatarFrameName string `gorm:"column:avatar_frame_name;size:128"`
|
||||||
|
AvatarFrameURL string `gorm:"column:avatar_frame_url;size:512"`
|
||||||
|
EntryEffectResourceID int64 `gorm:"column:entry_effect_resource_id"`
|
||||||
|
EntryEffectName string `gorm:"column:entry_effect_name;size:128"`
|
||||||
|
EntryEffectURL string `gorm:"column:entry_effect_url;size:512"`
|
||||||
|
ChatBubbleResourceID int64 `gorm:"column:chat_bubble_resource_id"`
|
||||||
|
ChatBubbleName string `gorm:"column:chat_bubble_name;size:128"`
|
||||||
|
ChatBubbleURL string `gorm:"column:chat_bubble_url;size:512"`
|
||||||
|
FloatPictureResourceID int64 `gorm:"column:float_picture_resource_id"`
|
||||||
|
FloatPictureName string `gorm:"column:float_picture_name;size:128"`
|
||||||
|
FloatPictureURL string `gorm:"column:float_picture_url;size:512"`
|
||||||
|
BackgroundCardResourceID int64 `gorm:"column:background_card_resource_id"`
|
||||||
|
BackgroundCardName string `gorm:"column:background_card_name;size:128"`
|
||||||
|
BackgroundCardURL string `gorm:"column:background_card_url;size:512"`
|
||||||
|
EffectImageResourceID int64 `gorm:"column:effect_image_resource_id"`
|
||||||
|
EffectImageName string `gorm:"column:effect_image_name;size:128"`
|
||||||
|
EffectImageURL string `gorm:"column:effect_image_url;size:512"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the user VIP state table name.
|
||||||
|
func (UserVipState) TableName() string { return "user_vip_state" }
|
||||||
|
|
||||||
|
// VipOrderRecord stores VIP purchase, renew, and upgrade records.
|
||||||
|
type VipOrderRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_vip_order_event"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_vip_order_user_time,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_vip_order_user_time,priority:2"`
|
||||||
|
OrderType string `gorm:"column:order_type;size:32"`
|
||||||
|
FromLevel int `gorm:"column:from_level"`
|
||||||
|
ToLevel int `gorm:"column:to_level"`
|
||||||
|
DurationDays int `gorm:"column:duration_days"`
|
||||||
|
OriginalPriceGold int64 `gorm:"column:original_price_gold"`
|
||||||
|
PaidGold int64 `gorm:"column:paid_gold"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_vip_order_status"`
|
||||||
|
StartAt time.Time `gorm:"column:start_at"`
|
||||||
|
ExpireAt time.Time `gorm:"column:expire_at"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:512"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_vip_order_user_time,priority:3"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the VIP order table name.
|
||||||
|
func (VipOrderRecord) TableName() string { return "vip_order_record" }
|
||||||
127
internal/model/voice_room_red_packet_models.go
Normal file
127
internal/model/voice_room_red_packet_models.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// VoiceRoomRedPacketConfig 保存语音房红包系统级配置。
|
||||||
|
type VoiceRoomRedPacketConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_red_packet_config"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
DailySendLimit int `gorm:"column:daily_send_limit"`
|
||||||
|
DelaySeconds int `gorm:"column:delay_seconds"`
|
||||||
|
RefundAfterSeconds int `gorm:"column:refund_after_seconds"`
|
||||||
|
Timezone string `gorm:"column:timezone;size:64"`
|
||||||
|
AmountOptions string `gorm:"column:amount_options;size:255"`
|
||||||
|
CountOptions string `gorm:"column:count_options;size:255"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRedPacketConfig) TableName() string { return "voice_room_red_packet_config" }
|
||||||
|
|
||||||
|
// VoiceRoomRegionCountry 保存国家到区域的映射。
|
||||||
|
type VoiceRoomRegionCountry struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_region_country,priority:1;index:idx_voice_room_region_country_region,priority:1"`
|
||||||
|
RegionCode string `gorm:"column:region_code;size:32;index:idx_voice_room_region_country_region,priority:2"`
|
||||||
|
CountryCode string `gorm:"column:country_code;size:32;uniqueIndex:uk_voice_room_region_country,priority:2"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRegionCountry) TableName() string { return "voice_room_region_country" }
|
||||||
|
|
||||||
|
// RoomLiveBroadcastVIP 是 Java 房间资料表,Go 侧只读取 room_id 到 room_account 的映射。
|
||||||
|
type RoomLiveBroadcastVIP struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;index"`
|
||||||
|
RoomAccount int64 `gorm:"column:room_account"`
|
||||||
|
ExpireDate time.Time `gorm:"column:expire_date"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RoomLiveBroadcastVIP) TableName() string { return "room_live_broadcasts_vip" }
|
||||||
|
|
||||||
|
// VoiceRoomRedPacket 保存语音房红包主记录。
|
||||||
|
type VoiceRoomRedPacket struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
PacketNo string `gorm:"column:packet_no;size:64;uniqueIndex:uk_voice_room_red_packet_no"`
|
||||||
|
RequestID string `gorm:"column:request_id;size:128;uniqueIndex:uk_voice_room_red_packet_request"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_voice_room_red_packet_room,priority:1;index:idx_voice_room_red_packet_sender_day,priority:1"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;index:idx_voice_room_red_packet_room,priority:2"`
|
||||||
|
SenderUserID int64 `gorm:"column:sender_user_id;index:idx_voice_room_red_packet_sender_day,priority:2"`
|
||||||
|
SenderRegionCode string `gorm:"column:sender_region_code;size:32"`
|
||||||
|
PacketMode string `gorm:"column:packet_mode;size:16"`
|
||||||
|
AmountMode string `gorm:"column:amount_mode;size:16"`
|
||||||
|
TotalAmount int64 `gorm:"column:total_amount"`
|
||||||
|
TotalCount int `gorm:"column:total_count"`
|
||||||
|
RemainAmount int64 `gorm:"column:remain_amount"`
|
||||||
|
RemainCount int `gorm:"column:remain_count"`
|
||||||
|
ClaimStartTime time.Time `gorm:"column:claim_start_time"`
|
||||||
|
ExpireTime time.Time `gorm:"column:expire_time;index:idx_voice_room_red_packet_expire,priority:2"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_voice_room_red_packet_expire,priority:1"`
|
||||||
|
SendWalletEventID string `gorm:"column:send_wallet_event_id;size:128;uniqueIndex:uk_voice_room_red_packet_send_event"`
|
||||||
|
RefundWalletEventID string `gorm:"column:refund_wallet_event_id;size:128"`
|
||||||
|
RefundAmount int64 `gorm:"column:refund_amount"`
|
||||||
|
RefundStatus string `gorm:"column:refund_status;size:32"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:1024"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_voice_room_red_packet_sender_day,priority:3"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRedPacket) TableName() string { return "voice_room_red_packet" }
|
||||||
|
|
||||||
|
// VoiceRoomRedPacketSlice 保存红包预拆分份额。
|
||||||
|
type VoiceRoomRedPacketSlice struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;index:idx_voice_room_red_packet_slice_claim_order,priority:4"`
|
||||||
|
PacketID int64 `gorm:"column:packet_id;uniqueIndex:uk_voice_room_red_packet_slice,priority:1;index:idx_voice_room_red_packet_slice_claim,priority:1;index:idx_voice_room_red_packet_slice_claim_order,priority:1"`
|
||||||
|
PacketNo string `gorm:"column:packet_no;size:64"`
|
||||||
|
SortNo int `gorm:"column:sort_no;uniqueIndex:uk_voice_room_red_packet_slice,priority:2;index:idx_voice_room_red_packet_slice_claim_order,priority:3"`
|
||||||
|
Amount int64 `gorm:"column:amount"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_voice_room_red_packet_slice_claim,priority:2;index:idx_voice_room_red_packet_slice_claim_order,priority:2"`
|
||||||
|
ClaimUserID *int64 `gorm:"column:claim_user_id"`
|
||||||
|
ClaimRecordID *int64 `gorm:"column:claim_record_id"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRedPacketSlice) TableName() string { return "voice_room_red_packet_slice" }
|
||||||
|
|
||||||
|
// VoiceRoomRedPacketClaim 保存领取记录和入账状态。
|
||||||
|
type VoiceRoomRedPacketClaim struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
PacketID int64 `gorm:"column:packet_id;uniqueIndex:uk_voice_room_red_packet_claim_user,priority:1;index:idx_voice_room_red_packet_claim_packet,priority:1"`
|
||||||
|
PacketNo string `gorm:"column:packet_no;size:64"`
|
||||||
|
SliceID int64 `gorm:"column:slice_id"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32"`
|
||||||
|
RoomID int64 `gorm:"column:room_id"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_voice_room_red_packet_claim_user,priority:2"`
|
||||||
|
UserRegionCode string `gorm:"column:user_region_code;size:32"`
|
||||||
|
Amount int64 `gorm:"column:amount"`
|
||||||
|
WalletEventID string `gorm:"column:wallet_event_id;size:128;uniqueIndex:uk_voice_room_red_packet_claim_event"`
|
||||||
|
WalletStatus string `gorm:"column:wallet_status;size:32"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:1024"`
|
||||||
|
ClaimTime time.Time `gorm:"column:claim_time;index:idx_voice_room_red_packet_claim_packet,priority:2"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRedPacketClaim) TableName() string { return "voice_room_red_packet_claim" }
|
||||||
|
|
||||||
|
// VoiceRoomRedPacketDailyCounter 保存用户单日发送计数。
|
||||||
|
type VoiceRoomRedPacketDailyCounter struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_red_packet_daily,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_voice_room_red_packet_daily,priority:2"`
|
||||||
|
DayKey string `gorm:"column:day_key;size:16;uniqueIndex:uk_voice_room_red_packet_daily,priority:3"`
|
||||||
|
SendCount int `gorm:"column:send_count"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRedPacketDailyCounter) TableName() string {
|
||||||
|
return "voice_room_red_packet_daily_counter"
|
||||||
|
}
|
||||||
214
internal/model/voice_room_rocket_models.go
Normal file
214
internal/model/voice_room_rocket_models.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// VoiceRoomRocketConfig 保存语音房火箭系统级配置。
|
||||||
|
type VoiceRoomRocketConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_rocket_config"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
Timezone string `gorm:"column:timezone;size:64"`
|
||||||
|
MaxLevel int `gorm:"column:max_level"`
|
||||||
|
RankingTopLimit int `gorm:"column:ranking_top_limit"`
|
||||||
|
RewardRecordKeepDays int `gorm:"column:reward_record_keep_days"`
|
||||||
|
BroadcastDurationSeconds int `gorm:"column:broadcast_duration_seconds"`
|
||||||
|
InRoomRewardDelaySeconds int `gorm:"column:in_room_reward_delay_seconds"`
|
||||||
|
EnergyRuleJSON string `gorm:"column:energy_rule_json;type:json"`
|
||||||
|
RuleText string `gorm:"column:rule_text;type:json"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketConfig) TableName() string { return "voice_room_rocket_config" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketLevelConfig 保存每级能量、资源和抖动阈值配置。
|
||||||
|
type VoiceRoomRocketLevelConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_rocket_level,priority:1"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_voice_room_rocket_level,priority:2"`
|
||||||
|
NeedEnergy int64 `gorm:"column:need_energy"`
|
||||||
|
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"`
|
||||||
|
RocketAnimationURL string `gorm:"column:rocket_animation_url;size:512"`
|
||||||
|
ProgressBarURL string `gorm:"column:progress_bar_url;size:512"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
Sort int `gorm:"column:sort"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketLevelConfig) TableName() string { return "voice_room_rocket_level_config" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketRewardConfig 保存 Top1、引爆、在房间三类奖励配置。
|
||||||
|
type VoiceRoomRocketRewardConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_voice_room_rocket_reward_scene,priority:1"`
|
||||||
|
Level int `gorm:"column:level;index:idx_voice_room_rocket_reward_scene,priority:2"`
|
||||||
|
RewardScene string `gorm:"column:reward_scene;size:32;index:idx_voice_room_rocket_reward_scene,priority:3"`
|
||||||
|
RewardType string `gorm:"column:reward_type;size:32"`
|
||||||
|
RewardItemID *int64 `gorm:"column:reward_item_id"`
|
||||||
|
RewardName string `gorm:"column:reward_name;size:128"`
|
||||||
|
RewardCover string `gorm:"column:reward_cover;size:512"`
|
||||||
|
RewardAmount int64 `gorm:"column:reward_amount"`
|
||||||
|
ExpireDays *int `gorm:"column:expire_days"`
|
||||||
|
RewardCopies int `gorm:"column:reward_copies;default:1"`
|
||||||
|
Weight int `gorm:"column:weight"`
|
||||||
|
Sort int `gorm:"column:sort"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_voice_room_rocket_reward_scene,priority:4"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketRewardConfig) TableName() string { return "voice_room_rocket_reward_config" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketStatus 保存房间当天当前火箭状态。
|
||||||
|
type VoiceRoomRocketStatus struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_rocket_status,priority:1"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;uniqueIndex:uk_voice_room_rocket_status,priority:2"`
|
||||||
|
DayKey string `gorm:"column:day_key;size:16;uniqueIndex:uk_voice_room_rocket_status,priority:3"`
|
||||||
|
RoundNo int `gorm:"column:round_no"`
|
||||||
|
CurrentLevel int `gorm:"column:current_level"`
|
||||||
|
CurrentEnergy int64 `gorm:"column:current_energy"`
|
||||||
|
NeedEnergy int64 `gorm:"column:need_energy"`
|
||||||
|
Status string `gorm:"column:status;size:32"`
|
||||||
|
LevelStartTime time.Time `gorm:"column:level_start_time"`
|
||||||
|
LastEventTime *time.Time `gorm:"column:last_event_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketStatus) TableName() string { return "voice_room_rocket_status" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketLevelRound 保存每轮每级的充能/发射快照。
|
||||||
|
type VoiceRoomRocketLevelRound struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_rocket_level_round,priority:1;index:idx_voice_room_rocket_level_round_room,priority:1"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;uniqueIndex:uk_voice_room_rocket_level_round,priority:2;index:idx_voice_room_rocket_level_round_room,priority:2"`
|
||||||
|
DayKey string `gorm:"column:day_key;size:16;uniqueIndex:uk_voice_room_rocket_level_round,priority:3;index:idx_voice_room_rocket_level_round_room,priority:3"`
|
||||||
|
RoundNo int `gorm:"column:round_no;uniqueIndex:uk_voice_room_rocket_level_round,priority:4;index:idx_voice_room_rocket_level_round_room,priority:4"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_voice_room_rocket_level_round,priority:5"`
|
||||||
|
NeedEnergy int64 `gorm:"column:need_energy"`
|
||||||
|
FinalEnergy int64 `gorm:"column:final_energy"`
|
||||||
|
Status string `gorm:"column:status;size:32"`
|
||||||
|
Top1UserID *int64 `gorm:"column:top1_user_id"`
|
||||||
|
IgniteUserID *int64 `gorm:"column:ignite_user_id"`
|
||||||
|
ReachTime *time.Time `gorm:"column:reach_time"`
|
||||||
|
LaunchTime *time.Time `gorm:"column:launch_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketLevelRound) TableName() string { return "voice_room_rocket_level_round" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketContribution 保存本轮本级用户贡献榜。
|
||||||
|
type VoiceRoomRocketContribution struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_rocket_contribution_user,priority:1;index:idx_voice_room_rocket_contribution_rank,priority:1"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;uniqueIndex:uk_voice_room_rocket_contribution_user,priority:2;index:idx_voice_room_rocket_contribution_rank,priority:2"`
|
||||||
|
DayKey string `gorm:"column:day_key;size:16;uniqueIndex:uk_voice_room_rocket_contribution_user,priority:3;index:idx_voice_room_rocket_contribution_rank,priority:3"`
|
||||||
|
RoundNo int `gorm:"column:round_no;uniqueIndex:uk_voice_room_rocket_contribution_user,priority:4;index:idx_voice_room_rocket_contribution_rank,priority:4"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_voice_room_rocket_contribution_user,priority:5;index:idx_voice_room_rocket_contribution_rank,priority:5"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_voice_room_rocket_contribution_user,priority:6"`
|
||||||
|
ScoreEnergy int64 `gorm:"column:score_energy;index:idx_voice_room_rocket_contribution_rank,priority:6"`
|
||||||
|
GiftCount int `gorm:"column:gift_count"`
|
||||||
|
FirstContributeTime time.Time `gorm:"column:first_contribute_time"`
|
||||||
|
LastContributeTime time.Time `gorm:"column:last_contribute_time;index:idx_voice_room_rocket_contribution_rank,priority:7"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketContribution) TableName() string { return "voice_room_rocket_contribution" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketGiftEventLog 保存送礼事件幂等和处理日志。
|
||||||
|
type VoiceRoomRocketGiftEventLog struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_voice_room_rocket_event_room,priority:1"`
|
||||||
|
TrackID string `gorm:"column:track_id;size:128;uniqueIndex:uk_voice_room_rocket_track"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;index:idx_voice_room_rocket_event_room,priority:2"`
|
||||||
|
SendUserID int64 `gorm:"column:send_user_id"`
|
||||||
|
GiftID *int64 `gorm:"column:gift_id"`
|
||||||
|
GiftType string `gorm:"column:gift_type;size:32"`
|
||||||
|
GiftTab string `gorm:"column:gift_tab;size:64"`
|
||||||
|
RawEnergy int64 `gorm:"column:raw_energy"`
|
||||||
|
AcceptedEnergy int64 `gorm:"column:accepted_energy"`
|
||||||
|
DayKey string `gorm:"column:day_key;size:16;index:idx_voice_room_rocket_event_room,priority:3"`
|
||||||
|
RoundNo int `gorm:"column:round_no;index:idx_voice_room_rocket_event_room,priority:4"`
|
||||||
|
Level int `gorm:"column:level;index:idx_voice_room_rocket_event_room,priority:5"`
|
||||||
|
Status string `gorm:"column:status;size:32"`
|
||||||
|
RawPayload string `gorm:"column:raw_payload;type:mediumtext"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:1024"`
|
||||||
|
EventTime time.Time `gorm:"column:event_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketGiftEventLog) TableName() string { return "voice_room_rocket_gift_event_log" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketLaunch 保存火箭每次发射记录。
|
||||||
|
type VoiceRoomRocketLaunch struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
LaunchNo string `gorm:"column:launch_no;size:64;uniqueIndex:uk_voice_room_rocket_launch_no"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_voice_room_rocket_launch_level,priority:1;index:idx_voice_room_rocket_launch_room,priority:1"`
|
||||||
|
RoomID int64 `gorm:"column:room_id;uniqueIndex:uk_voice_room_rocket_launch_level,priority:2;index:idx_voice_room_rocket_launch_room,priority:2"`
|
||||||
|
RoomAccount string `gorm:"column:room_account;size:64"`
|
||||||
|
DayKey string `gorm:"column:day_key;size:16;uniqueIndex:uk_voice_room_rocket_launch_level,priority:3"`
|
||||||
|
RoundNo int `gorm:"column:round_no;uniqueIndex:uk_voice_room_rocket_launch_level,priority:4"`
|
||||||
|
Level int `gorm:"column:level;uniqueIndex:uk_voice_room_rocket_launch_level,priority:5"`
|
||||||
|
FinalEnergy int64 `gorm:"column:final_energy"`
|
||||||
|
Top1UserID *int64 `gorm:"column:top1_user_id"`
|
||||||
|
IgniteUserID *int64 `gorm:"column:ignite_user_id"`
|
||||||
|
InRoomSnapshotStatus string `gorm:"column:in_room_snapshot_status;size:32"`
|
||||||
|
LaunchTime time.Time `gorm:"column:launch_time;index:idx_voice_room_rocket_launch_room,priority:3"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketLaunch) TableName() string { return "voice_room_rocket_launch" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketRewardRecord 保存火箭奖励发放和弹窗状态。
|
||||||
|
type VoiceRoomRocketRewardRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
LaunchID int64 `gorm:"column:launch_id;index:idx_voice_room_rocket_reward_launch,priority:1"`
|
||||||
|
LaunchNo string `gorm:"column:launch_no;size:64"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_voice_room_rocket_reward_user,priority:1"`
|
||||||
|
RoomID int64 `gorm:"column:room_id"`
|
||||||
|
DayKey string `gorm:"column:day_key;size:16"`
|
||||||
|
RoundNo int `gorm:"column:round_no"`
|
||||||
|
Level int `gorm:"column:level"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_voice_room_rocket_reward_user,priority:2;index:idx_voice_room_rocket_reward_launch,priority:2"`
|
||||||
|
RewardScene string `gorm:"column:reward_scene;size:32"`
|
||||||
|
RewardConfigID int64 `gorm:"column:reward_config_id"`
|
||||||
|
RewardType string `gorm:"column:reward_type;size:32"`
|
||||||
|
RewardItemID *int64 `gorm:"column:reward_item_id"`
|
||||||
|
RewardName string `gorm:"column:reward_name;size:128"`
|
||||||
|
RewardCover string `gorm:"column:reward_cover;size:512"`
|
||||||
|
RewardAmount int64 `gorm:"column:reward_amount"`
|
||||||
|
ExpireDays *int `gorm:"column:expire_days"`
|
||||||
|
GrantEventID string `gorm:"column:grant_event_id;size:128;uniqueIndex:uk_voice_room_rocket_reward_event"`
|
||||||
|
GrantStatus string `gorm:"column:grant_status;size:32;index:idx_voice_room_rocket_reward_retry,priority:1"`
|
||||||
|
PopupStatus string `gorm:"column:popup_status;size:32"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:1024"`
|
||||||
|
GrantTime *time.Time `gorm:"column:grant_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_voice_room_rocket_reward_user,priority:3"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time;index:idx_voice_room_rocket_reward_retry,priority:2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketRewardRecord) TableName() string { return "voice_room_rocket_reward_record" }
|
||||||
|
|
||||||
|
// VoiceRoomRocketAudienceSnapshot 保存发射后窗口内在线用户快照。
|
||||||
|
type VoiceRoomRocketAudienceSnapshot struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
LaunchID int64 `gorm:"column:launch_id;uniqueIndex:uk_voice_room_rocket_audience,priority:1;index:idx_voice_room_rocket_audience_launch,priority:1"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32"`
|
||||||
|
RoomID int64 `gorm:"column:room_id"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_voice_room_rocket_audience,priority:2"`
|
||||||
|
LastSeenTime time.Time `gorm:"column:last_seen_time"`
|
||||||
|
Selected bool `gorm:"column:selected;index:idx_voice_room_rocket_audience_launch,priority:2"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoiceRoomRocketAudienceSnapshot) TableName() string {
|
||||||
|
return "voice_room_rocket_audience_snapshot"
|
||||||
|
}
|
||||||
94
internal/model/wheel_models.go
Normal file
94
internal/model/wheel_models.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// WheelConfig stores the resident activity wheel master configuration.
|
||||||
|
type WheelConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_wheel_config_sys_origin"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
Timezone string `gorm:"column:timezone;size:64"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the wheel master configuration table name.
|
||||||
|
func (WheelConfig) TableName() string { return "wheel_config" }
|
||||||
|
|
||||||
|
// WheelPoolConfig stores category-level wheel prices and switches.
|
||||||
|
type WheelPoolConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;uniqueIndex:uk_wheel_pool_config,priority:1;index:idx_wheel_pool_config,priority:1"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_wheel_pool_sys_origin,priority:1"`
|
||||||
|
Category string `gorm:"column:category;size:32;uniqueIndex:uk_wheel_pool_config,priority:2;index:idx_wheel_pool_sys_origin,priority:2"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
PriceOneGold int64 `gorm:"column:price_one_gold"`
|
||||||
|
PriceTenGold int64 `gorm:"column:price_ten_gold"`
|
||||||
|
PriceFiftyGold int64 `gorm:"column:price_fifty_gold"`
|
||||||
|
Sort int `gorm:"column:sort"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the wheel pool configuration table name.
|
||||||
|
func (WheelPoolConfig) TableName() string { return "wheel_pool_config" }
|
||||||
|
|
||||||
|
// WheelRewardConfig stores one reward option on the wheel.
|
||||||
|
type WheelRewardConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ConfigID int64 `gorm:"column:config_id;index:idx_wheel_reward_config,priority:1"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_wheel_reward_enabled,priority:1"`
|
||||||
|
Category string `gorm:"column:category;size:32;index:idx_wheel_reward_config,priority:2;index:idx_wheel_reward_enabled,priority:2"`
|
||||||
|
RewardType string `gorm:"column:reward_type;size:32"`
|
||||||
|
ResourceID *int64 `gorm:"column:resource_id"`
|
||||||
|
ResourceType string `gorm:"column:resource_type;size:64"`
|
||||||
|
ResourceName string `gorm:"column:resource_name;size:255"`
|
||||||
|
ResourceURL string `gorm:"column:resource_url;size:512"`
|
||||||
|
CoverURL string `gorm:"column:cover_url;size:512"`
|
||||||
|
DurationDays int `gorm:"column:duration_days"`
|
||||||
|
DisplayGoldAmount int64 `gorm:"column:display_gold_amount"`
|
||||||
|
GoldAmount int64 `gorm:"column:gold_amount"`
|
||||||
|
Probability int `gorm:"column:probability"`
|
||||||
|
TotalLimit int64 `gorm:"column:total_limit"`
|
||||||
|
UserLimit int64 `gorm:"column:user_limit"`
|
||||||
|
IssuedCount int64 `gorm:"column:issued_count"`
|
||||||
|
Sort int `gorm:"column:sort"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_wheel_reward_enabled,priority:3"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the wheel reward configuration table name.
|
||||||
|
func (WheelRewardConfig) TableName() string { return "wheel_reward_config" }
|
||||||
|
|
||||||
|
// WheelDrawRecord stores each reward item produced by a wheel draw.
|
||||||
|
type WheelDrawRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
DrawNo string `gorm:"column:draw_no;size:64;index:idx_wheel_draw_record_draw_no"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_wheel_draw_record_event"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_wheel_draw_record_user_time,priority:1"`
|
||||||
|
Category string `gorm:"column:category;size:32;index:idx_wheel_draw_record_category"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_wheel_draw_record_user_time,priority:2"`
|
||||||
|
DrawTimes int `gorm:"column:draw_times"`
|
||||||
|
PaidGold int64 `gorm:"column:paid_gold"`
|
||||||
|
RewardConfigID int64 `gorm:"column:reward_config_id;index:idx_wheel_draw_reward_user,priority:1"`
|
||||||
|
RewardType string `gorm:"column:reward_type;size:32"`
|
||||||
|
ResourceID *int64 `gorm:"column:resource_id"`
|
||||||
|
ResourceType string `gorm:"column:resource_type;size:64"`
|
||||||
|
ResourceName string `gorm:"column:resource_name;size:255"`
|
||||||
|
ResourceURL string `gorm:"column:resource_url;size:512"`
|
||||||
|
CoverURL string `gorm:"column:cover_url;size:512"`
|
||||||
|
DurationDays int `gorm:"column:duration_days"`
|
||||||
|
DisplayGoldAmount int64 `gorm:"column:display_gold_amount"`
|
||||||
|
GoldAmount int64 `gorm:"column:gold_amount"`
|
||||||
|
Probability int `gorm:"column:probability"`
|
||||||
|
Status string `gorm:"column:status;size:32;index:idx_wheel_draw_record_status"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:1024"`
|
||||||
|
GrantTime *time.Time `gorm:"column:grant_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_wheel_draw_record_user_time,priority:3"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName returns the wheel draw record table name.
|
||||||
|
func (WheelDrawRecord) TableName() string { return "wheel_draw_record" }
|
||||||
185
internal/model/yumi_game_king_models.go
Normal file
185
internal/model/yumi_game_king_models.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YumiGameKingActivity 保存一次游戏王活动及其不可变统计口径。
|
||||||
|
type YumiGameKingActivity struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityCode string `gorm:"column:activity_code;size:64;uniqueIndex:uk_yumi_gk_activity_code"`
|
||||||
|
ActivityName string `gorm:"column:activity_name;size:128"`
|
||||||
|
ActivityDesc string `gorm:"column:activity_desc;size:1000"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_yumi_gk_activity_window,priority:1"`
|
||||||
|
Timezone string `gorm:"column:time_zone;size:64"`
|
||||||
|
StartTime time.Time `gorm:"column:start_time;index:idx_yumi_gk_activity_window,priority:3"`
|
||||||
|
EndTime time.Time `gorm:"column:end_time;index:idx_yumi_gk_activity_window,priority:4;index:idx_yumi_gk_activity_settle,priority:3"`
|
||||||
|
SettlementDelayMinutes int `gorm:"column:settlement_delay_minutes"`
|
||||||
|
SettlementTime time.Time `gorm:"column:settlement_time;index:idx_yumi_gk_activity_settle,priority:2"`
|
||||||
|
CoinPerDraw int64 `gorm:"column:coin_per_draw"`
|
||||||
|
RankingType string `gorm:"column:ranking_type;size:32"`
|
||||||
|
RankingPeriod string `gorm:"column:ranking_period;size:32"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_yumi_gk_activity_window,priority:2"`
|
||||||
|
SettlementStatus string `gorm:"column:settlement_status;size:32;index:idx_yumi_gk_activity_settle,priority:1"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingActivity) TableName() string { return "yumi_game_king_activity" }
|
||||||
|
|
||||||
|
// YumiGameKingScopeLock 串行同租户启用窗口检查,避免并发创建不同活动时同时通过 overlap COUNT。
|
||||||
|
type YumiGameKingScopeLock struct {
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;primaryKey"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingScopeLock) TableName() string { return "yumi_game_king_scope_lock" }
|
||||||
|
|
||||||
|
// YumiGameKingPrize 保存固定七个抽奖槽位;RewardItemsJSON 是启用前校验得到的展示快照。
|
||||||
|
type YumiGameKingPrize struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;uniqueIndex:uk_yumi_gk_prize_position,priority:1;index:idx_yumi_gk_prize_enabled,priority:1"`
|
||||||
|
PrizeName string `gorm:"column:prize_name;size:128"`
|
||||||
|
PrizeImage string `gorm:"column:prize_image;size:500"`
|
||||||
|
Weight int64 `gorm:"column:weight"`
|
||||||
|
Stock int64 `gorm:"column:stock"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
RewardGroupName string `gorm:"column:reward_group_name;size:128"`
|
||||||
|
RewardItemsJSON string `gorm:"column:reward_items_json;type:text"`
|
||||||
|
Enabled bool `gorm:"column:enabled;index:idx_yumi_gk_prize_enabled,priority:2"`
|
||||||
|
SortOrder int `gorm:"column:sort_order;uniqueIndex:uk_yumi_gk_prize_position,priority:2"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingPrize) TableName() string { return "yumi_game_king_prize" }
|
||||||
|
|
||||||
|
// YumiGameKingRankReward 保存活动结束时固定六个连续名次奖励档。
|
||||||
|
type YumiGameKingRankReward struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;uniqueIndex:uk_yumi_gk_rank_range,priority:1;index:idx_yumi_gk_rank_start,priority:1"`
|
||||||
|
StartRank int `gorm:"column:start_rank;uniqueIndex:uk_yumi_gk_rank_range,priority:2;index:idx_yumi_gk_rank_start,priority:2"`
|
||||||
|
EndRank int `gorm:"column:end_rank;uniqueIndex:uk_yumi_gk_rank_range,priority:3"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
RewardName string `gorm:"column:reward_name;size:128"`
|
||||||
|
RewardItemsJSON string `gorm:"column:reward_items_json;type:text"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingRankReward) TableName() string { return "yumi_game_king_rank_reward" }
|
||||||
|
|
||||||
|
// YumiGameKingConsumeLedger 是活动维度的幂等消费账本,实时事件和后台补数共用此边界。
|
||||||
|
type YumiGameKingConsumeLedger struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;uniqueIndex:uk_yumi_gk_ledger_event,priority:1;index:idx_yumi_gk_ledger_user,priority:1"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_yumi_gk_ledger_event,priority:2"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_yumi_gk_ledger_user,priority:2"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32"`
|
||||||
|
Amount int64 `gorm:"column:amount"`
|
||||||
|
EventTime time.Time `gorm:"column:event_time;index:idx_yumi_gk_ledger_user,priority:3"`
|
||||||
|
PayloadJSON string `gorm:"column:payload_json;type:text"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingConsumeLedger) TableName() string { return "yumi_game_king_consume_ledger" }
|
||||||
|
|
||||||
|
// YumiGameKingBackfillState 持久化复合游标。调用方重启后从该水位继续,GC 不依赖
|
||||||
|
// webconsole/chatapp-cron 的内存 lastId 来判断历史事件是否已经确认。
|
||||||
|
type YumiGameKingBackfillState struct {
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;primaryKey"`
|
||||||
|
LastOutboxID int64 `gorm:"column:last_outbox_id"`
|
||||||
|
LastOccurredAt *time.Time `gorm:"column:last_occurred_at"`
|
||||||
|
ScannedRows int64 `gorm:"column:scanned_rows"`
|
||||||
|
AcceptedRows int64 `gorm:"column:accepted_rows"`
|
||||||
|
Completed bool `gorm:"column:completed"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingBackfillState) TableName() string { return "yumi_game_king_backfill_state" }
|
||||||
|
|
||||||
|
// YumiGameKingUser 保存榜单聚合和已使用抽奖次数,避免排行榜扫描消费账本。
|
||||||
|
type YumiGameKingUser struct {
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;primaryKey;index:idx_yumi_gk_user_rank,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;primaryKey;index:idx_yumi_gk_user_rank,priority:4,sort:asc"`
|
||||||
|
TotalConsumed int64 `gorm:"column:total_consumed;index:idx_yumi_gk_user_rank,priority:2,sort:desc"`
|
||||||
|
UsedChances int64 `gorm:"column:used_chances"`
|
||||||
|
ConsumeReachedTime time.Time `gorm:"column:consume_reached_time;index:idx_yumi_gk_user_rank,priority:3,sort:asc"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingUser) TableName() string { return "yumi_game_king_user" }
|
||||||
|
|
||||||
|
// YumiGameKingUserDaily 保存活动时区自然日聚合,DAILY 排行不扫描消费明细。
|
||||||
|
type YumiGameKingUserDaily struct {
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;primaryKey;index:idx_yumi_gk_daily_rank,priority:1"`
|
||||||
|
StatDate time.Time `gorm:"column:stat_date;type:date;primaryKey;index:idx_yumi_gk_daily_rank,priority:2"`
|
||||||
|
UserID int64 `gorm:"column:user_id;primaryKey;index:idx_yumi_gk_daily_rank,priority:5,sort:asc"`
|
||||||
|
TotalConsumed int64 `gorm:"column:total_consumed;index:idx_yumi_gk_daily_rank,priority:3,sort:desc"`
|
||||||
|
ConsumeReachedTime time.Time `gorm:"column:consume_reached_time;index:idx_yumi_gk_daily_rank,priority:4,sort:asc"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingUserDaily) TableName() string { return "yumi_game_king_user_daily" }
|
||||||
|
|
||||||
|
// YumiGameKingDrawRecord 保存一次请求幂等的抽奖结果和整组奖励发放状态。
|
||||||
|
type YumiGameKingDrawRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;uniqueIndex:uk_yumi_gk_draw_request,priority:1;index:idx_yumi_gk_draw_user,priority:1;index:idx_yumi_gk_draw_manage,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_yumi_gk_draw_request,priority:2;index:idx_yumi_gk_draw_user,priority:2"`
|
||||||
|
RequestID string `gorm:"column:request_id;size:64;uniqueIndex:uk_yumi_gk_draw_request,priority:3"`
|
||||||
|
PrizeID int64 `gorm:"column:prize_id"`
|
||||||
|
PrizeName string `gorm:"column:prize_name;size:128"`
|
||||||
|
PrizeImage string `gorm:"column:prize_image;size:500"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
DeliveryStatus string `gorm:"column:delivery_status;size:32;index:idx_yumi_gk_draw_manage,priority:2"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason;size:500"`
|
||||||
|
DrawTime time.Time `gorm:"column:draw_time"`
|
||||||
|
DeliverTime *time.Time `gorm:"column:deliver_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time;index:idx_yumi_gk_draw_user,priority:3"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingDrawRecord) TableName() string { return "yumi_game_king_draw_record" }
|
||||||
|
|
||||||
|
// YumiGameKingSettlementRecord 是活动结算时冻结的 Top30 排名和发奖状态。
|
||||||
|
type YumiGameKingSettlementRecord struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;uniqueIndex:uk_yumi_gk_settle_user,priority:1;uniqueIndex:uk_yumi_gk_settle_rank,priority:1;index:idx_yumi_gk_settle_status,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;uniqueIndex:uk_yumi_gk_settle_user,priority:2"`
|
||||||
|
RankNo int `gorm:"column:rank_no;uniqueIndex:uk_yumi_gk_settle_rank,priority:2"`
|
||||||
|
TotalConsumed int64 `gorm:"column:total_consumed"`
|
||||||
|
RankRewardID int64 `gorm:"column:rank_reward_id"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
BusinessNo string `gorm:"column:business_no;size:128;uniqueIndex:uk_yumi_gk_settle_business"`
|
||||||
|
DeliveryStatus string `gorm:"column:delivery_status;size:32;index:idx_yumi_gk_settle_status,priority:2"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason;size:500"`
|
||||||
|
DeliverTime *time.Time `gorm:"column:deliver_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingSettlementRecord) TableName() string {
|
||||||
|
return "yumi_game_king_settlement_record"
|
||||||
|
}
|
||||||
|
|
||||||
|
// YumiGameKingDeliveryItem 把一次奖励组调用作为最小核账单元;UNKNOWN 不允许自动重试。
|
||||||
|
type YumiGameKingDeliveryItem struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;index:idx_yumi_gk_delivery_status,priority:3"`
|
||||||
|
OwnerType string `gorm:"column:owner_type;size:20;uniqueIndex:uk_yumi_gk_delivery_owner,priority:1"`
|
||||||
|
OwnerID int64 `gorm:"column:owner_id;uniqueIndex:uk_yumi_gk_delivery_owner,priority:2"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;index:idx_yumi_gk_delivery_activity,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
DeliveryStatus string `gorm:"column:delivery_status;size:32;index:idx_yumi_gk_delivery_status,priority:1;index:idx_yumi_gk_delivery_activity,priority:2"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason;size:500"`
|
||||||
|
DeliverTime *time.Time `gorm:"column:deliver_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time;index:idx_yumi_gk_delivery_status,priority:2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGameKingDeliveryItem) TableName() string { return "yumi_game_king_delivery_item" }
|
||||||
222
internal/model/yumi_gift_challenge_models.go
Normal file
222
internal/model/yumi_gift_challenge_models.go
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// YumiGiftChallengeActivity 保存一次独立活动周期;总榜结算开始前允许调整活动配置,
|
||||||
|
// 已产生的用户任务、日榜结算和奖励项由各自快照保持历史语义。
|
||||||
|
type YumiGiftChallengeActivity struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityCode string `gorm:"column:activity_code"`
|
||||||
|
ActivityName string `gorm:"column:activity_name"`
|
||||||
|
ActivityDesc string `gorm:"column:activity_desc"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin"`
|
||||||
|
Timezone string `gorm:"column:time_zone"`
|
||||||
|
StartTime time.Time `gorm:"column:start_time"`
|
||||||
|
EndTime time.Time `gorm:"column:end_time"`
|
||||||
|
DailySettlementDelayMinutes int `gorm:"column:daily_settlement_delay_minutes"`
|
||||||
|
OverallSettlementDelayMinutes int `gorm:"column:overall_settlement_delay_minutes"`
|
||||||
|
OverallSettlementTime time.Time `gorm:"column:overall_settlement_time"`
|
||||||
|
DisplayTopN int `gorm:"column:display_top_n"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
OverallSettlementStatus string `gorm:"column:overall_settlement_status"`
|
||||||
|
Version int `gorm:"column:version"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeActivity) TableName() string { return "yumi_gift_challenge_activity" }
|
||||||
|
|
||||||
|
// YumiGiftChallengeTaskConfig 是活动固定三个每日任务槽位的定义。
|
||||||
|
type YumiGiftChallengeTaskConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
TaskCode string `gorm:"column:task_code"`
|
||||||
|
TaskType string `gorm:"column:task_type"`
|
||||||
|
TaskTitle string `gorm:"column:task_title"`
|
||||||
|
TaskDesc string `gorm:"column:task_desc"`
|
||||||
|
TargetValue Decimal24_2 `gorm:"column:target_value;type:decimal(24,2)"`
|
||||||
|
ResourceGroupID *int64 `gorm:"column:resource_group_id"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
SortOrder int `gorm:"column:sort_order"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeTaskConfig) TableName() string { return "yumi_gift_challenge_task_config" }
|
||||||
|
|
||||||
|
// YumiGiftChallengeRankReward 把一个连续名次区间绑定到一个奖励组。
|
||||||
|
type YumiGiftChallengeRankReward struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
PeriodType string `gorm:"column:period_type"`
|
||||||
|
StartRank int `gorm:"column:start_rank"`
|
||||||
|
EndRank int `gorm:"column:end_rank"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
RewardName string `gorm:"column:reward_name"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeRankReward) TableName() string { return "yumi_gift_challenge_rank_reward" }
|
||||||
|
|
||||||
|
// YumiGiftChallengeRewardSnapshot 是启用时冻结的完整奖励项,展示和实际发放均读取此表。
|
||||||
|
type YumiGiftChallengeRewardSnapshot struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
RewardConfigID int64 `gorm:"column:reward_config_id"`
|
||||||
|
RewardType string `gorm:"column:reward_type"`
|
||||||
|
DetailType string `gorm:"column:detail_type"`
|
||||||
|
Content string `gorm:"column:content"`
|
||||||
|
Quantity int64 `gorm:"column:quantity"`
|
||||||
|
SortOrder int `gorm:"column:sort_order"`
|
||||||
|
Remark string `gorm:"column:remark"`
|
||||||
|
Cover string `gorm:"column:cover"`
|
||||||
|
SourceURL string `gorm:"column:source_url"`
|
||||||
|
DisplayName string `gorm:"column:display_name"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeRewardSnapshot) TableName() string {
|
||||||
|
return "yumi_gift_challenge_reward_snapshot"
|
||||||
|
}
|
||||||
|
|
||||||
|
// YumiGiftChallengeGiftLedger 是原始送礼事件的活动账本,也是消费幂等边界。
|
||||||
|
type YumiGiftChallengeGiftLedger struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
SourceEventTrackID string `gorm:"column:source_event_track_id"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
GiftID int64 `gorm:"column:gift_id"`
|
||||||
|
GiftTab string `gorm:"column:gift_tab"`
|
||||||
|
Amount Decimal24_2 `gorm:"column:amount;type:decimal(24,2)"`
|
||||||
|
EventTime time.Time `gorm:"column:event_time"`
|
||||||
|
StatDate string `gorm:"column:stat_date;type:date"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeGiftLedger) TableName() string { return "yumi_gift_challenge_gift_ledger" }
|
||||||
|
|
||||||
|
// YumiGiftChallengeUserScore 保存活动总榜聚合,避免榜单查询扫描明细账本。
|
||||||
|
type YumiGiftChallengeUserScore struct {
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;primaryKey"`
|
||||||
|
UserID int64 `gorm:"column:user_id;primaryKey"`
|
||||||
|
Score Decimal24_2 `gorm:"column:score;type:decimal(24,2)"`
|
||||||
|
ScoreReachedTime time.Time `gorm:"column:score_reached_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeUserScore) TableName() string { return "yumi_gift_challenge_user_score" }
|
||||||
|
|
||||||
|
// YumiGiftChallengeUserDailyScore 保存活动时区自然日榜聚合。
|
||||||
|
type YumiGiftChallengeUserDailyScore struct {
|
||||||
|
ActivityID int64 `gorm:"column:activity_id;primaryKey"`
|
||||||
|
StatDate string `gorm:"column:stat_date;type:date;primaryKey"`
|
||||||
|
UserID int64 `gorm:"column:user_id;primaryKey"`
|
||||||
|
Score Decimal24_2 `gorm:"column:score;type:decimal(24,2)"`
|
||||||
|
ScoreReachedTime time.Time `gorm:"column:score_reached_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeUserDailyScore) TableName() string {
|
||||||
|
return "yumi_gift_challenge_user_daily_score"
|
||||||
|
}
|
||||||
|
|
||||||
|
// YumiGiftChallengeUserTaskDaily 冻结用户当天任务目标和奖励归属,并记录首次领取 requestId。
|
||||||
|
type YumiGiftChallengeUserTaskDaily struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
StatDate string `gorm:"column:stat_date;type:date"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
TaskConfigID int64 `gorm:"column:task_config_id"`
|
||||||
|
TaskCode string `gorm:"column:task_code"`
|
||||||
|
TaskType string `gorm:"column:task_type"`
|
||||||
|
TaskTitle string `gorm:"column:task_title"`
|
||||||
|
SortOrder int `gorm:"column:sort_order"`
|
||||||
|
TargetValue Decimal24_2 `gorm:"column:target_value;type:decimal(24,2)"`
|
||||||
|
ProgressValue Decimal24_2 `gorm:"column:progress_value;type:decimal(24,2)"`
|
||||||
|
CompletedTime *time.Time `gorm:"column:completed_time"`
|
||||||
|
ResourceGroupID *int64 `gorm:"column:resource_group_id"`
|
||||||
|
BusinessNo string `gorm:"column:business_no"`
|
||||||
|
ClaimRequestID string `gorm:"column:claim_request_id"`
|
||||||
|
DeliveryStatus string `gorm:"column:delivery_status"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason"`
|
||||||
|
ClaimedTime *time.Time `gorm:"column:claimed_time"`
|
||||||
|
DeliverTime *time.Time `gorm:"column:deliver_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeUserTaskDaily) TableName() string {
|
||||||
|
return "yumi_gift_challenge_user_task_daily"
|
||||||
|
}
|
||||||
|
|
||||||
|
// YumiGiftChallengePeriodSettlement 是包括空榜在内的周期门闩;PROCESSING 后禁止迟到写入。
|
||||||
|
type YumiGiftChallengePeriodSettlement struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
PeriodType string `gorm:"column:period_type"`
|
||||||
|
PeriodKey string `gorm:"column:period_key"`
|
||||||
|
StatDate *string `gorm:"column:stat_date;type:date"`
|
||||||
|
SnapshotDueTime time.Time `gorm:"column:snapshot_due_time"`
|
||||||
|
Status string `gorm:"column:status"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengePeriodSettlement) TableName() string {
|
||||||
|
return "yumi_gift_challenge_period_settlement"
|
||||||
|
}
|
||||||
|
|
||||||
|
// YumiGiftChallengeSettlement 是已经冻结的获奖 parent,先提交后再物化和发送奖励项。
|
||||||
|
type YumiGiftChallengeSettlement struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
PeriodType string `gorm:"column:period_type"`
|
||||||
|
PeriodKey string `gorm:"column:period_key"`
|
||||||
|
StatDate *string `gorm:"column:stat_date;type:date"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
RankNo int `gorm:"column:rank_no"`
|
||||||
|
Score Decimal24_2 `gorm:"column:score;type:decimal(24,2)"`
|
||||||
|
RankRewardID int64 `gorm:"column:rank_reward_id"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
BusinessNo string `gorm:"column:business_no"`
|
||||||
|
DeliveryStatus string `gorm:"column:delivery_status"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason"`
|
||||||
|
DeliverTime *time.Time `gorm:"column:deliver_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeSettlement) TableName() string { return "yumi_gift_challenge_settlement" }
|
||||||
|
|
||||||
|
// YumiGiftChallengeDeliveryItem 是实际发奖最小幂等单元,UNKNOWN 只允许人工核账处理。
|
||||||
|
type YumiGiftChallengeDeliveryItem struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
OwnerType string `gorm:"column:owner_type"`
|
||||||
|
OwnerID int64 `gorm:"column:owner_id"`
|
||||||
|
ActivityID int64 `gorm:"column:activity_id"`
|
||||||
|
UserID int64 `gorm:"column:user_id"`
|
||||||
|
RewardConfigID int64 `gorm:"column:reward_config_id"`
|
||||||
|
ResourceGroupID int64 `gorm:"column:resource_group_id"`
|
||||||
|
RewardType string `gorm:"column:reward_type"`
|
||||||
|
DetailType string `gorm:"column:detail_type"`
|
||||||
|
Content string `gorm:"column:content"`
|
||||||
|
Quantity int64 `gorm:"column:quantity"`
|
||||||
|
SortOrder int `gorm:"column:sort_order"`
|
||||||
|
Remark string `gorm:"column:remark"`
|
||||||
|
DeliveryStatus string `gorm:"column:delivery_status"`
|
||||||
|
RetryCount int `gorm:"column:retry_count"`
|
||||||
|
FailureReason string `gorm:"column:failure_reason"`
|
||||||
|
DeliverTime *time.Time `gorm:"column:deliver_time"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YumiGiftChallengeDeliveryItem) TableName() string {
|
||||||
|
return "yumi_gift_challenge_delivery_item"
|
||||||
|
}
|
||||||
81
internal/router/agora_error_log_routes.go
Normal file
81
internal/router/agora_error_log_routes.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/errorlog"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerAgoraErrorLogRoutes registers client report and admin page APIs for Agora errors.
|
||||||
|
func registerAgoraErrorLogRoutes(engine *gin.Engine, javaClient authGateway, service *errorlog.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/agora")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.POST("/error-log", func(c *gin.Context) {
|
||||||
|
raw, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, errorlog.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req errorlog.AgoraErrorReportRequest
|
||||||
|
if len(strings.TrimSpace(string(raw))) > 0 {
|
||||||
|
if err := json.Unmarshal(raw, &req); err != nil {
|
||||||
|
writeError(c, errorlog.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := service.ReportAgoraError(
|
||||||
|
c.Request.Context(),
|
||||||
|
mustAuthUser(c),
|
||||||
|
req,
|
||||||
|
errorlog.AgoraErrorReportContext{
|
||||||
|
ReqClient: c.GetHeader("Req-Client"),
|
||||||
|
ReqAppIntel: c.GetHeader("Req-App-Intel"),
|
||||||
|
ClientIP: resolveClientIP(c),
|
||||||
|
UserAgent: c.GetHeader("User-Agent"),
|
||||||
|
RawPayload: string(raw),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/app-system/error-logs/agora")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.GET("/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
bannedOnly, _ := strconv.ParseBool(strings.TrimSpace(c.DefaultQuery("bannedOnly", "false")))
|
||||||
|
resp, err := service.PageAgoraErrorLogs(c.Request.Context(), errorlog.AgoraErrorLogQuery{
|
||||||
|
SysOrigin: c.Query("sysOrigin"),
|
||||||
|
UserID: parseInt64(c.Query("userId")),
|
||||||
|
RoomID: c.Query("roomId"),
|
||||||
|
AgoraUID: c.Query("agoraUid"),
|
||||||
|
ErrorCodeType: c.Query("errorCodeType"),
|
||||||
|
ReasonType: c.Query("reasonType"),
|
||||||
|
BannedOnly: bannedOnly,
|
||||||
|
NetworkType: c.Query("networkType"),
|
||||||
|
StartTime: c.Query("startTime"),
|
||||||
|
EndTime: c.Query("endTime"),
|
||||||
|
Cursor: cursor,
|
||||||
|
Limit: limit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
77
internal/router/app_popup_routes.go
Normal file
77
internal/router/app_popup_routes.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/apppopup"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerAppPopupRoutes 注册 APP 进场弹窗接口。
|
||||||
|
func registerAppPopupRoutes(engine *gin.Engine, javaClient authGateway, service *apppopup.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/popups")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.POST("/entry", func(c *gin.Context) {
|
||||||
|
var req apppopup.EntryQueryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
writeError(c, apppopup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.QueryEntryPopups(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/app-system/home-popups")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.GET("/configs", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageAdminConfigs(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("scene"),
|
||||||
|
c.Query("popupKey"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/configs/save", func(c *gin.Context) {
|
||||||
|
var req apppopup.SaveAdminConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, apppopup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveAdminConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.DELETE("/configs/:id", func(c *gin.Context) {
|
||||||
|
id, _ := strconv.ParseInt(strings.TrimSpace(c.Param("id")), 10, 64)
|
||||||
|
if err := service.DeleteAdminConfig(c.Request.Context(), id); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"deleted": true})
|
||||||
|
})
|
||||||
|
}
|
||||||
183
internal/router/app_popup_routes_test.go
Normal file
183
internal/router/app_popup_routes_test.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/repo"
|
||||||
|
"chatapp3-golang/internal/service/apppopup"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appPopupAuthStub struct{}
|
||||||
|
|
||||||
|
func (appPopupAuthStub) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||||
|
return integration.UserCredential{UserID: 123, SysOrigin: "LIKEI"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (appPopupAuthStub) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
||||||
|
return integration.ConsoleAccount{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppPopupEntryRoute(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterAppPopupDB(t)
|
||||||
|
if err := db.Create(&model.AppPopupConfig{
|
||||||
|
ID: 1001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "invite",
|
||||||
|
Scene: "APP_ENTER",
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 100,
|
||||||
|
Version: 1,
|
||||||
|
ContentJSON: `{"title":"Invite friends","image":"https://cdn.example.com/invite.png"}`,
|
||||||
|
JumpType: "H5",
|
||||||
|
JumpURL: "/h5/app-invite/landing.html",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create config: %v", err)
|
||||||
|
}
|
||||||
|
service := apppopup.NewService(config.Config{}, db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, appPopupAuthStub{}, Services{AppPopup: service})
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"scene":"APP_ENTER","platform":"ios","appVersion":"3.12.0"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/app/popups/entry", body)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
queue, ok := data["queue"].([]any)
|
||||||
|
if !ok || len(queue) != 1 || queue[0] != "invite" {
|
||||||
|
t.Fatalf("queue = %#v, want [invite]", data["queue"])
|
||||||
|
}
|
||||||
|
invite, ok := data["invite"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("invite missing: %#v", data)
|
||||||
|
}
|
||||||
|
if invite["value"] != true || invite["limit"] != float64(7) {
|
||||||
|
t.Fatalf("invite = %#v, want value true limit 7", invite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppPopupEntryRouteRequiresAuth(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterAppPopupDB(t)
|
||||||
|
service := apppopup.NewService(config.Config{}, db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, appPopupAuthStub{}, Services{AppPopup: service})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/app/popups/entry", bytes.NewBufferString(`{}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
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 TestAppPopupEntryRouteAllowsEmptyBody(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterAppPopupDB(t)
|
||||||
|
if err := db.Create(&model.AppPopupConfig{
|
||||||
|
ID: 1001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "invite",
|
||||||
|
Scene: "APP_ENTER",
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 100,
|
||||||
|
Version: 1,
|
||||||
|
UserScopeType: "ALL",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create config: %v", err)
|
||||||
|
}
|
||||||
|
service := apppopup.NewService(config.Config{}, db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, appPopupAuthStub{}, Services{AppPopup: service})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/app/popups/entry", 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, want 200", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppPopupAdminRoutes(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterAppPopupDB(t)
|
||||||
|
service := apppopup.NewService(config.Config{}, db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, appPopupAuthStub{}, Services{AppPopup: service})
|
||||||
|
|
||||||
|
saveBody := bytes.NewBufferString(`{"sysOrigin":"LIKEI","popupKey":"invite","enabled":true,"limitDays":7,"description":"邀请弹窗"}`)
|
||||||
|
saveReq := httptest.NewRequest(http.MethodPost, "/app-system/home-popups/configs/save", saveBody)
|
||||||
|
saveReq.Header.Set("Authorization", "Bearer console-token")
|
||||||
|
saveReq.Header.Set("Content-Type", "application/json")
|
||||||
|
saveRec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(saveRec, saveReq)
|
||||||
|
if saveRec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("save status = %d body = %s", saveRec.Code, saveRec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
listReq := httptest.NewRequest(http.MethodGet, "/app-system/home-popups/configs?sysOrigin=LIKEI", nil)
|
||||||
|
listReq.Header.Set("Authorization", "Bearer console-token")
|
||||||
|
listRec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(listRec, listReq)
|
||||||
|
if listRec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("list status = %d body = %s", listRec.Code, listRec.Body.String())
|
||||||
|
}
|
||||||
|
var listPayload map[string]any
|
||||||
|
if err := json.Unmarshal(listRec.Body.Bytes(), &listPayload); err != nil {
|
||||||
|
t.Fatalf("decode list response: %v", err)
|
||||||
|
}
|
||||||
|
data := listPayload["data"].(map[string]any)
|
||||||
|
records := data["records"].([]any)
|
||||||
|
if len(records) != 1 {
|
||||||
|
t.Fatalf("records = %#v, want one", records)
|
||||||
|
}
|
||||||
|
record := records[0].(map[string]any)
|
||||||
|
id := record["id"].(string)
|
||||||
|
|
||||||
|
deleteReq := httptest.NewRequest(http.MethodDelete, "/app-system/home-popups/configs/"+id, nil)
|
||||||
|
deleteReq.Header.Set("Authorization", "Bearer console-token")
|
||||||
|
deleteRec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(deleteRec, deleteReq)
|
||||||
|
if deleteRec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("delete status = %d body = %s", deleteRec.Code, deleteRec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterAppPopupDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.AppPopupConfig{}); err != nil {
|
||||||
|
t.Fatalf("migrate app popup config: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
@ -96,7 +96,7 @@ func registerBaishunRoutes(
|
|||||||
consoleGroup := engine.Group("/operate/baishun-game")
|
consoleGroup := engine.Group("/operate/baishun-game")
|
||||||
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
consoleGroup.GET("/config", func(c *gin.Context) {
|
consoleGroup.GET("/config", func(c *gin.Context) {
|
||||||
resp, err := baishunService.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"))
|
resp, err := baishunService.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
@ -116,12 +116,26 @@ func registerBaishunRoutes(
|
|||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
|
consoleGroup.POST("/config/activate", func(c *gin.Context) {
|
||||||
|
var req baishun.ActivateBaishunProviderProfileRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := baishunService.ActivateProviderProfile(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
consoleGroup.GET("/page", func(c *gin.Context) {
|
consoleGroup.GET("/page", func(c *gin.Context) {
|
||||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
resp, err := baishunService.PageAdminGames(
|
resp, err := baishunService.PageAdminGames(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
c.Query("sysOrigin"),
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("profile"),
|
||||||
c.Query("keyword"),
|
c.Query("keyword"),
|
||||||
parseOptionalBool(c.Query("showcase")),
|
parseOptionalBool(c.Query("showcase")),
|
||||||
cursor,
|
cursor,
|
||||||
@ -139,6 +153,7 @@ func registerBaishunRoutes(
|
|||||||
resp, err := baishunService.PageCatalog(
|
resp, err := baishunService.PageCatalog(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
c.Query("sysOrigin"),
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("profile"),
|
||||||
c.Query("keyword"),
|
c.Query("keyword"),
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
@ -164,7 +179,7 @@ func registerBaishunRoutes(
|
|||||||
})
|
})
|
||||||
consoleGroup.DELETE("", func(c *gin.Context) {
|
consoleGroup.DELETE("", func(c *gin.Context) {
|
||||||
id, _ := strconv.ParseInt(strings.TrimSpace(c.Query("id")), 10, 64)
|
id, _ := strconv.ParseInt(strings.TrimSpace(c.Query("id")), 10, 64)
|
||||||
if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), id); err != nil {
|
if err := baishunService.DeleteAdminGame(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"), id); err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -189,7 +204,11 @@ func registerBaishunRoutes(
|
|||||||
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
writeError(c, baishun.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.VendorGameIDs)
|
defaultShowcase := true
|
||||||
|
if req.DefaultShowcase != nil {
|
||||||
|
defaultShowcase = *req.DefaultShowcase
|
||||||
|
}
|
||||||
|
resp, err := baishunService.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs, defaultShowcase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
@ -229,7 +248,7 @@ func registerBaishunRoutes(
|
|||||||
})
|
})
|
||||||
callbackGroup.POST("/report", func(c *gin.Context) {
|
callbackGroup.POST("/report", func(c *gin.Context) {
|
||||||
raw, payload := readRawPayload(c)
|
raw, payload := readRawPayload(c)
|
||||||
writeBaishunJSON(c, http.StatusOK, baishunService.HandleReport(c.Request.Context(), payload, raw))
|
writeBaishunJSON(c, http.StatusOK, baishunService.HandleReport(c.Request.Context(), mapToReportRequest(payload), raw))
|
||||||
})
|
})
|
||||||
callbackGroup.POST("/balance-info", func(c *gin.Context) {
|
callbackGroup.POST("/balance-info", func(c *gin.Context) {
|
||||||
raw, payload := readRawPayload(c)
|
raw, payload := readRawPayload(c)
|
||||||
@ -319,6 +338,16 @@ func mapToChangeBalanceRequest(payload map[string]any) baishun.BaishunChangeBala
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mapToReportRequest 把百顺 report 回调报文映射成服务层 DTO。
|
||||||
|
func mapToReportRequest(payload map[string]any) baishun.BaishunReportRequest {
|
||||||
|
return baishun.BaishunReportRequest{
|
||||||
|
ReportType: asString(payload["report_type"]),
|
||||||
|
ReportMsg: asMap(payload["report_msg"]),
|
||||||
|
UserID: asString(payload["user_id"]),
|
||||||
|
SSToken: asString(payload["ss_token"]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mapToBalanceInfoRequest 把百顺 balance-info 回调报文映射成服务层 DTO。
|
// mapToBalanceInfoRequest 把百顺 balance-info 回调报文映射成服务层 DTO。
|
||||||
func mapToBalanceInfoRequest(payload map[string]any) baishun.BaishunBalanceInfoRequest {
|
func mapToBalanceInfoRequest(payload map[string]any) baishun.BaishunBalanceInfoRequest {
|
||||||
return baishun.BaishunBalanceInfoRequest{
|
return baishun.BaishunBalanceInfoRequest{
|
||||||
@ -331,6 +360,26 @@ func mapToBalanceInfoRequest(payload map[string]any) baishun.BaishunBalanceInfoR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// asMap 宽松读取对象字段。
|
||||||
|
func asMap(value any) map[string]any {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
return typed
|
||||||
|
case string:
|
||||||
|
result := map[string]any{}
|
||||||
|
_ = json.Unmarshal([]byte(typed), &result)
|
||||||
|
return result
|
||||||
|
default:
|
||||||
|
body, err := json.Marshal(typed)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]any{}
|
||||||
|
}
|
||||||
|
result := map[string]any{}
|
||||||
|
_ = json.Unmarshal(body, &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// asString 宽松读取字符串字段。
|
// asString 宽松读取字符串字段。
|
||||||
func asString(value any) string {
|
func asString(value any) string {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
|
|||||||
56
internal/router/binance_recharge_routes.go
Normal file
56
internal/router/binance_recharge_routes.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/binancerecharge"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerBinanceRechargeRoutes registers H5 verification and admin config routes.
|
||||||
|
func registerBinanceRechargeRoutes(engine *gin.Engine, javaClient authGateway, service *binancerecharge.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/h5/binance-recharge")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.POST("/verify", func(c *gin.Context) {
|
||||||
|
var req binancerecharge.VerifyRechargeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, binancerecharge.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.VerifyAndRecharge(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/payment/binance")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req binancerecharge.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, binancerecharge.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
101
internal/router/first_recharge_reward_routes.go
Normal file
101
internal/router/first_recharge_reward_routes.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/firstrechargereward"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerFirstRechargeRewardRoutes registers app and admin first recharge reward APIs.
|
||||||
|
func registerFirstRechargeRewardRoutes(engine *gin.Engine, javaClient authGateway, service *firstrechargereward.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerFirstRechargeRewardAppGroup(engine.Group("/app/first-recharge-reward"), javaClient, service)
|
||||||
|
registerFirstRechargeRewardAppGroup(engine.Group("/app/h5/first-recharge-reward"), javaClient, service)
|
||||||
|
|
||||||
|
group := engine.Group("/resident-activity/first-recharge-reward")
|
||||||
|
group.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
group.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
group.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req firstrechargereward.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, firstrechargereward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
group.GET("/grant-record/page", func(c *gin.Context) {
|
||||||
|
resp, err := service.PageGrantRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("status"),
|
||||||
|
firstrechargereward.ParseInt64(c.Query("userId")),
|
||||||
|
int(firstrechargereward.ParseInt64(c.Query("cursor"))),
|
||||||
|
int(firstrechargereward.ParseInt64(c.Query("limit"))),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerFirstRechargeRewardAppGroup(group *gin.RouterGroup, javaClient authGateway, service *firstrechargereward.Service) {
|
||||||
|
group.Use(authMiddleware(javaClient))
|
||||||
|
group.GET("/home", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c), firstRechargeRewardRequestVersion(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstRechargeRewardRequestVersion(c *gin.Context) string {
|
||||||
|
return firstNonEmptyString(
|
||||||
|
c.Query("appVersion"),
|
||||||
|
c.Query("version"),
|
||||||
|
c.GetHeader("req-version"),
|
||||||
|
c.GetHeader("Req-Version"),
|
||||||
|
versionFromReqAppIntel(c.GetHeader("req-app-intel")),
|
||||||
|
versionFromReqAppIntel(c.GetHeader("Req-App-Intel")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionFromReqAppIntel(value string) string {
|
||||||
|
for _, item := range strings.Split(value, ";") {
|
||||||
|
parts := strings.SplitN(item, "=", 2)
|
||||||
|
if len(parts) == 2 && strings.EqualFold(strings.TrimSpace(parts[0]), "version") {
|
||||||
|
return strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmptyString(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if text := strings.TrimSpace(value); text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
253
internal/router/game_king_routes.go
Normal file
253
internal/router/game_king_routes.go
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/service/gameking"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerGameKingRoutes 注册 Yumi H5、运营后台和外部 cron 内部触发接口。
|
||||||
|
func registerGameKingRoutes(engine *gin.Engine, cfg config.Config, javaClient authGateway, service *gameking.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 := engine.Group("/app/h5/game-king/yumi")
|
||||||
|
h5.Use(authMiddleware(javaClient))
|
||||||
|
h5.GET("/detail", func(c *gin.Context) {
|
||||||
|
resp, err := service.Detail(c.Request.Context(), mustAuthUser(c), parseGameKingID(c.Query("activityId")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
h5.GET("/me", func(c *gin.Context) {
|
||||||
|
resp, err := service.Me(c.Request.Context(), mustAuthUser(c), parseGameKingID(c.Query("activityId")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
h5.POST("/draw", func(c *gin.Context) {
|
||||||
|
var req gameking.DrawRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, gameking.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.Draw(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
h5.GET("/ranking", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "30")))
|
||||||
|
resp, err := service.Ranking(c.Request.Context(), mustAuthUser(c), parseGameKingID(c.Query("activityId")), c.Query("rankingType"), c.Query("period"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
h5.GET("/draw-records", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.DrawRecords(c.Request.Context(), mustAuthUser(c), parseGameKingID(c.Query("activityId")), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
admin := engine.Group("/resident-activity/game-king")
|
||||||
|
admin.Use(consoleAuthMiddleware(javaClient), consoleMenuMiddleware(javaClient, "ResidentGameKingActivity"))
|
||||||
|
admin.GET("/list", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListActivities(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
admin.GET("/detail/:id", func(c *gin.Context) {
|
||||||
|
resp, err := service.ManageDetail(c.Request.Context(), parseGameKingID(c.Param("id")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
admin.POST("/save", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:edit"), func(c *gin.Context) {
|
||||||
|
var req gameking.SaveActivityRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, gameking.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveActivity(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
admin.PUT("/enable/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:enable"), func(c *gin.Context) {
|
||||||
|
raw, exists := c.GetQuery("enabled")
|
||||||
|
if !exists {
|
||||||
|
writeError(c, gameking.NewAppError(http.StatusBadRequest, "enabled_required", "enabled query is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enabled, err := parseGameKingBool(raw, "enabled")
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.EnableActivity(c.Request.Context(), parseGameKingID(c.Param("id")), enabled); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
admin.GET("/prizes/:id", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListPrizes(c.Request.Context(), parseGameKingID(c.Param("id")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
admin.PUT("/prizes/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:edit"), func(c *gin.Context) {
|
||||||
|
var req gameking.SavePrizesRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, gameking.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.SavePrizes(c.Request.Context(), parseGameKingID(c.Param("id")), req); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
admin.GET("/rank-rewards/:id", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListRankRewards(c.Request.Context(), parseGameKingID(c.Param("id")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
admin.PUT("/rank-rewards/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:edit"), func(c *gin.Context) {
|
||||||
|
var req gameking.SaveRankRewardsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, gameking.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.SaveRankRewards(c.Request.Context(), parseGameKingID(c.Param("id")), req); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
admin.GET("/draw-records/:id", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "100")))
|
||||||
|
resp, err := service.ManageDrawRecords(c.Request.Context(), parseGameKingID(c.Param("id")), c.Query("deliveryStatus"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
admin.POST("/draw/retry/:recordId", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:reconcile"), func(c *gin.Context) {
|
||||||
|
if err := service.RetryDraw(c.Request.Context(), parseGameKingID(c.Param("recordId"))); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
admin.GET("/settlement-records/:id", func(c *gin.Context) {
|
||||||
|
resp, err := service.SettlementRecords(c.Request.Context(), parseGameKingID(c.Param("id")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
admin.POST("/settlement/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:settle"), func(c *gin.Context) {
|
||||||
|
if err := service.Settle(c.Request.Context(), parseGameKingID(c.Param("id"))); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
admin.POST("/settlement/retry/:recordId", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:reconcile"), func(c *gin.Context) {
|
||||||
|
if err := service.RetrySettlement(c.Request.Context(), parseGameKingID(c.Param("recordId"))); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
admin.POST("/delivery-item/resolve/:itemId", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:reconcile"), func(c *gin.Context) {
|
||||||
|
raw, exists := c.GetQuery("delivered")
|
||||||
|
if !exists {
|
||||||
|
writeError(c, gameking.NewAppError(http.StatusBadRequest, "delivered_required", "delivered query is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delivered, err := parseGameKingBool(raw, "delivered")
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.ResolveUnknownDeliveryItem(c.Request.Context(), parseGameKingID(c.Param("itemId")), delivered); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
admin.POST("/backfill/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-game-king:backfill"), func(c *gin.Context) {
|
||||||
|
lastID := parseGameKingID(c.DefaultQuery("lastId", "0"))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "200")))
|
||||||
|
resp, err := service.Backfill(c.Request.Context(), parseGameKingID(c.Param("id")), lastID, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
internal := engine.Group("/internal/game-king")
|
||||||
|
// 结算入口会冻结榜单并产生真实奖励副作用,必须与 Yumi 礼物挑战保持相同的
|
||||||
|
// fail-closed 边界:服务端未配置密钥时直接拒绝,不能沿用历史内部接口的空密钥兼容行为。
|
||||||
|
internal.Use(requiredInternalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
|
internal.POST("/settle-due", func(c *gin.Context) {
|
||||||
|
resp, err := service.SettleDue(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGameKingID(value string) int64 {
|
||||||
|
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGameKingBool(value, field string) (bool, error) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
|
case "true", "1":
|
||||||
|
return true, nil
|
||||||
|
case "false", "0":
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
// resolve delivered=false 会立即重发,因此不能把拼写错误或空值默认为 false。
|
||||||
|
return false, gameking.NewAppError(http.StatusBadRequest, field+"_invalid", field+" must be true, false, 1 or 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,6 +46,8 @@ func registerGameOpenRoutes(engine *gin.Engine, cfg config.Config, service *game
|
|||||||
c.JSON(http.StatusOK, service.HandleSupplement(c.Request.Context(), req, string(raw)))
|
c.JSON(http.StatusOK, service.HandleSupplement(c.Request.Context(), req, string(raw)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
registerHotgameCallbackRoutes(engine, service)
|
||||||
|
|
||||||
internalGroup := engine.Group("/internal/game/open")
|
internalGroup := engine.Group("/internal/game/open")
|
||||||
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
internalGroup.GET("/info", func(c *gin.Context) {
|
internalGroup.GET("/info", func(c *gin.Context) {
|
||||||
@ -58,6 +60,34 @@ func registerGameOpenRoutes(engine *gin.Engine, cfg config.Config, service *game
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerHotgameCallbackRoutes(engine *gin.Engine, service *gameopen.GameOpenService) {
|
||||||
|
handleUserInfo := func(c *gin.Context) {
|
||||||
|
raw, _ := c.GetRawData()
|
||||||
|
var req gameopen.HotgameGetUserInfoRequest
|
||||||
|
if err := json.Unmarshal(raw, &req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gameopen.CallbackResponse{ErrorCode: 4001, ErrorMsg: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, service.HandleHotgameGetUserInfo(c.Request.Context(), req, string(raw)))
|
||||||
|
}
|
||||||
|
handleUpdateBalance := func(c *gin.Context) {
|
||||||
|
raw, _ := c.GetRawData()
|
||||||
|
var req gameopen.HotgameUpdateBalanceRequest
|
||||||
|
if err := json.Unmarshal(raw, &req); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gameopen.CallbackResponse{ErrorCode: 4001, ErrorMsg: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, service.HandleHotgameUpdateBalance(c.Request.Context(), req, string(raw)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 热游文档要求平台直接提供 /getUserInfo 和 /updateBalance,同时保留命名空间路径便于网关灰度。
|
||||||
|
engine.POST("/getUserInfo", handleUserInfo)
|
||||||
|
engine.POST("/updateBalance", handleUpdateBalance)
|
||||||
|
hotgameGroup := engine.Group("/game/hotgame")
|
||||||
|
hotgameGroup.POST("/getUserInfo", handleUserInfo)
|
||||||
|
hotgameGroup.POST("/updateBalance", handleUpdateBalance)
|
||||||
|
}
|
||||||
|
|
||||||
func resolvePublicBaseURL(c *gin.Context, cfg config.Config) string {
|
func resolvePublicBaseURL(c *gin.Context, cfg config.Config) string {
|
||||||
if value := strings.TrimRight(strings.TrimSpace(cfg.GameOpen.PublicBaseURL), "/"); value != "" {
|
if value := strings.TrimRight(strings.TrimSpace(cfg.GameOpen.PublicBaseURL), "/"); value != "" {
|
||||||
return value
|
return value
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
"chatapp3-golang/internal/common"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
@ -10,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// registerGameProviderRoutes 注册统一游戏厂商 app 路由。
|
// registerGameProviderRoutes 注册统一游戏厂商 app 路由。
|
||||||
func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry) {
|
func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry, visibility *gameprovider.VisibilityPolicy) {
|
||||||
if registry == nil {
|
if registry == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -18,20 +20,36 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
appGroup := engine.Group("/app/game")
|
appGroup := engine.Group("/app/game")
|
||||||
appGroup.Use(authMiddleware(javaClient))
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
appGroup.GET("/room/shortcut", func(c *gin.Context) {
|
appGroup.GET("/room/shortcut", func(c *gin.Context) {
|
||||||
resp, err := registry.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
user, visible, ok := gameListAccess(c, visibility)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
writeOK(c, []publicRoomGameItem{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := registry.ListShortcutGames(c.Request.Context(), user, c.Query("roomId"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, stripProvidersFromRoomGameItems(resp))
|
writeOK(c, publicRoomGameItems(resp))
|
||||||
})
|
})
|
||||||
appGroup.GET("/room/list", func(c *gin.Context) {
|
appGroup.GET("/room/list", func(c *gin.Context) {
|
||||||
resp, err := registry.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
user, visible, ok := gameListAccess(c, visibility)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := registry.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, stripProvidersFromRoomGameListResponse(resp))
|
writeOK(c, publicRoomGameListResponse(resp))
|
||||||
})
|
})
|
||||||
appGroup.GET("/state", func(c *gin.Context) {
|
appGroup.GET("/state", func(c *gin.Context) {
|
||||||
resp, err := registry.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
resp, err := registry.GetRoomState(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||||
@ -39,7 +57,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, stripProviderFromRoomState(resp))
|
writeOK(c, publicRoomState(resp))
|
||||||
})
|
})
|
||||||
appGroup.POST("/launch", func(c *gin.Context) {
|
appGroup.POST("/launch", func(c *gin.Context) {
|
||||||
var req gameprovider.LaunchRequest
|
var req gameprovider.LaunchRequest
|
||||||
@ -52,7 +70,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, stripProviderFromLaunch(resp))
|
writeOK(c, publicLaunch(resp))
|
||||||
})
|
})
|
||||||
appGroup.POST("/close", func(c *gin.Context) {
|
appGroup.POST("/close", func(c *gin.Context) {
|
||||||
var req gameprovider.CloseRequest
|
var req gameprovider.CloseRequest
|
||||||
@ -65,37 +83,61 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, stripProviderFromRoomState(resp))
|
writeOK(c, publicRoomState(resp))
|
||||||
})
|
})
|
||||||
|
|
||||||
providerGroup := engine.Group("/app/game/providers")
|
providerGroup := engine.Group("/app/game/providers")
|
||||||
providerGroup.Use(authMiddleware(javaClient))
|
providerGroup.Use(authMiddleware(javaClient))
|
||||||
providerGroup.GET("", func(c *gin.Context) {
|
providerGroup.GET("", func(c *gin.Context) {
|
||||||
|
_, visible, ok := gameListAccess(c, visibility)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
writeOK(c, gameprovider.ProviderListResponse{Items: []gameprovider.ProviderSummary{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
writeOK(c, registry.List())
|
writeOK(c, registry.List())
|
||||||
})
|
})
|
||||||
providerGroup.GET("/:provider/room/shortcut", func(c *gin.Context) {
|
providerGroup.GET("/:provider/room/shortcut", func(c *gin.Context) {
|
||||||
|
user, visible, ok := gameListAccess(c, visibility)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
writeOK(c, gin.H{"items": []publicRoomGameItem{}})
|
||||||
|
return
|
||||||
|
}
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
provider, ok := resolveGameProvider(c, registry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := provider.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
resp, err := provider.ListShortcutGames(c.Request.Context(), user, c.Query("roomId"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, gin.H{"items": resp})
|
writeOK(c, gin.H{"items": publicRoomGameItems(resp)})
|
||||||
})
|
})
|
||||||
providerGroup.GET("/:provider/room/list", func(c *gin.Context) {
|
providerGroup.GET("/:provider/room/list", func(c *gin.Context) {
|
||||||
|
user, visible, ok := gameListAccess(c, visibility)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}))
|
||||||
|
return
|
||||||
|
}
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
provider, ok := resolveGameProvider(c, registry)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := provider.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
resp, err := provider.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, publicRoomGameListResponse(resp))
|
||||||
})
|
})
|
||||||
providerGroup.GET("/:provider/state", func(c *gin.Context) {
|
providerGroup.GET("/:provider/state", func(c *gin.Context) {
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
provider, ok := resolveGameProvider(c, registry)
|
||||||
@ -107,7 +149,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, publicRoomState(resp))
|
||||||
})
|
})
|
||||||
providerGroup.POST("/:provider/launch", func(c *gin.Context) {
|
providerGroup.POST("/:provider/launch", func(c *gin.Context) {
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
provider, ok := resolveGameProvider(c, registry)
|
||||||
@ -119,12 +161,12 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := provider.LaunchGame(c.Request.Context(), mustAuthUser(c), req, resolveClientIP(c))
|
resp, err := launchProviderGame(c, registry, provider, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, publicLaunch(resp))
|
||||||
})
|
})
|
||||||
providerGroup.POST("/:provider/close", func(c *gin.Context) {
|
providerGroup.POST("/:provider/close", func(c *gin.Context) {
|
||||||
provider, ok := resolveGameProvider(c, registry)
|
provider, ok := resolveGameProvider(c, registry)
|
||||||
@ -141,10 +183,37 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
|||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, publicRoomState(resp))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gameListAccess(c *gin.Context, visibility *gameprovider.VisibilityPolicy) (common.AuthUser, bool, bool) {
|
||||||
|
user := mustAuthUser(c)
|
||||||
|
// iOS 审核和上架场景需要隐藏游戏入口;这里放在统一列表门禁最前面,避免继续请求 Java 区域/等级接口或 provider SQL。
|
||||||
|
if isIOSGameListClient(c) {
|
||||||
|
return user, false, true
|
||||||
|
}
|
||||||
|
if gameprovider.IsEmptyGameListUser(user.UserID) {
|
||||||
|
return user, false, true
|
||||||
|
}
|
||||||
|
if visibility == nil {
|
||||||
|
return user, true, true
|
||||||
|
}
|
||||||
|
// 游戏列表只在列表入口做展示门禁,结果里的区域继续向下传给 provider SQL 做 regions 过滤。
|
||||||
|
result, err := visibility.Evaluate(c.Request.Context(), user)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return user, false, false
|
||||||
|
}
|
||||||
|
user.RegionID = result.RegionID
|
||||||
|
user.RegionCode = result.RegionCode
|
||||||
|
return user, result.Allow, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIOSGameListClient(c *gin.Context) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(c.GetHeader("Req-Client")), "ios")
|
||||||
|
}
|
||||||
|
|
||||||
func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gameprovider.Provider, bool) {
|
func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gameprovider.Provider, bool) {
|
||||||
provider, err := registry.Resolve(c.Param("provider"))
|
provider, err := registry.Resolve(c.Param("provider"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -154,42 +223,189 @@ func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gamep
|
|||||||
return provider, true
|
return provider, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripProvidersFromRoomGameItems(items []gameprovider.RoomGameListItem) []gameprovider.RoomGameListItem {
|
func launchProviderGame(c *gin.Context, registry *gameprovider.Registry, provider gameprovider.Provider, req gameprovider.LaunchRequest) (*gameprovider.LaunchResponse, error) {
|
||||||
if len(items) == 0 {
|
user := mustAuthUser(c)
|
||||||
return []gameprovider.RoomGameListItem{}
|
clientIP := resolveClientIP(c)
|
||||||
|
resp, err := provider.LaunchGame(c.Request.Context(), user, req, clientIP)
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
result := make([]gameprovider.RoomGameListItem, 0, len(items))
|
if !isGameNotFoundError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 旧客户端只能把热游列表项伪装成 LEADER 进入已有 WebView;这里再按 gameId 回到真实 provider,避免 LEADER 路由找不到 hg_*。
|
||||||
|
return registry.LaunchGame(c.Request.Context(), user, req, clientIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGameNotFoundError(err error) bool {
|
||||||
|
var appErr *common.AppError
|
||||||
|
return errors.As(err, &appErr) && appErr.Code == "game_not_found"
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicRoomGameItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
GameID string `json:"gameId"`
|
||||||
|
GameType string `json:"gameType,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
VendorType string `json:"vendorType,omitempty"`
|
||||||
|
ProviderGameID string `json:"providerGameId,omitempty"`
|
||||||
|
VendorGameID string `json:"vendorGameId,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Cover string `json:"cover"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Sort int64 `json:"sort,omitempty"`
|
||||||
|
LaunchMode string `json:"launchMode,omitempty"`
|
||||||
|
FullScreen bool `json:"fullScreen"`
|
||||||
|
GameMode int `json:"gameMode,omitempty"`
|
||||||
|
SafeHeight int `json:"safeHeight,omitempty"`
|
||||||
|
Orientation int `json:"orientation,omitempty"`
|
||||||
|
PackageVersion string `json:"packageVersion,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
LaunchParams map[string]any `json:"launchParams,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicRoomGameList struct {
|
||||||
|
Items []publicRoomGameItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicRoomStateResponse struct {
|
||||||
|
RoomID string `json:"roomId"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
VendorType string `json:"vendorType,omitempty"`
|
||||||
|
GameSessionID string `json:"gameSessionId,omitempty"`
|
||||||
|
CurrentGameID string `json:"currentGameId,omitempty"`
|
||||||
|
CurrentProviderGameID string `json:"currentProviderGameId,omitempty"`
|
||||||
|
CurrentVendorGameID string `json:"currentVendorGameId,omitempty"`
|
||||||
|
CurrentGameName string `json:"currentGameName,omitempty"`
|
||||||
|
CurrentGameCover string `json:"currentGameCover,omitempty"`
|
||||||
|
HostUserID int64 `json:"hostUserId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicLaunchResponse struct {
|
||||||
|
ID int64 `json:"id,omitempty"`
|
||||||
|
GameSessionID string `json:"gameSessionId"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
VendorType string `json:"vendorType,omitempty"`
|
||||||
|
GameID string `json:"gameId"`
|
||||||
|
ProviderGameID string `json:"providerGameId,omitempty"`
|
||||||
|
VendorGameID string `json:"vendorGameId,omitempty"`
|
||||||
|
Entry gameprovider.LaunchEntry `json:"entry"`
|
||||||
|
LaunchConfig any `json:"launchConfig,omitempty"`
|
||||||
|
BridgeConfig any `json:"bridgeConfig,omitempty"`
|
||||||
|
RoomState publicRoomStateResponse `json:"roomState"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicRoomGameItems(items []gameprovider.RoomGameListItem) []publicRoomGameItem {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return []publicRoomGameItem{}
|
||||||
|
}
|
||||||
|
result := make([]publicRoomGameItem, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
item.Provider = ""
|
provider := appCompatProviderName(item.Provider)
|
||||||
result = append(result, item)
|
gameType := appCompatProviderName(item.GameType)
|
||||||
|
result = append(result, publicRoomGameItem{
|
||||||
|
ID: item.ID,
|
||||||
|
GameID: item.GameID,
|
||||||
|
GameType: gameType,
|
||||||
|
Provider: provider,
|
||||||
|
VendorType: provider,
|
||||||
|
ProviderGameID: item.ProviderGameID,
|
||||||
|
VendorGameID: item.ProviderGameID,
|
||||||
|
Name: item.Name,
|
||||||
|
Cover: item.Cover,
|
||||||
|
Category: item.Category,
|
||||||
|
Sort: item.Sort,
|
||||||
|
LaunchMode: item.LaunchMode,
|
||||||
|
FullScreen: item.FullScreen,
|
||||||
|
GameMode: item.GameMode,
|
||||||
|
SafeHeight: item.SafeHeight,
|
||||||
|
Orientation: item.Orientation,
|
||||||
|
PackageVersion: item.PackageVersion,
|
||||||
|
Status: item.Status,
|
||||||
|
LaunchParams: publicLaunchParams(item.LaunchParams),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripProvidersFromRoomGameListResponse(resp *gameprovider.RoomGameListResponse) *gameprovider.RoomGameListResponse {
|
func publicLaunchParams(params map[string]any) map[string]any {
|
||||||
if resp == nil {
|
if len(params) == 0 {
|
||||||
return &gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}
|
return nil
|
||||||
}
|
}
|
||||||
return &gameprovider.RoomGameListResponse{
|
result := make(map[string]any, len(params))
|
||||||
Items: stripProvidersFromRoomGameItems(resp.Items),
|
for key, value := range params {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(key), "gameType") {
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
result[key] = appCompatProviderName(text)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicRoomGameListResponse(resp *gameprovider.RoomGameListResponse) *publicRoomGameList {
|
||||||
|
if resp == nil {
|
||||||
|
return &publicRoomGameList{Items: []publicRoomGameItem{}}
|
||||||
|
}
|
||||||
|
return &publicRoomGameList{
|
||||||
|
Items: publicRoomGameItems(resp.Items),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripProviderFromRoomState(resp *gameprovider.RoomStateResponse) *gameprovider.RoomStateResponse {
|
func publicRoomState(resp *gameprovider.RoomStateResponse) *publicRoomStateResponse {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
copied := *resp
|
return &publicRoomStateResponse{
|
||||||
copied.Provider = ""
|
RoomID: resp.RoomID,
|
||||||
return &copied
|
State: resp.State,
|
||||||
|
Provider: appCompatProviderName(resp.Provider),
|
||||||
|
VendorType: appCompatProviderName(resp.Provider),
|
||||||
|
GameSessionID: resp.GameSessionID,
|
||||||
|
CurrentGameID: resp.CurrentGameID,
|
||||||
|
CurrentProviderGameID: resp.CurrentProviderGameID,
|
||||||
|
CurrentVendorGameID: resp.CurrentProviderGameID,
|
||||||
|
CurrentGameName: resp.CurrentGameName,
|
||||||
|
CurrentGameCover: resp.CurrentGameCover,
|
||||||
|
HostUserID: resp.HostUserID,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripProviderFromLaunch(resp *gameprovider.LaunchResponse) *gameprovider.LaunchResponse {
|
func publicLaunch(resp *gameprovider.LaunchResponse) *publicLaunchResponse {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
copied := *resp
|
roomState := publicRoomState(&resp.RoomState)
|
||||||
copied.Provider = ""
|
if roomState == nil {
|
||||||
copied.RoomState = *stripProviderFromRoomState(&resp.RoomState)
|
roomState = &publicRoomStateResponse{}
|
||||||
return &copied
|
}
|
||||||
|
return &publicLaunchResponse{
|
||||||
|
ID: resp.ID,
|
||||||
|
GameSessionID: resp.GameSessionID,
|
||||||
|
Provider: appCompatProviderName(resp.Provider),
|
||||||
|
VendorType: appCompatProviderName(resp.Provider),
|
||||||
|
GameID: resp.GameID,
|
||||||
|
ProviderGameID: resp.ProviderGameID,
|
||||||
|
VendorGameID: resp.ProviderGameID,
|
||||||
|
Entry: resp.Entry,
|
||||||
|
LaunchConfig: resp.LaunchConfig,
|
||||||
|
BridgeConfig: resp.LaunchConfig,
|
||||||
|
RoomState: *roomState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appCompatProviderName(provider string) string {
|
||||||
|
// 客户端历史上只认识 LEADER,不认识内部厂商名 LINGXIAN,所以出参继续保持旧字段值。
|
||||||
|
if strings.EqualFold(strings.TrimSpace(provider), "LINGXIAN") {
|
||||||
|
return "LEADER"
|
||||||
|
}
|
||||||
|
// 旧客户端没有 HOTGAME 页面;热游先按 LEADER 展示并进入已有 WebView,launch 路由再按 hg_* 分发回真实热游 provider。
|
||||||
|
switch strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(provider), "_", "")) {
|
||||||
|
case "HOTGAME", "REYOU", "LALU":
|
||||||
|
return "LEADER"
|
||||||
|
}
|
||||||
|
return provider
|
||||||
}
|
}
|
||||||
|
|||||||
509
internal/router/game_provider_routes_test.go
Normal file
509
internal/router/game_provider_routes_test.go
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) {
|
||||||
|
resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{
|
||||||
|
Items: []gameprovider.RoomGameListItem{
|
||||||
|
{
|
||||||
|
ID: 9527,
|
||||||
|
GameID: "bs_1046",
|
||||||
|
GameType: "BAISHUN",
|
||||||
|
Provider: "BAISHUN",
|
||||||
|
ProviderGameID: "1046",
|
||||||
|
Name: "Lucky77Pro",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(resp.Items) != 1 {
|
||||||
|
t.Fatalf("items = %d, want 1", len(resp.Items))
|
||||||
|
}
|
||||||
|
item := resp.Items[0]
|
||||||
|
if item.Provider != "BAISHUN" {
|
||||||
|
t.Fatalf("provider = %q, want BAISHUN", item.Provider)
|
||||||
|
}
|
||||||
|
if item.VendorType != "BAISHUN" {
|
||||||
|
t.Fatalf("vendorType = %q, want BAISHUN", item.VendorType)
|
||||||
|
}
|
||||||
|
if item.ProviderGameID != "1046" {
|
||||||
|
t.Fatalf("providerGameId = %q, want 1046", item.ProviderGameID)
|
||||||
|
}
|
||||||
|
if item.VendorGameID != "1046" {
|
||||||
|
t.Fatalf("vendorGameId = %q, want 1046", item.VendorGameID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicRoomGameListResponseMapsLingxianToLeaderForOldClients(t *testing.T) {
|
||||||
|
resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{
|
||||||
|
Items: []gameprovider.RoomGameListItem{
|
||||||
|
{
|
||||||
|
ID: 9528,
|
||||||
|
GameID: "1090",
|
||||||
|
GameType: "LINGXIAN",
|
||||||
|
Provider: "LINGXIAN",
|
||||||
|
ProviderGameID: "1090",
|
||||||
|
Name: "Lingxian Game",
|
||||||
|
LaunchParams: map[string]any{"gameType": "LINGXIAN", "screenMode": "half"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(resp.Items) != 1 {
|
||||||
|
t.Fatalf("items = %d, want 1", len(resp.Items))
|
||||||
|
}
|
||||||
|
item := resp.Items[0]
|
||||||
|
if item.Provider != "LEADER" {
|
||||||
|
t.Fatalf("provider = %q, want LEADER", item.Provider)
|
||||||
|
}
|
||||||
|
if item.GameType != "LEADER" {
|
||||||
|
t.Fatalf("gameType = %q, want LEADER", item.GameType)
|
||||||
|
}
|
||||||
|
if item.VendorType != "LEADER" {
|
||||||
|
t.Fatalf("vendorType = %q, want LEADER", item.VendorType)
|
||||||
|
}
|
||||||
|
if got := item.LaunchParams["gameType"]; got != "LEADER" {
|
||||||
|
t.Fatalf("launchParams.gameType = %v, want LEADER", got)
|
||||||
|
}
|
||||||
|
if got := item.LaunchParams["screenMode"]; got != "half" {
|
||||||
|
t.Fatalf("launchParams.screenMode = %v, want half", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicRoomGameListResponseMapsHotgameAliasesToLeaderForOldClients(t *testing.T) {
|
||||||
|
for _, alias := range []string{"HOTGAME", "HOT_GAME", "REYOU", "LALU"} {
|
||||||
|
resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{
|
||||||
|
Items: []gameprovider.RoomGameListItem{
|
||||||
|
{
|
||||||
|
ID: 9530,
|
||||||
|
GameID: "hg_1",
|
||||||
|
GameType: alias,
|
||||||
|
Provider: alias,
|
||||||
|
ProviderGameID: "1",
|
||||||
|
Name: "Hotgame",
|
||||||
|
LaunchParams: map[string]any{"gameType": alias, "screenMode": "seven"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(resp.Items) != 1 {
|
||||||
|
t.Fatalf("%s items = %d, want 1", alias, len(resp.Items))
|
||||||
|
}
|
||||||
|
item := resp.Items[0]
|
||||||
|
if item.Provider != "LEADER" {
|
||||||
|
t.Fatalf("%s provider = %q, want LEADER", alias, item.Provider)
|
||||||
|
}
|
||||||
|
if item.GameType != "LEADER" {
|
||||||
|
t.Fatalf("%s gameType = %q, want LEADER", alias, item.GameType)
|
||||||
|
}
|
||||||
|
if item.VendorType != "LEADER" {
|
||||||
|
t.Fatalf("%s vendorType = %q, want LEADER", alias, item.VendorType)
|
||||||
|
}
|
||||||
|
if got := item.LaunchParams["gameType"]; got != "LEADER" {
|
||||||
|
t.Fatalf("%s launchParams.gameType = %v, want LEADER", alias, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) {
|
||||||
|
launchConfig := map[string]any{"code": "launch-code"}
|
||||||
|
resp := publicLaunch(&gameprovider.LaunchResponse{
|
||||||
|
ID: 9527,
|
||||||
|
GameSessionID: "bs_room_1",
|
||||||
|
Provider: "BAISHUN",
|
||||||
|
GameID: "bs_1046",
|
||||||
|
ProviderGameID: "1046",
|
||||||
|
LaunchConfig: launchConfig,
|
||||||
|
RoomState: gameprovider.RoomStateResponse{
|
||||||
|
RoomID: "room-1",
|
||||||
|
State: "PLAYING",
|
||||||
|
Provider: "BAISHUN",
|
||||||
|
CurrentProviderGameID: "1046",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if resp.Provider != "BAISHUN" {
|
||||||
|
t.Fatalf("provider = %q, want BAISHUN", resp.Provider)
|
||||||
|
}
|
||||||
|
if resp.VendorType != "BAISHUN" {
|
||||||
|
t.Fatalf("vendorType = %q, want BAISHUN", resp.VendorType)
|
||||||
|
}
|
||||||
|
if resp.VendorGameID != "1046" {
|
||||||
|
t.Fatalf("vendorGameId = %q, want 1046", resp.VendorGameID)
|
||||||
|
}
|
||||||
|
bridgeConfig, ok := resp.BridgeConfig.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("bridgeConfig type = %T, want map[string]any", resp.BridgeConfig)
|
||||||
|
}
|
||||||
|
if got := bridgeConfig["code"]; got != "launch-code" {
|
||||||
|
t.Fatalf("bridgeConfig[code] = %v, want launch-code", got)
|
||||||
|
}
|
||||||
|
if resp.RoomState.Provider != "BAISHUN" {
|
||||||
|
t.Fatalf("roomState.provider = %q, want BAISHUN", resp.RoomState.Provider)
|
||||||
|
}
|
||||||
|
if resp.RoomState.CurrentVendorGameID != "1046" {
|
||||||
|
t.Fatalf("roomState.currentVendorGameId = %q, want 1046", resp.RoomState.CurrentVendorGameID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicLaunchMapsLingxianToLeaderForOldClients(t *testing.T) {
|
||||||
|
resp := publicLaunch(&gameprovider.LaunchResponse{
|
||||||
|
ID: 9528,
|
||||||
|
GameSessionID: "lx_1_1090",
|
||||||
|
Provider: "LINGXIAN",
|
||||||
|
GameID: "1090",
|
||||||
|
ProviderGameID: "1090",
|
||||||
|
LaunchConfig: map[string]any{"uid": "user-1"},
|
||||||
|
RoomState: gameprovider.RoomStateResponse{
|
||||||
|
RoomID: "room-1",
|
||||||
|
State: "PLAYING",
|
||||||
|
Provider: "LINGXIAN",
|
||||||
|
CurrentProviderGameID: "1090",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if resp.Provider != "LEADER" {
|
||||||
|
t.Fatalf("provider = %q, want LEADER", resp.Provider)
|
||||||
|
}
|
||||||
|
if resp.VendorType != "LEADER" {
|
||||||
|
t.Fatalf("vendorType = %q, want LEADER", resp.VendorType)
|
||||||
|
}
|
||||||
|
if resp.RoomState.Provider != "LEADER" {
|
||||||
|
t.Fatalf("roomState.provider = %q, want LEADER", resp.RoomState.Provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameProviderRoutesReturnRoomListForTurkeyRegionUser(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
provider := &routeStubProvider{}
|
||||||
|
engine := gin.New()
|
||||||
|
registerGameProviderRoutes(
|
||||||
|
engine,
|
||||||
|
routeStubAuthGateway{},
|
||||||
|
gameprovider.NewRegistry(provider),
|
||||||
|
gameprovider.NewVisibilityPolicy(
|
||||||
|
routeStubVisibilityGateway{
|
||||||
|
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/game/room/list?roomId=1", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Body struct {
|
||||||
|
Items []publicRoomGameItem `json:"items"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
if len(payload.Body.Items) != 1 {
|
||||||
|
t.Fatalf("items = %d, want 1", len(payload.Body.Items))
|
||||||
|
}
|
||||||
|
if provider.listRoomCalls != 1 {
|
||||||
|
t.Fatalf("provider list calls = %d, want 1", provider.listRoomCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameProviderRoutesReturnEmptyRoomListForIOSClient(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
provider := &routeStubProvider{}
|
||||||
|
engine := gin.New()
|
||||||
|
registerGameProviderRoutes(
|
||||||
|
engine,
|
||||||
|
routeStubAuthGateway{},
|
||||||
|
gameprovider.NewRegistry(provider),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/game/room/list?roomId=1", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
req.Header.Set("Req-Client", "ios")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Body struct {
|
||||||
|
Items []publicRoomGameItem `json:"items"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
if len(payload.Body.Items) != 0 {
|
||||||
|
t.Fatalf("items = %d, want 0", len(payload.Body.Items))
|
||||||
|
}
|
||||||
|
if provider.listRoomCalls != 0 {
|
||||||
|
t.Fatalf("provider list calls = %d, want 0", provider.listRoomCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameProviderRoutesReturnEmptyShortcutForIOSClient(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
provider := &routeStubProvider{}
|
||||||
|
engine := gin.New()
|
||||||
|
registerGameProviderRoutes(
|
||||||
|
engine,
|
||||||
|
routeStubAuthGateway{},
|
||||||
|
gameprovider.NewRegistry(provider),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/game/room/shortcut?roomId=1", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
req.Header.Set("Req-Client", " iOS ")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Body []publicRoomGameItem `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
if len(payload.Body) != 0 {
|
||||||
|
t.Fatalf("items = %d, want 0", len(payload.Body))
|
||||||
|
}
|
||||||
|
if provider.shortcutCalls != 0 {
|
||||||
|
t.Fatalf("provider shortcut calls = %d, want 0", provider.shortcutCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameProviderRoutesReturnEmptyProvidersForIOSClient(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
engine := gin.New()
|
||||||
|
registerGameProviderRoutes(
|
||||||
|
engine,
|
||||||
|
routeStubAuthGateway{},
|
||||||
|
gameprovider.NewRegistry(&routeStubProvider{}),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/game/providers", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
req.Header.Set("Req-Client", "IOS")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Body struct {
|
||||||
|
Items []gameprovider.ProviderSummary `json:"items"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
if len(payload.Body.Items) != 0 {
|
||||||
|
t.Fatalf("providers = %d, want 0", len(payload.Body.Items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameProviderRoutesReturnProvidersWhenVisibilityPolicyAllows(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
engine := gin.New()
|
||||||
|
registerGameProviderRoutes(
|
||||||
|
engine,
|
||||||
|
routeStubAuthGateway{},
|
||||||
|
gameprovider.NewRegistry(&routeStubProvider{}),
|
||||||
|
gameprovider.NewVisibilityPolicy(
|
||||||
|
routeStubVisibilityGateway{
|
||||||
|
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/game/providers", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
req.Header.Set("X-Real-IP", "88.255.1.1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Body struct {
|
||||||
|
Items []gameprovider.ProviderSummary `json:"items"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
if len(payload.Body.Items) != 1 {
|
||||||
|
t.Fatalf("providers = %d, want 1", len(payload.Body.Items))
|
||||||
|
}
|
||||||
|
if payload.Body.Items[0].Key != "TEST" {
|
||||||
|
t.Fatalf("provider key = %q, want TEST", payload.Body.Items[0].Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderLaunchFallsBackToRegistryWhenLegacyProviderCannotFindGame(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
engine := gin.New()
|
||||||
|
registerGameProviderRoutes(
|
||||||
|
engine,
|
||||||
|
routeStubAuthGateway{},
|
||||||
|
gameprovider.NewRegistry(
|
||||||
|
&routeLaunchStubProvider{key: "LINGXIAN"},
|
||||||
|
&routeLaunchStubProvider{key: "HOTGAME", matchGameID: "hg_1"},
|
||||||
|
),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/app/game/providers/LEADER/launch", strings.NewReader(`{"roomId":"room-1","gameId":"hg_1"}`))
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Body publicLaunchResponse `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("response json error = %v: %s", err, rec.Body.String())
|
||||||
|
}
|
||||||
|
if payload.Body.GameID != "hg_1" {
|
||||||
|
t.Fatalf("gameId = %q, want hg_1", payload.Body.GameID)
|
||||||
|
}
|
||||||
|
if payload.Body.Provider != "LEADER" {
|
||||||
|
t.Fatalf("provider = %q, want LEADER for old-client compat", payload.Body.Provider)
|
||||||
|
}
|
||||||
|
if payload.Body.GameSessionID != "hg-session" {
|
||||||
|
t.Fatalf("gameSessionId = %q, want hg-session", payload.Body.GameSessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeStubAuthGateway struct{}
|
||||||
|
|
||||||
|
func (routeStubAuthGateway) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||||
|
return integration.UserCredential{UserID: integration.Int64Value(1001), SysOrigin: "LIKEI"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (routeStubAuthGateway) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
||||||
|
return integration.ConsoleAccount{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeStubVisibilityGateway struct {
|
||||||
|
region integration.UserRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s routeStubVisibilityGateway) GetUserRegion(context.Context, int64, string) (integration.UserRegion, error) {
|
||||||
|
return s.region, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeStubProvider struct {
|
||||||
|
listRoomCalls int
|
||||||
|
shortcutCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeStubProvider) Key() string { return "TEST" }
|
||||||
|
|
||||||
|
func (p *routeStubProvider) DisplayName() string { return "Test Provider" }
|
||||||
|
|
||||||
|
func (p *routeStubProvider) SupportsLaunch(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeStubProvider) ListShortcutGames(context.Context, gameprovider.AuthUser, string) ([]gameprovider.RoomGameListItem, error) {
|
||||||
|
p.shortcutCalls++
|
||||||
|
return []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeStubProvider) ListRoomGames(context.Context, gameprovider.AuthUser, string, string) (*gameprovider.RoomGameListResponse, error) {
|
||||||
|
p.listRoomCalls++
|
||||||
|
return &gameprovider.RoomGameListResponse{
|
||||||
|
Items: []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeStubProvider) GetRoomState(context.Context, gameprovider.AuthUser, string) (*gameprovider.RoomStateResponse, error) {
|
||||||
|
return &gameprovider.RoomStateResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeStubProvider) LaunchGame(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest, string) (*gameprovider.LaunchResponse, error) {
|
||||||
|
return nil, common.NewAppError(http.StatusBadRequest, "unsupported", "unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeStubProvider) CloseGame(context.Context, gameprovider.AuthUser, gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
||||||
|
return &gameprovider.RoomStateResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type routeLaunchStubProvider struct {
|
||||||
|
key string
|
||||||
|
matchGameID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) Key() string { return p.key }
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) DisplayName() string { return p.key }
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) SupportsLaunch(_ context.Context, _ gameprovider.AuthUser, req gameprovider.LaunchRequest) (bool, error) {
|
||||||
|
return p.matchGameID != "" && req.GameID == p.matchGameID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) ListShortcutGames(context.Context, gameprovider.AuthUser, string) ([]gameprovider.RoomGameListItem, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) ListRoomGames(context.Context, gameprovider.AuthUser, string, string) (*gameprovider.RoomGameListResponse, error) {
|
||||||
|
return &gameprovider.RoomGameListResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) GetRoomState(context.Context, gameprovider.AuthUser, string) (*gameprovider.RoomStateResponse, error) {
|
||||||
|
return &gameprovider.RoomStateResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) LaunchGame(_ context.Context, _ gameprovider.AuthUser, req gameprovider.LaunchRequest, _ string) (*gameprovider.LaunchResponse, error) {
|
||||||
|
if p.matchGameID == "" || req.GameID != p.matchGameID {
|
||||||
|
return nil, common.NewAppError(http.StatusNotFound, "game_not_found", "game not found")
|
||||||
|
}
|
||||||
|
return &gameprovider.LaunchResponse{
|
||||||
|
GameSessionID: "hg-session",
|
||||||
|
Provider: p.key,
|
||||||
|
GameID: req.GameID,
|
||||||
|
ProviderGameID: "1",
|
||||||
|
Entry: gameprovider.LaunchEntry{LaunchMode: "H5_REMOTE"},
|
||||||
|
RoomState: gameprovider.RoomStateResponse{
|
||||||
|
RoomID: req.RoomID,
|
||||||
|
State: "PLAYING",
|
||||||
|
Provider: p.key,
|
||||||
|
GameSessionID: "hg-session",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *routeLaunchStubProvider) CloseGame(context.Context, gameprovider.AuthUser, gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
||||||
|
return &gameprovider.RoomStateResponse{}, nil
|
||||||
|
}
|
||||||
55
internal/router/host_center_routes.go
Normal file
55
internal/router/host_center_routes.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerHostCenterRoutes(engine *gin.Engine, service *hostcenter.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
group := engine.Group("/app/h5/host-center")
|
||||||
|
group.GET("/policy", func(c *gin.Context) {
|
||||||
|
token := strings.TrimSpace(queryValuePreservePlus(c.Request.URL.RawQuery, "token"))
|
||||||
|
if token == "" {
|
||||||
|
writeError(c, hostcenter.NewAppError(http.StatusUnauthorized, "missing_token", "token is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.GetAnchorPolicy(c.Request.Context(), hostcenter.AnchorPolicyRequest{
|
||||||
|
Token: token,
|
||||||
|
PolicyType: c.Query("policyType"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryValuePreservePlus(rawQuery, name string) string {
|
||||||
|
for _, part := range strings.Split(rawQuery, "&") {
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, value, _ := strings.Cut(part, "=")
|
||||||
|
decodedKey, err := url.QueryUnescape(key)
|
||||||
|
if err != nil || decodedKey != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
decodedValue, err := url.QueryUnescape(strings.ReplaceAll(value, "+", "%2B"))
|
||||||
|
if err != nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return decodedValue
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
10
internal/router/host_center_routes_test.go
Normal file
10
internal/router/host_center_routes_test.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestQueryValuePreservePlusKeepsTokenBase64Plus(t *testing.T) {
|
||||||
|
got := queryValuePreservePlus("lang=en&token=abc+def%2Bghi", "token")
|
||||||
|
if got != "abc+def+ghi" {
|
||||||
|
t.Fatalf("token = %q, want abc+def+ghi", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
internal/router/hotgame_routes.go
Normal file
115
internal/router/hotgame_routes.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/service/hotgame"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerHotgameRoutes 注册热游后台管理类 Go 接口。
|
||||||
|
func registerHotgameRoutes(engine *gin.Engine, javaClient authGateway, service *hotgame.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
consoleGroup := engine.Group("/operate/hotgame-game")
|
||||||
|
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
consoleGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/config", func(c *gin.Context) {
|
||||||
|
var req hotgame.SaveProviderConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveProviderConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/config/activate", func(c *gin.Context) {
|
||||||
|
var req hotgame.ActivateProviderProfileRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ActivateProviderProfile(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.GET("/catalog/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageCatalog(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("profile"),
|
||||||
|
c.Query("keyword"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/catalog/fetch", func(c *gin.Context) {
|
||||||
|
var req hotgame.SyncCatalogRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.FetchAdminCatalog(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/import-catalog", func(c *gin.Context) {
|
||||||
|
var req hotgame.SyncCatalogRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaultShowcase := true
|
||||||
|
if req.DefaultShowcase != nil {
|
||||||
|
defaultShowcase = *req.DefaultShowcase
|
||||||
|
}
|
||||||
|
resp, err := service.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs, defaultShowcase)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/sync", func(c *gin.Context) {
|
||||||
|
var req hotgame.SyncCatalogRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SyncAdminGames(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
|
|
||||||
@ -37,6 +40,19 @@ func registerInviteRoutes(engine *gin.Engine, javaClient authGateway, inviteServ
|
|||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
|
internalGroup.POST("/register-bind-code", func(c *gin.Context) {
|
||||||
|
var req invite.RegisterBindCodeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, invite.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := inviteService.BindRegisteredCode(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
appGroup := engine.Group("/app/h5/invite-campaign")
|
appGroup := engine.Group("/app/h5/invite-campaign")
|
||||||
appGroup.Use(authMiddleware(javaClient))
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
@ -48,19 +64,21 @@ func registerInviteRoutes(engine *gin.Engine, javaClient authGateway, inviteServ
|
|||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
appGroup.POST("/bind-code", func(c *gin.Context) {
|
appGroup.GET("/rank", func(c *gin.Context) {
|
||||||
var req invite.BindCodeRequest
|
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
resp, err := inviteService.GetRank(c.Request.Context(), mustAuthUser(c), limit)
|
||||||
writeError(c, invite.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := inviteService.BindCode(c.Request.Context(), mustAuthUser(c), resolveClientIP(c), req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
|
appGroup.POST("/bind-code", func(c *gin.Context) {
|
||||||
|
bindInviteCode(c, inviteService)
|
||||||
|
})
|
||||||
|
appGroup.POST("/bind-invite-code", func(c *gin.Context) {
|
||||||
|
bindInviteCode(c, inviteService)
|
||||||
|
})
|
||||||
appGroup.POST("/tasks/claim", func(c *gin.Context) {
|
appGroup.POST("/tasks/claim", func(c *gin.Context) {
|
||||||
var req invite.TaskClaimRequest
|
var req invite.TaskClaimRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@ -75,3 +93,23 @@ func registerInviteRoutes(engine *gin.Engine, javaClient authGateway, inviteServ
|
|||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bindInviteCode(c *gin.Context, inviteService *invite.InviteService) {
|
||||||
|
var req invite.BindCodeRequest
|
||||||
|
if err := c.ShouldBind(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
writeError(c, invite.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.NormalizedInviteCode() == "" {
|
||||||
|
req.InviteCodeSnake = c.Query("invite_code")
|
||||||
|
req.InviteCode = c.Query("inviteCode")
|
||||||
|
req.InvitationCode = c.Query("invitationCode")
|
||||||
|
req.InvitationCodeSnake = c.Query("invitation_code")
|
||||||
|
}
|
||||||
|
resp, err := inviteService.BindCode(c.Request.Context(), mustAuthUser(c), resolveClientIP(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
}
|
||||||
|
|||||||
114
internal/router/lingxian_routes.go
Normal file
114
internal/router/lingxian_routes.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerLingxianRoutes(engine *gin.Engine, javaClient authGateway, service *lingxian.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
consoleGroup := engine.Group("/operate/lingxian-game")
|
||||||
|
consoleGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
consoleGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetProviderConfig(c.Request.Context(), c.Query("sysOrigin"), c.Query("profile"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/config", func(c *gin.Context) {
|
||||||
|
var req lingxian.SaveProviderConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveProviderConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/config/activate", func(c *gin.Context) {
|
||||||
|
var req lingxian.ActivateProviderProfileRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ActivateProviderProfile(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.GET("/catalog/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageCatalog(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("profile"),
|
||||||
|
c.Query("keyword"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/catalog/fetch", func(c *gin.Context) {
|
||||||
|
var req lingxian.SyncCatalogRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.FetchAdminCatalog(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/import-catalog", func(c *gin.Context) {
|
||||||
|
var req lingxian.SyncCatalogRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaultShowcase := true
|
||||||
|
if req.DefaultShowcase != nil {
|
||||||
|
defaultShowcase = *req.DefaultShowcase
|
||||||
|
}
|
||||||
|
resp, err := service.ImportCatalogGames(c.Request.Context(), req.SysOrigin, req.Profile, req.VendorGameIDs, defaultShowcase)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
consoleGroup.POST("/sync", func(c *gin.Context) {
|
||||||
|
var req lingxian.SyncCatalogRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SyncAdminGames(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
136
internal/router/manager_center_routes.go
Normal file
136
internal/router/manager_center_routes.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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("/countries", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListCountries(c.Request.Context(), mustAuthUser(c))
|
||||||
|
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.GET("/bd-leaders", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListBDLeaders(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.POST("/bd-leaders", func(c *gin.Context) {
|
||||||
|
var req managercenter.AddBDLeaderRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, managercenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.AddBDLeader(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)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.POST("/users/unban", func(c *gin.Context) {
|
||||||
|
var req managercenter.UnbanUserRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, managercenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.UnbanUser(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.POST("/users/country", func(c *gin.Context) {
|
||||||
|
var req managercenter.ChangeUserCountryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, managercenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ChangeUserCountry(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -19,6 +20,14 @@ type authGateway interface {
|
|||||||
AuthenticateConsoleToken(ctx context.Context, authorization string) (integration.ConsoleAccount, error)
|
AuthenticateConsoleToken(ctx context.Context, authorization string) (integration.ConsoleAccount, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type consolePermissionGateway interface {
|
||||||
|
ListConsoleButtonAliases(ctx context.Context, authorization string) (map[string]struct{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleMenuGateway interface {
|
||||||
|
HasConsoleMenuAlias(ctx context.Context, authorization, requiredAlias string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
// authMiddleware 校验用户 token,并把认证后的用户信息写入上下文。
|
// authMiddleware 校验用户 token,并把认证后的用户信息写入上下文。
|
||||||
func authMiddleware(javaClient authGateway) gin.HandlerFunc {
|
func authMiddleware(javaClient authGateway) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
@ -73,6 +82,58 @@ func consoleAuthMiddleware(javaClient authGateway) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// consolePermissionMiddleware 在写接口执行前读取 Java Console 的真实按钮权限,禁止只依赖前端隐藏按钮。
|
||||||
|
func consolePermissionMiddleware(javaClient authGateway, requiredAlias string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
gateway, ok := javaClient.(consolePermissionGateway)
|
||||||
|
if !ok {
|
||||||
|
writeError(c, common.NewAppError(http.StatusServiceUnavailable, "permission_gateway_unavailable", "console permission gateway is unavailable"))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
aliases, err := gateway.ListConsoleButtonAliases(ctx, strings.TrimSpace(c.GetHeader("Authorization")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusForbidden, "permission_check_failed", err.Error()))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, allowed := aliases[requiredAlias]; !allowed {
|
||||||
|
writeError(c, common.NewAppError(http.StatusForbidden, "permission_denied", "required permission: "+requiredAlias))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consoleMenuMiddleware 校验页面菜单权限,所有管理端 GET 都不能只依赖前端路由可见性。
|
||||||
|
func consoleMenuMiddleware(javaClient authGateway, requiredAlias string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
gateway, ok := javaClient.(consoleMenuGateway)
|
||||||
|
if !ok {
|
||||||
|
writeError(c, common.NewAppError(http.StatusServiceUnavailable, "menu_gateway_unavailable", "console menu gateway is unavailable"))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
allowed, err := gateway.HasConsoleMenuAlias(ctx, strings.TrimSpace(c.GetHeader("Authorization")), requiredAlias)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, common.NewAppError(http.StatusForbidden, "menu_permission_check_failed", err.Error()))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
writeError(c, common.NewAppError(http.StatusForbidden, "permission_denied", "required menu: "+requiredAlias))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// internalSecretMiddleware 校验内部回调密钥。
|
// internalSecretMiddleware 校验内部回调密钥。
|
||||||
func internalSecretMiddleware(secret string) gin.HandlerFunc {
|
func internalSecretMiddleware(secret string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
@ -92,6 +153,29 @@ func internalSecretMiddleware(secret string) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requiredInternalSecretMiddleware 用于会触发资金/奖励副作用的内部入口。
|
||||||
|
// 与兼容历史调用的 internalSecretMiddleware 不同,服务端未配置密钥时必须 fail-closed,
|
||||||
|
// 否则错误的部署配置会把无鉴权请求直接放进结算状态机。
|
||||||
|
func requiredInternalSecretMiddleware(secret string) gin.HandlerFunc {
|
||||||
|
configuredSecret := strings.TrimSpace(secret)
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if configuredSecret == "" {
|
||||||
|
writeError(c, common.NewAppError(http.StatusServiceUnavailable, "internal_secret_not_configured", "internal callback secret is not configured"))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimSpace(c.GetHeader("X-Internal-Token"))
|
||||||
|
if subtle.ConstantTimeCompare([]byte(token), []byte(configuredSecret)) != 1 {
|
||||||
|
writeError(c, common.NewAppError(http.StatusUnauthorized, "invalid_internal_token", "internal token is invalid"))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mustAuthUser 从 Gin 上下文读取已经通过鉴权的用户。
|
// mustAuthUser 从 Gin 上下文读取已经通过鉴权的用户。
|
||||||
func mustAuthUser(c *gin.Context) common.AuthUser {
|
func mustAuthUser(c *gin.Context) common.AuthUser {
|
||||||
value, exists := c.Get(authUserContextKey)
|
value, exists := c.Get(authUserContextKey)
|
||||||
@ -112,6 +196,9 @@ func trimBearer(authorization string) string {
|
|||||||
|
|
||||||
// resolveClientIP 解析请求来源 IP。
|
// resolveClientIP 解析请求来源 IP。
|
||||||
func resolveClientIP(c *gin.Context) string {
|
func resolveClientIP(c *gin.Context) string {
|
||||||
|
if realIP := strings.TrimSpace(c.GetHeader("X-Real-IP")); realIP != "" {
|
||||||
|
return realIP
|
||||||
|
}
|
||||||
if forwarded := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); forwarded != "" {
|
if forwarded := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); forwarded != "" {
|
||||||
parts := strings.Split(forwarded, ",")
|
parts := strings.Split(forwarded, ",")
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
|
|||||||
45
internal/router/props_store_routes.go
Normal file
45
internal/router/props_store_routes.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/propsstore"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerPropsStoreRoutes registers admin props store routes.
|
||||||
|
func registerPropsStoreRoutes(engine *gin.Engine, javaClient authGateway, service *propsstore.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/props/store")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.POST("/source-map", func(c *gin.Context) {
|
||||||
|
var req propsstore.MapBySourceIDsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, propsstore.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.MapBySourceIDs(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/add-or-update", func(c *gin.Context) {
|
||||||
|
var req propsstore.SaveCommodityRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, propsstore.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveCommodity(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
95
internal/router/props_store_routes_test.go
Normal file
95
internal/router/props_store_routes_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/repo"
|
||||||
|
"chatapp3-golang/internal/service/propsstore"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPropsStoreAdminRoutesSaveAndMap(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterPropsStoreDB(t)
|
||||||
|
service := propsstore.NewService(db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, propsStoreAuthStub{}, Services{
|
||||||
|
PropsStore: service,
|
||||||
|
})
|
||||||
|
|
||||||
|
saveBody := []byte(`{
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"sourceId": "100",
|
||||||
|
"propsType": "AVATAR_FRAME",
|
||||||
|
"currencyTypes": "GOLD",
|
||||||
|
"validDays": "7",
|
||||||
|
"discount": 0,
|
||||||
|
"shelfStatus": true,
|
||||||
|
"sort": 0,
|
||||||
|
"label": ""
|
||||||
|
}`)
|
||||||
|
saveReq := httptest.NewRequest(http.MethodPost, "/props/store/add-or-update", bytes.NewReader(saveBody))
|
||||||
|
saveReq.Header.Set("Authorization", "Bearer test-token")
|
||||||
|
saveReq.Header.Set("Content-Type", "application/json")
|
||||||
|
saveResp := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(saveResp, saveReq)
|
||||||
|
if saveResp.Code != http.StatusOK {
|
||||||
|
t.Fatalf("save status = %d, body = %s", saveResp.Code, saveResp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
mapReq := httptest.NewRequest(http.MethodPost, "/props/store/source-map", bytes.NewReader([]byte(`{
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"sourceIds": ["100"]
|
||||||
|
}`)))
|
||||||
|
mapReq.Header.Set("Authorization", "Bearer test-token")
|
||||||
|
mapReq.Header.Set("Content-Type", "application/json")
|
||||||
|
mapResp := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(mapResp, mapReq)
|
||||||
|
if mapResp.Code != http.StatusOK {
|
||||||
|
t.Fatalf("map status = %d, body = %s", mapResp.Code, mapResp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded struct {
|
||||||
|
ErrorCode int `json:"errorCode"`
|
||||||
|
Body map[string]propsstore.CommodityView `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(mapResp.Body.Bytes(), &decoded); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
item, ok := decoded.Body["100"]
|
||||||
|
if decoded.ErrorCode != 0 || !ok || item.ShelfStatus == nil || !*item.ShelfStatus {
|
||||||
|
t.Fatalf("unexpected source-map body: %#v", decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type propsStoreAuthStub struct{}
|
||||||
|
|
||||||
|
func (propsStoreAuthStub) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||||
|
return integration.UserCredential{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (propsStoreAuthStub) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
||||||
|
return integration.ConsoleAccount{LoginName: "admin"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterPropsStoreDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.PropsCommodityStore{}); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
59
internal/router/recharge_agency_routes.go
Normal file
59
internal/router/recharge_agency_routes.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/rechargeagency"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerRechargeAgencyRoutes 注册充值代理 H5 币商名单路由。
|
||||||
|
func registerRechargeAgencyRoutes(engine *gin.Engine, javaClient authGateway, service *rechargeagency.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
|
||||||
|
resp, err := service.PageSellers(c.Request.Context(), mustAuthUser(c), cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
listGroup := engine.Group("/app/h5/recharge-agency-list")
|
||||||
|
listGroup.Use(authMiddleware(javaClient))
|
||||||
|
listGroup.GET("/sellers", handler)
|
||||||
|
|
||||||
|
agencyGroup := engine.Group("/app/h5/recharge-agency")
|
||||||
|
agencyGroup.Use(authMiddleware(javaClient))
|
||||||
|
agencyGroup.GET("/sellers", handler)
|
||||||
|
agencyGroup.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)
|
||||||
|
})
|
||||||
|
agencyGroup.POST("/profile/whatsapp", func(c *gin.Context) {
|
||||||
|
var req rechargeagency.UpdateWhatsappRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, rechargeagency.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.UpdateWhatsapp(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
161
internal/router/recharge_agency_routes_test.go
Normal file
161
internal/router/recharge_agency_routes_test.go
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/service/rechargeagency"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rechargeAgencyRouteAuth struct{}
|
||||||
|
|
||||||
|
func (rechargeAgencyRouteAuth) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||||
|
return integration.UserCredential{
|
||||||
|
UserID: integration.Int64Value(7),
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rechargeAgencyRouteAuth) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
||||||
|
return integration.ConsoleAccount{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type rechargeAgencyRouteJava struct {
|
||||||
|
authorization string
|
||||||
|
sysOrigin string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *rechargeAgencyRouteJava) PageFreightSellers(_ context.Context, authorization string, sysOrigin string, cursor int, limit int) (integration.FreightSellerPage, error) {
|
||||||
|
f.authorization = authorization
|
||||||
|
f.sysOrigin = sysOrigin
|
||||||
|
return integration.FreightSellerPage{
|
||||||
|
Total: 1,
|
||||||
|
Current: cursor,
|
||||||
|
Size: limit,
|
||||||
|
Records: []integration.FreightSellerRecord{
|
||||||
|
{
|
||||||
|
ID: integration.Int64Value(1),
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
UserID: integration.Int64Value(99),
|
||||||
|
Balance: integration.DecimalString("10"),
|
||||||
|
UserBaseInfo: integration.FreightSellerProfile{
|
||||||
|
Account: "u99",
|
||||||
|
UserNickname: "Nina",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRechargeAgencySellersRoute(t *testing.T) {
|
||||||
|
java := &rechargeAgencyRouteJava{}
|
||||||
|
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.RechargeAgencySellerInfo{}); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.RechargeAgencySellerInfo{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
UserID: 99,
|
||||||
|
Whatsapp: "+8613800138000",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed whatsapp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := rechargeagency.NewService(config.Config{}, db, java)
|
||||||
|
engine := NewRouter(config.Config{}, nil, rechargeAgencyRouteAuth{}, Services{
|
||||||
|
RechargeAgency: service,
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/h5/recharge-agency-list/sellers?cursor=2&limit=1", 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())
|
||||||
|
}
|
||||||
|
if java.authorization != "Bearer token" || java.sysOrigin != "LIKEI" {
|
||||||
|
t.Fatalf("gateway authorization/sysOrigin = %q/%q", java.authorization, java.sysOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Body struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Records []struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
UserNickname string `json:"userNickname"`
|
||||||
|
Whatsapp string `json:"whatsapp"`
|
||||||
|
} `json:"records"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Body.Total != 1 || len(payload.Body.Records) != 1 {
|
||||||
|
t.Fatalf("payload body mismatch: %+v", payload.Body)
|
||||||
|
}
|
||||||
|
if payload.Body.Records[0].UserID != "99" || payload.Body.Records[0].UserNickname != "Nina" {
|
||||||
|
t.Fatalf("record mismatch: %+v", payload.Body.Records[0])
|
||||||
|
}
|
||||||
|
if payload.Body.Records[0].Whatsapp != "+8613800138000" {
|
||||||
|
t.Fatalf("whatsapp mismatch: %+v", payload.Body.Records[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRechargeAgencyProfileRoutes(t *testing.T) {
|
||||||
|
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.RechargeAgencySellerInfo{}); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
service := rechargeagency.NewService(config.Config{}, db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, nil, rechargeAgencyRouteAuth{}, Services{
|
||||||
|
RechargeAgency: service,
|
||||||
|
})
|
||||||
|
|
||||||
|
postReq := httptest.NewRequest(http.MethodPost, "/app/h5/recharge-agency/profile/whatsapp", bytes.NewBufferString(`{"whatsapp":"+8613800138000"}`))
|
||||||
|
postReq.Header.Set("Authorization", "Bearer token")
|
||||||
|
postReq.Header.Set("Content-Type", "application/json")
|
||||||
|
postRec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(postRec, postReq)
|
||||||
|
if postRec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("post status = %d body = %s", postRec.Code, postRec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
getReq := httptest.NewRequest(http.MethodGet, "/app/h5/recharge-agency/profile", nil)
|
||||||
|
getReq.Header.Set("Authorization", "Bearer token")
|
||||||
|
getRec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(getRec, getReq)
|
||||||
|
if getRec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("get status = %d body = %s", getRec.Code, getRec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Body struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
Whatsapp string `json:"whatsapp"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(getRec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode get response: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Body.UserID != "7" || payload.Body.Whatsapp != "+8613800138000" {
|
||||||
|
t.Fatalf("profile body mismatch: %+v", payload.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
internal/router/recharge_reward_routes.go
Normal file
69
internal/router/recharge_reward_routes.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/rechargereward"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerRechargeRewardRoutes 注册充值奖励 H5 和后台配置路由。
|
||||||
|
func registerRechargeRewardRoutes(engine *gin.Engine, javaClient authGateway, service *rechargereward.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/h5/recharge-reward")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/home", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group := engine.Group("/resident-activity/recharge-reward")
|
||||||
|
group.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
|
||||||
|
group.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req rechargereward.SaveRechargeRewardConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, rechargereward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.GET("/claim-record/page", func(c *gin.Context) {
|
||||||
|
resp, err := service.PageClaimRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("month"),
|
||||||
|
rechargereward.ParseInt64(c.Query("userId")),
|
||||||
|
int(rechargereward.ParseInt64(c.Query("cursor"))),
|
||||||
|
int(rechargereward.ParseInt64(c.Query("limit"))),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
150
internal/router/region_im_group_routes.go
Normal file
150
internal/router/region_im_group_routes.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/repo"
|
||||||
|
"chatapp3-golang/internal/service/highwin"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerRegionIMGroupRoutes 注册区域通用 IM 群组和区域广播接口。
|
||||||
|
func registerRegionIMGroupRoutes(
|
||||||
|
engine *gin.Engine,
|
||||||
|
cfg config.Config,
|
||||||
|
repository *repo.Repository,
|
||||||
|
javaClient authGateway,
|
||||||
|
service *regionimgroup.Service,
|
||||||
|
) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRegionIMGroupAppRoutes(engine, javaClient, service)
|
||||||
|
registerRegionIMGroupInternalRoutes(engine, cfg, service)
|
||||||
|
registerRegionIMGroupAdminRoutes(engine, repository, javaClient, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRegionIMGroupAppRoutes(engine *gin.Engine, javaClient authGateway, service *regionimgroup.Service) {
|
||||||
|
handler := func(c *gin.Context) {
|
||||||
|
resp, err := service.GetCurrentRegionIMGroup(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/region-im-group")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/current", handler)
|
||||||
|
|
||||||
|
legacyGroup := engine.Group("/app/voice-room/red-packet")
|
||||||
|
legacyGroup.Use(authMiddleware(javaClient))
|
||||||
|
legacyGroup.GET("/im-group", handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRegionIMGroupInternalRoutes(engine *gin.Engine, cfg config.Config, service *regionimgroup.Service) {
|
||||||
|
internalGroup := engine.Group("/internal/voice-room")
|
||||||
|
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
|
internalGroup.POST("/region-broadcast", func(c *gin.Context) {
|
||||||
|
var req regionimgroup.RegionBroadcastRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, regionimgroup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SendRegionBroadcast(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRegionIMGroupAdminRoutes(engine *gin.Engine, repository *repo.Repository, javaClient authGateway, service *regionimgroup.Service) {
|
||||||
|
registerRegionIMGroupAdminGroup(engine.Group("/region/config"), javaClient, service)
|
||||||
|
registerRegionIMGroupAdminGroup(engine.Group("/resident-activity/voice-room-red-packet"), javaClient, service)
|
||||||
|
var worker *highwin.BroadcastSimulationWorker
|
||||||
|
if repository != nil {
|
||||||
|
worker = highwin.NewBroadcastSimulationWorker(repository.DB, repository.Redis, service)
|
||||||
|
}
|
||||||
|
registerRegionBroadcastSimulationAdminGroup(engine.Group("/operate/region-broadcast"), javaClient, service, worker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRegionIMGroupAdminGroup(group *gin.RouterGroup, javaClient authGateway, service *regionimgroup.Service) {
|
||||||
|
group.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
group.GET("/im-groups", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListRegionIMGroups(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
group.POST("/im-groups/save", func(c *gin.Context) {
|
||||||
|
var req regionimgroup.RegionIMGroupRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, regionimgroup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveRegionIMGroup(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
group.POST("/im-groups/create", func(c *gin.Context) {
|
||||||
|
var req regionimgroup.RegionIMGroupRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, regionimgroup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.CreateRegionIMGroup(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRegionBroadcastSimulationAdminGroup(group *gin.RouterGroup, javaClient authGateway, service *regionimgroup.Service, worker *highwin.BroadcastSimulationWorker) {
|
||||||
|
group.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
group.POST("/simulate", func(c *gin.Context) {
|
||||||
|
var req highwin.BroadcastSimulationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, regionimgroup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := highwin.SendSimulationBroadcast(c.Request.Context(), service, req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
group.GET("/worker/status", func(c *gin.Context) {
|
||||||
|
writeOK(c, worker.Status())
|
||||||
|
})
|
||||||
|
group.POST("/worker/start", func(c *gin.Context) {
|
||||||
|
var req highwin.ContinuousSimulationRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, regionimgroup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := worker.Start(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
group.POST("/worker/stop", func(c *gin.Context) {
|
||||||
|
writeOK(c, worker.Stop())
|
||||||
|
})
|
||||||
|
}
|
||||||
120
internal/router/room_turnover_reward_routes.go
Normal file
120
internal/router/room_turnover_reward_routes.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerRoomTurnoverRewardRoutes 注册房间流水奖励后台配置和发奖记录路由。
|
||||||
|
func registerRoomTurnoverRewardRoutes(engine *gin.Engine, javaClient authGateway, service *roomturnoverreward.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.GET("/public/h5/room-turnover-reward/page", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetPublicPage(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/h5/room-turnover-reward")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/home", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetHome(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/reward/claim/status", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetClaimStatus(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/reward/claim", func(c *gin.Context) {
|
||||||
|
var req roomturnoverreward.RoomTurnoverRewardClaimRequest
|
||||||
|
if c.Request.ContentLength != 0 {
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, roomturnoverreward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
writeError(c, roomturnoverreward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ClaimReward(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group := engine.Group("/resident-activity/room-turnover-reward")
|
||||||
|
group.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
|
||||||
|
group.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req roomturnoverreward.SaveRoomTurnoverRewardConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, roomturnoverreward.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.GET("/reward-record/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageRewardRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("cycleKey"),
|
||||||
|
c.Query("status"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
group.GET("/current-rank", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.CurrentRoomRanking(c.Request.Context(), c.Query("sysOrigin"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -6,28 +6,73 @@ import (
|
|||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
"chatapp3-golang/internal/config"
|
||||||
"chatapp3-golang/internal/repo"
|
"chatapp3-golang/internal/repo"
|
||||||
|
"chatapp3-golang/internal/service/apppopup"
|
||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
|
"chatapp3-golang/internal/service/binancerecharge"
|
||||||
|
"chatapp3-golang/internal/service/errorlog"
|
||||||
|
"chatapp3-golang/internal/service/firstrechargereward"
|
||||||
|
"chatapp3-golang/internal/service/gameking"
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
|
"chatapp3-golang/internal/service/hotgame"
|
||||||
"chatapp3-golang/internal/service/invite"
|
"chatapp3-golang/internal/service/invite"
|
||||||
|
"chatapp3-golang/internal/service/lingxian"
|
||||||
"chatapp3-golang/internal/service/luckygift"
|
"chatapp3-golang/internal/service/luckygift"
|
||||||
|
"chatapp3-golang/internal/service/managercenter"
|
||||||
|
"chatapp3-golang/internal/service/propsstore"
|
||||||
|
"chatapp3-golang/internal/service/rechargeagency"
|
||||||
|
"chatapp3-golang/internal/service/rechargereward"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
|
"chatapp3-golang/internal/service/roomturnoverreward"
|
||||||
"chatapp3-golang/internal/service/signinreward"
|
"chatapp3-golang/internal/service/signinreward"
|
||||||
|
"chatapp3-golang/internal/service/smashegg"
|
||||||
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
|
"chatapp3-golang/internal/service/userbadge"
|
||||||
|
"chatapp3-golang/internal/service/vip"
|
||||||
|
"chatapp3-golang/internal/service/voiceroomredpacket"
|
||||||
|
"chatapp3-golang/internal/service/voiceroomrocket"
|
||||||
"chatapp3-golang/internal/service/weekstar"
|
"chatapp3-golang/internal/service/weekstar"
|
||||||
|
"chatapp3-golang/internal/service/wheel"
|
||||||
|
"chatapp3-golang/internal/service/yumigiftchallenge"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Services 聚合所有业务模块服务,供路由层统一装配。
|
// Services 聚合所有业务模块服务,供路由层统一装配。
|
||||||
type Services struct {
|
type Services struct {
|
||||||
|
AppPopup *apppopup.Service
|
||||||
|
ErrorLog *errorlog.Service
|
||||||
Invite *invite.InviteService
|
Invite *invite.InviteService
|
||||||
Baishun *baishun.BaishunService
|
Baishun *baishun.BaishunService
|
||||||
|
BinanceRecharge *binancerecharge.Service
|
||||||
|
FirstRechargeReward *firstrechargereward.Service
|
||||||
|
Lingxian *lingxian.Service
|
||||||
GameOpen *gameopen.GameOpenService
|
GameOpen *gameopen.GameOpenService
|
||||||
|
GameKing *gameking.Service
|
||||||
GameProviders *gameprovider.Registry
|
GameProviders *gameprovider.Registry
|
||||||
|
GameVisibility *gameprovider.VisibilityPolicy
|
||||||
LuckyGift *luckygift.LuckyGiftService
|
LuckyGift *luckygift.LuckyGiftService
|
||||||
|
ManagerCenter *managercenter.Service
|
||||||
|
PropsStore *propsstore.Service
|
||||||
|
HostCenter *hostcenter.Service
|
||||||
|
Hotgame *hotgame.Service
|
||||||
|
RechargeAgency *rechargeagency.Service
|
||||||
|
RechargeReward *rechargereward.Service
|
||||||
|
RegionIMGroup *regionimgroup.Service
|
||||||
RegisterReward *registerreward.RegisterRewardService
|
RegisterReward *registerreward.RegisterRewardService
|
||||||
|
RoomTurnoverReward *roomturnoverreward.Service
|
||||||
SignInReward *signinreward.SignInRewardService
|
SignInReward *signinreward.SignInRewardService
|
||||||
|
SmashEgg *smashegg.Service
|
||||||
|
TaskCenter *taskcenter.Service
|
||||||
|
UserBadge *userbadge.Service
|
||||||
|
VIP *vip.Service
|
||||||
|
Wheel *wheel.Service
|
||||||
|
VoiceRoomRedPacket *voiceroomredpacket.Service
|
||||||
|
VoiceRoomRocket *voiceroomrocket.Service
|
||||||
WeekStar *weekstar.WeekStarService
|
WeekStar *weekstar.WeekStarService
|
||||||
|
YumiGiftChallenge *yumigiftchallenge.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter 创建 Gin 路由,并按模块注册所有接口。
|
// NewRouter 创建 Gin 路由,并按模块注册所有接口。
|
||||||
@ -38,21 +83,66 @@ func NewRouter(
|
|||||||
services Services,
|
services Services,
|
||||||
) *gin.Engine {
|
) *gin.Engine {
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(gin.Logger(), gin.Recovery())
|
engine.Use(gin.Logger(), gin.Recovery(), corsMiddleware())
|
||||||
|
|
||||||
registerHealthRoutes(engine, cfg, repository)
|
registerHealthRoutes(engine, cfg, repository)
|
||||||
|
registerAppPopupRoutes(engine, javaClient, services.AppPopup)
|
||||||
|
registerAgoraErrorLogRoutes(engine, javaClient, services.ErrorLog)
|
||||||
registerInviteRoutes(engine, javaClient, services.Invite)
|
registerInviteRoutes(engine, javaClient, services.Invite)
|
||||||
|
registerRechargeAgencyRoutes(engine, javaClient, services.RechargeAgency)
|
||||||
|
registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward)
|
||||||
|
registerFirstRechargeRewardRoutes(engine, javaClient, services.FirstRechargeReward)
|
||||||
|
registerBinanceRechargeRoutes(engine, javaClient, services.BinanceRecharge)
|
||||||
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)
|
||||||
|
registerTaskCenterArchiveGCRoutes(engine, cfg, javaClient, services.TaskCenter)
|
||||||
|
registerGameKingRoutes(engine, cfg, javaClient, services.GameKing)
|
||||||
|
registerUserBadgeRoutes(engine, javaClient, services.UserBadge)
|
||||||
|
registerVipRoutes(engine, javaClient, services.VIP)
|
||||||
|
registerWheelRoutes(engine, cfg, javaClient, services.Wheel)
|
||||||
|
registerSmashEggRoutes(engine, javaClient, services.SmashEgg)
|
||||||
|
registerRegionIMGroupRoutes(engine, cfg, repository, javaClient, services.RegionIMGroup)
|
||||||
|
registerVoiceRoomRedPacketRoutes(engine, javaClient, services.VoiceRoomRedPacket)
|
||||||
|
registerVoiceRoomRocketRoutes(engine, cfg, javaClient, services.VoiceRoomRocket)
|
||||||
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
||||||
|
registerYumiGiftChallengeRoutes(engine, cfg, javaClient, services.YumiGiftChallenge)
|
||||||
|
registerRoomTurnoverRewardRoutes(engine, javaClient, services.RoomTurnoverReward)
|
||||||
|
registerHostCenterRoutes(engine, services.HostCenter)
|
||||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
||||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
registerGameProviderRoutes(engine, javaClient, services.GameProviders, services.GameVisibility)
|
||||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||||
|
registerLingxianRoutes(engine, javaClient, services.Lingxian)
|
||||||
|
registerHotgameRoutes(engine, javaClient, services.Hotgame)
|
||||||
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
||||||
|
registerManagerCenterRoutes(engine, javaClient, services.ManagerCenter)
|
||||||
|
registerPropsStoreRoutes(engine, javaClient, services.PropsStore)
|
||||||
|
|
||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func corsMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
origin := c.GetHeader("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
origin = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Authorization,Content-Type,Origin,Accept,sign,timestamp,token,req-lang,req-client,req-imei,req-app-intel,req-sys-origin,req-version,req-zone,X-Requested-With")
|
||||||
|
c.Header("Access-Control-Expose-Headers", "Authorization")
|
||||||
|
c.Header("Access-Control-Max-Age", "86400")
|
||||||
|
|
||||||
|
if c.Request.Method == http.MethodOptions {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// registerHealthRoutes 注册健康检查接口。
|
// registerHealthRoutes 注册健康检查接口。
|
||||||
func registerHealthRoutes(engine *gin.Engine, cfg config.Config, repository *repo.Repository) {
|
func registerHealthRoutes(engine *gin.Engine, cfg config.Config, repository *repo.Repository) {
|
||||||
engine.GET("/health", func(c *gin.Context) {
|
engine.GET("/health", func(c *gin.Context) {
|
||||||
|
|||||||
263
internal/router/smash_egg_routes.go
Normal file
263
internal/router/smash_egg_routes.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/service/smashegg"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerSmashEggRoutes registers app, legacy H5, and admin smash egg APIs.
|
||||||
|
func registerSmashEggRoutes(engine *gin.Engine, javaClient authGateway, service *smashegg.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/smash-golden-egg")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetAppConfig(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/draw", func(c *gin.Context) {
|
||||||
|
var req smashegg.DrawRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, smashegg.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.IdempotencyKey = requestIdempotencyKey(c, req.IdempotencyKey)
|
||||||
|
resp, err := service.Draw(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/records", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageUserRecords(c.Request.Context(), mustAuthUser(c), cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/notices", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListRecentNotices(c.Request.Context(), mustAuthUser(c).SysOrigin, 20)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/rank/day", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "100")))
|
||||||
|
resp, err := service.PageDayRank(c.Request.Context(), mustAuthUser(c).SysOrigin, cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/resident-activity/smash-golden-egg")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req smashegg.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, smashegg.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.GET("/draw-record/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageAdminRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
smashegg.ParseUserID(c.Query("userId")),
|
||||||
|
c.Query("status"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
c.Query("startTime"),
|
||||||
|
c.Query("endTime"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
h5Group := engine.Group("/h5/smash-golden-egg")
|
||||||
|
h5Group.Any("/config", func(c *gin.Context) {
|
||||||
|
handleLegacySmashEggAction(c, javaClient, service, "Action/TreasureHunt.getPrizeList")
|
||||||
|
})
|
||||||
|
h5Group.Any("/draw", func(c *gin.Context) {
|
||||||
|
handleLegacySmashEggAction(c, javaClient, service, "Action/TreasureHunt.startHunt")
|
||||||
|
})
|
||||||
|
h5Group.Any("/records", func(c *gin.Context) {
|
||||||
|
handleLegacySmashEggAction(c, javaClient, service, "Action/TreasureHunt.getMyHuntRecord")
|
||||||
|
})
|
||||||
|
h5Group.Any("/notices", func(c *gin.Context) {
|
||||||
|
handleLegacySmashEggAction(c, javaClient, service, "Action/TreasureHunt.getHuntRecord")
|
||||||
|
})
|
||||||
|
h5Group.Any("/rank/day", func(c *gin.Context) {
|
||||||
|
handleLegacySmashEggAction(c, javaClient, service, "Action/TreasureHunt.getDayRank")
|
||||||
|
})
|
||||||
|
|
||||||
|
engine.Any("/index.php", func(c *gin.Context) {
|
||||||
|
action := strings.TrimSpace(c.Query("action"))
|
||||||
|
if !strings.HasPrefix(action, "Action/TreasureHunt.") {
|
||||||
|
writeError(c, smashegg.NewAppError(http.StatusNotFound, "not_found", "legacy action is not supported"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleLegacySmashEggAction(c, javaClient, service, action)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLegacySmashEggAction(c *gin.Context, javaClient authGateway, service *smashegg.Service, action string) {
|
||||||
|
user, err := legacySmashEggUser(c, javaClient)
|
||||||
|
if err != nil {
|
||||||
|
writeLegacyError(c, http.StatusUnauthorized, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]any
|
||||||
|
switch action {
|
||||||
|
case "Action/TreasureHunt.getPrizeList":
|
||||||
|
resp, err = service.LegacyPrizeList(c.Request.Context(), user)
|
||||||
|
case "Action/TreasureHunt.startHunt":
|
||||||
|
times, _ := strconv.Atoi(strings.TrimSpace(firstNonEmptyQuery(c, "times", "num")))
|
||||||
|
if times <= 0 {
|
||||||
|
times = 1
|
||||||
|
}
|
||||||
|
resp, err = service.LegacyStartHunt(c.Request.Context(), user, times, requestIdempotencyKey(c, ""))
|
||||||
|
case "Action/TreasureHunt.getMyHuntRecord":
|
||||||
|
start, _ := strconv.Atoi(strings.TrimSpace(firstNonEmptyQuery(c, "offset", "start")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(firstNonEmptyQuery(c, "limit", "num")))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
resp, err = service.LegacyMyHuntRecords(c.Request.Context(), user, start, limit)
|
||||||
|
case "Action/TreasureHunt.getHuntRecord":
|
||||||
|
resp, err = service.LegacyHuntNotices(c.Request.Context(), user.SysOrigin)
|
||||||
|
case "Action/TreasureHunt.getDayRank":
|
||||||
|
resp, err = service.LegacyDayRank(c.Request.Context(), user.SysOrigin)
|
||||||
|
default:
|
||||||
|
writeLegacyError(c, http.StatusNotFound, "legacy action is not supported")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
writeLegacyError(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeLegacyOK(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacySmashEggUser(c *gin.Context, javaClient authGateway) (common.AuthUser, error) {
|
||||||
|
token := strings.TrimSpace(c.Query("token"))
|
||||||
|
if token == "" {
|
||||||
|
token = trimBearer(strings.TrimSpace(c.GetHeader("Authorization")))
|
||||||
|
}
|
||||||
|
if token != "" && javaClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
credential, err := javaClient.AuthenticateToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return common.AuthUser{}, err
|
||||||
|
}
|
||||||
|
return common.AuthUser{
|
||||||
|
UserID: int64(credential.UserID),
|
||||||
|
SysOrigin: credential.SysOrigin,
|
||||||
|
Token: token,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := smashegg.ParseUserID(firstNonEmptyQuery(c, "_login_uid", "uid", "userId"))
|
||||||
|
sysOrigin := strings.ToUpper(strings.TrimSpace(firstNonEmptyQuery(c, "sysOrigin", "originSys")))
|
||||||
|
if sysOrigin == "" {
|
||||||
|
sysOrigin = strings.ToUpper(strings.TrimSpace(c.GetHeader("req-sys-origin")))
|
||||||
|
}
|
||||||
|
if userID <= 0 || sysOrigin == "" {
|
||||||
|
return common.AuthUser{}, common.NewAppError(http.StatusUnauthorized, "missing_token", "token is required")
|
||||||
|
}
|
||||||
|
return common.AuthUser{UserID: userID, SysOrigin: sysOrigin, Token: token}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmptyQuery(c *gin.Context, keys ...string) string {
|
||||||
|
for _, key := range keys {
|
||||||
|
if value := strings.TrimSpace(c.Query(key)); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestIdempotencyKey(c *gin.Context, fallback string) string {
|
||||||
|
for _, value := range []string{
|
||||||
|
fallback,
|
||||||
|
c.GetHeader("Idempotency-Key"),
|
||||||
|
c.GetHeader("X-Idempotency-Key"),
|
||||||
|
firstNonEmptyQuery(c, "idempotencyKey", "idempotency_key", "requestId", "request_id", "nonce"),
|
||||||
|
strings.TrimSpace(c.PostForm("idempotencyKey")),
|
||||||
|
strings.TrimSpace(c.PostForm("idempotency_key")),
|
||||||
|
strings.TrimSpace(c.PostForm("requestId")),
|
||||||
|
strings.TrimSpace(c.PostForm("request_id")),
|
||||||
|
strings.TrimSpace(c.PostForm("nonce")),
|
||||||
|
} {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLegacyOK(c *gin.Context, data any) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"response_data": data,
|
||||||
|
"response_status": gin.H{
|
||||||
|
"code": 0,
|
||||||
|
"error": "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLegacyError(c *gin.Context, code int, message string) {
|
||||||
|
if message == "" {
|
||||||
|
message = "error"
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"response_data": gin.H{},
|
||||||
|
"response_status": gin.H{
|
||||||
|
"code": code,
|
||||||
|
"error": message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
81
internal/router/task_center_archive_gc_routes.go
Normal file
81
internal/router/task_center_archive_gc_routes.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const taskCenterArchiveGCManagePermission = "resident-activity:task-center-archive-gc:manage"
|
||||||
|
const taskCenterArchiveGCMenuAlias = "ResidentTaskCenterArchiveGC"
|
||||||
|
|
||||||
|
// registerTaskCenterArchiveGCRoutes 只做管理命令/cron tick 的 HTTP 绑定;所有状态机、
|
||||||
|
// 租约、COS 校验和删除边界均由 taskcenter service 实现。
|
||||||
|
func registerTaskCenterArchiveGCRoutes(engine *gin.Engine, cfg config.Config, javaClient authGateway, service *taskcenter.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
admin := engine.Group("/resident-activity/task-center/archive-gc")
|
||||||
|
admin.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
admin.GET("/status", consoleMenuMiddleware(javaClient, taskCenterArchiveGCMenuAlias), func(c *gin.Context) {
|
||||||
|
response, err := service.ArchiveGCStatus(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
admin.POST("/dry-run", consolePermissionMiddleware(javaClient, taskCenterArchiveGCManagePermission), func(c *gin.Context) {
|
||||||
|
var request taskcenter.ArchiveGCDryRunRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeError(c, taskcenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.ArchiveGCDryRun(c.Request.Context(), request)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
admin.POST("/start", consolePermissionMiddleware(javaClient, taskCenterArchiveGCManagePermission), func(c *gin.Context) {
|
||||||
|
response, err := service.StartArchiveGC(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
admin.POST("/pause", consolePermissionMiddleware(javaClient, taskCenterArchiveGCManagePermission), func(c *gin.Context) {
|
||||||
|
response, err := service.PauseArchiveGC(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
admin.POST("/resume", consolePermissionMiddleware(javaClient, taskCenterArchiveGCManagePermission), func(c *gin.Context) {
|
||||||
|
response, err := service.ResumeArchiveGC(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
|
||||||
|
internal := engine.Group("/internal/task-center/archive-gc")
|
||||||
|
// tick 会最终删除生产数据,密钥缺失也必须 fail-closed,不能沿用历史空密钥兼容。
|
||||||
|
internal.Use(requiredInternalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
|
internal.POST("/tick", func(c *gin.Context) {
|
||||||
|
response, err := service.ArchiveGCTick(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
}
|
||||||
135
internal/router/task_center_routes.go
Normal file
135
internal/router/task_center_routes.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerTaskCenterRoutes 注册任务中心 App、后台和内部事件路由。
|
||||||
|
func registerTaskCenterRoutes(engine *gin.Engine, cfg config.Config, javaClient authGateway, service *taskcenter.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/task-center")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/list", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListTasks(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/claimable-count", func(c *gin.Context) {
|
||||||
|
resp, err := service.ClaimableCount(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/claim", func(c *gin.Context) {
|
||||||
|
var req taskcenter.ClaimRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, taskcenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.Claim(c.Request.Context(), mustAuthUser(c), req.TaskID.Int64())
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
v2Group := engine.Group("/app/v2/task")
|
||||||
|
v2Group.Use(authMiddleware(javaClient))
|
||||||
|
v2Group.GET("/list", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListTasks(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
v2Group.GET("/claimable-count", func(c *gin.Context) {
|
||||||
|
resp, err := service.ClaimableCount(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
v2Group.GET("/reward", func(c *gin.Context) {
|
||||||
|
resp, err := service.Claim(c.Request.Context(), mustAuthUser(c), taskcenter.ParseInt64(c.Query("taskId")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
internalGroup := engine.Group("/internal/task-center")
|
||||||
|
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
|
internalGroup.POST("/event", func(c *gin.Context) {
|
||||||
|
var req taskcenter.EventRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, taskcenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ReceiveEvent(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/resident-activity/task-center")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), c.Query("sysOrigin"), c.Query("taskCategory"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req taskcenter.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, taskcenter.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.GET("/claim-record/page", func(c *gin.Context) {
|
||||||
|
resp, err := service.PageClaimRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("taskCategory"),
|
||||||
|
taskcenter.ParseInt64(c.Query("userId")),
|
||||||
|
c.Query("taskCode"),
|
||||||
|
c.Query("status"),
|
||||||
|
c.Query("startTime"),
|
||||||
|
c.Query("endTime"),
|
||||||
|
int(taskcenter.ParseInt64(c.Query("cursor"))),
|
||||||
|
int(taskcenter.ParseInt64(c.Query("limit"))),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
37
internal/router/user_badge_routes.go
Normal file
37
internal/router/user_badge_routes.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatapp3-golang/internal/service/userbadge"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerUserBadgeRoutes registers App user badge APIs.
|
||||||
|
func registerUserBadgeRoutes(engine *gin.Engine, javaClient authGateway, service *userbadge.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/user/badge")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/current", func(c *gin.Context) {
|
||||||
|
resp, err := service.Current(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/other", func(c *gin.Context) {
|
||||||
|
targetUserID := userbadge.ParseUserID(c.Query("userId"))
|
||||||
|
if targetUserID <= 0 {
|
||||||
|
targetUserID = userbadge.ParseUserID(c.Query("targetUserId"))
|
||||||
|
}
|
||||||
|
resp, err := service.Other(c.Request.Context(), mustAuthUser(c), targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
199
internal/router/user_badge_routes_test.go
Normal file
199
internal/router/user_badge_routes_test.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/repo"
|
||||||
|
"chatapp3-golang/internal/service/userbadge"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userBadgeAuthStub struct{}
|
||||||
|
|
||||||
|
func (userBadgeAuthStub) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||||
|
return integration.UserCredential{UserID: 123, SysOrigin: "LIKEI"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (userBadgeAuthStub) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
||||||
|
return integration.ConsoleAccount{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserBadgeCurrentRoute(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterUserBadgeDB(t)
|
||||||
|
now := time.Now().Add(time.Hour)
|
||||||
|
if err := db.Create(&model.UserBadgeBackpack{
|
||||||
|
ID: 1,
|
||||||
|
UserID: 123,
|
||||||
|
BadgeID: 501,
|
||||||
|
ExpireType: "TEMPORARY",
|
||||||
|
ExpireTime: now,
|
||||||
|
UseProps: true,
|
||||||
|
UpdateTime: time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create backpack: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.SysBadgeConfig{
|
||||||
|
ID: 501,
|
||||||
|
Type: "ACTIVITY",
|
||||||
|
BadgeName: "Activity Badge",
|
||||||
|
BadgeKey: "ACTIVITY_BADGE",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create badge config: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.SysBadgePictureConfig{
|
||||||
|
ID: 601,
|
||||||
|
BadgeConfigID: 501,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
SelectURL: "https://cdn.example.com/activity.png",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create badge picture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := userbadge.NewService(db)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/current", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
data, ok := payload["data"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("data missing: %#v", payload)
|
||||||
|
}
|
||||||
|
shortBadges, ok := data["shortBadges"].([]any)
|
||||||
|
if !ok || len(shortBadges) != 1 {
|
||||||
|
t.Fatalf("shortBadges = %#v", data["shortBadges"])
|
||||||
|
}
|
||||||
|
item, ok := shortBadges[0].(map[string]any)
|
||||||
|
if !ok || item["type"] != "SHORT" || item["badgeId"] != "501" {
|
||||||
|
t.Fatalf("short badge item = %#v", shortBadges[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserBadgeCurrentRouteRequiresAuth(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
service := userbadge.NewService(newRouterUserBadgeDB(t))
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/current", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("status = %d body = %s, want 401", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserBadgeOtherRoute(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterUserBadgeDB(t)
|
||||||
|
targetUserID := int64(456)
|
||||||
|
if err := db.Create(&model.UserBadgeBackpack{
|
||||||
|
ID: 2,
|
||||||
|
UserID: targetUserID,
|
||||||
|
BadgeID: 502,
|
||||||
|
ExpireType: "TEMPORARY",
|
||||||
|
ExpireTime: time.Now().Add(time.Hour),
|
||||||
|
UseProps: true,
|
||||||
|
UpdateTime: time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create backpack: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.SysBadgeConfig{
|
||||||
|
ID: 502,
|
||||||
|
Type: "ACHIEVEMENT",
|
||||||
|
BadgeName: "Achievement Badge",
|
||||||
|
BadgeKey: "ACHIEVEMENT_BADGE",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create badge config: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.SysBadgePictureConfig{
|
||||||
|
ID: 602,
|
||||||
|
BadgeConfigID: 502,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
SelectURL: "https://cdn.example.com/achievement.png",
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create badge picture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := userbadge.NewService(db)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/other?userId=456", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
data, ok := payload["data"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("data missing: %#v", payload)
|
||||||
|
}
|
||||||
|
if data["userId"] != float64(targetUserID) {
|
||||||
|
t.Fatalf("userId = %#v, want target user", data["userId"])
|
||||||
|
}
|
||||||
|
shortBadges, ok := data["shortBadges"].([]any)
|
||||||
|
if !ok || len(shortBadges) != 1 {
|
||||||
|
t.Fatalf("shortBadges = %#v", data["shortBadges"])
|
||||||
|
}
|
||||||
|
item, ok := shortBadges[0].(map[string]any)
|
||||||
|
if !ok || item["sourceType"] != "ACHIEVEMENT" || item["badgeId"] != "502" {
|
||||||
|
t.Fatalf("short badge item = %#v", shortBadges[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserBadgeOtherRouteRequiresTargetUser(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
service := userbadge.NewService(newRouterUserBadgeDB(t))
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, userBadgeAuthStub{}, Services{UserBadge: service})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/app/user/badge/other", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d body = %s, want 400", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterUserBadgeDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(
|
||||||
|
&model.UserVipState{},
|
||||||
|
&model.UserBadgeBackpack{},
|
||||||
|
&model.SysBadgeConfig{},
|
||||||
|
&model.SysBadgePictureConfig{},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("migrate sqlite: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
132
internal/router/vip_routes.go
Normal file
132
internal/router/vip_routes.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/vip"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerVipRoutes registers app and admin VIP APIs.
|
||||||
|
func registerVipRoutes(engine *gin.Engine, javaClient authGateway, vipService *vip.Service) {
|
||||||
|
if vipService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/vip")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/home", func(c *gin.Context) {
|
||||||
|
resp, err := vipService.GetHome(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/status", func(c *gin.Context) {
|
||||||
|
resp, err := vipService.GetStatus(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/preview", func(c *gin.Context) {
|
||||||
|
var req vip.PurchaseRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, vip.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := vipService.PreviewPurchase(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/purchase", func(c *gin.Context) {
|
||||||
|
var req vip.PurchaseRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, vip.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := vipService.Purchase(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/orders", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := vipService.PageUserOrders(c.Request.Context(), mustAuthUser(c), cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
residentGroup := engine.Group("/resident-activity/vip")
|
||||||
|
residentGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
residentGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := vipService.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
residentGroup.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req vip.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, vip.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := vipService.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
residentGroup.GET("/order/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := vipService.PageOrders(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
vip.ParseUserID(c.Query("userId")),
|
||||||
|
c.Query("status"),
|
||||||
|
c.Query("orderType"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
residentGroup.GET("/user-state/page", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := vipService.PageUserStates(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
vip.ParseUserID(c.Query("userId")),
|
||||||
|
c.Query("status"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
164
internal/router/voice_room_red_packet_routes.go
Normal file
164
internal/router/voice_room_red_packet_routes.go
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/service/voiceroomredpacket"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerVoiceRoomRedPacketRoutes 注册语音房红包 App 和后台接口。
|
||||||
|
func registerVoiceRoomRedPacketRoutes(
|
||||||
|
engine *gin.Engine,
|
||||||
|
javaClient authGateway,
|
||||||
|
service *voiceroomredpacket.Service,
|
||||||
|
) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/voice-room/red-packet")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/presence/heartbeat", func(c *gin.Context) {
|
||||||
|
var req voiceroomredpacket.PresenceRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomredpacket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.Heartbeat(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/presence/leave", func(c *gin.Context) {
|
||||||
|
var req voiceroomredpacket.PresenceRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomredpacket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.Leave(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/send", func(c *gin.Context) {
|
||||||
|
var req voiceroomredpacket.SendRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomredpacket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.Send(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/claim", func(c *gin.Context) {
|
||||||
|
var req voiceroomredpacket.ClaimRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomredpacket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.Claim(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/detail", func(c *gin.Context) {
|
||||||
|
resp, err := service.Detail(c.Request.Context(), mustAuthUser(c), c.Query("packetNo"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/claim-records", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListClaimRecords(c.Request.Context(), mustAuthUser(c), c.Query("packetNo"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/sent-records", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageSentRecords(c.Request.Context(), mustAuthUser(c), cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/resident-activity/voice-room-red-packet")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetAdminConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req voiceroomredpacket.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomredpacket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveAdminConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.GET("/records", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageAdminRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("status"),
|
||||||
|
voiceroomredpacket.ParseUserID(c.Query("userId")),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/refund/retry", func(c *gin.Context) {
|
||||||
|
var req voiceroomredpacket.ClaimRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomredpacket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.RetryRefund(c.Request.Context(), req.PacketNo); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
}
|
||||||
243
internal/router/voice_room_rocket_routes.go
Normal file
243
internal/router/voice_room_rocket_routes.go
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/service/voiceroomrocket"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerVoiceRoomRocketRoutes 注册语音房火箭 App、后台和内部调试接口。
|
||||||
|
func registerVoiceRoomRocketRoutes(
|
||||||
|
engine *gin.Engine,
|
||||||
|
cfg config.Config,
|
||||||
|
javaClient authGateway,
|
||||||
|
service *voiceroomrocket.Service,
|
||||||
|
) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/voice-room/rocket")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetConfig(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/level-contents", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetLevelContents(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/status", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetStatus(c.Request.Context(), mustAuthUser(c), parseInt64(c.Query("roomId")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/king", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "100")))
|
||||||
|
level, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("level", "1")))
|
||||||
|
roundNo, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("roundNo", "0")))
|
||||||
|
resp, err := service.GetKingRanking(c.Request.Context(), mustAuthUser(c), parseInt64(c.Query("roomId")), level, roundNo, cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/reward-popups", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListRewardPopups(c.Request.Context(), mustAuthUser(c), parseInt64(c.Query("roomId")))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/reward-popups/ack", func(c *gin.Context) {
|
||||||
|
var req voiceroomrocket.AckRewardPopupsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomrocket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.AckRewardPopups(c.Request.Context(), mustAuthUser(c), req); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
appGroup.GET("/reward-records", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
user := mustAuthUser(c)
|
||||||
|
roomID := parseInt64(c.Query("roomId"))
|
||||||
|
launchNo := strings.TrimSpace(c.Query("launchNo"))
|
||||||
|
scope := strings.ToLower(strings.TrimSpace(c.Query("scope")))
|
||||||
|
var (
|
||||||
|
resp *voiceroomrocket.RewardRecordPageResponse
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if launchNo != "" || scope == "room" {
|
||||||
|
resp, err = service.PageRoomRewardRecords(c.Request.Context(), user, roomID, launchNo, cursor, limit)
|
||||||
|
} else {
|
||||||
|
resp, err = service.PageRewardRecords(c.Request.Context(), user, roomID, cursor, limit)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/rule", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetRule(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
adminGroup := engine.Group("/resident-activity/voice-room-rocket")
|
||||||
|
adminGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
adminGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := service.GetAdminConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req voiceroomrocket.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomrocket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveAdminConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.GET("/level-configs", func(c *gin.Context) {
|
||||||
|
resp, err := service.ListLevelConfigs(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/level-configs/save", func(c *gin.Context) {
|
||||||
|
var req voiceroomrocket.SaveLevelConfigsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomrocket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveLevelConfigs(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.GET("/reward-configs", func(c *gin.Context) {
|
||||||
|
level, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("level", "0")))
|
||||||
|
resp, err := service.ListRewardConfigs(c.Request.Context(), c.Query("sysOrigin"), level)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/reward-configs/save", func(c *gin.Context) {
|
||||||
|
var req voiceroomrocket.SaveRewardConfigsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomrocket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.SaveRewardConfigs(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.GET("/launch-records", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageAdminLaunchRecords(c.Request.Context(), c.Query("sysOrigin"), parseInt64(c.Query("roomId")), cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.GET("/reward-records", func(c *gin.Context) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := service.PageAdminRewardRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
parseInt64(c.Query("roomId")),
|
||||||
|
parseInt64(c.Query("userId")),
|
||||||
|
c.Query("grantStatus"),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
adminGroup.POST("/reward-records/retry", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, voiceroomrocket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.RetryReward(c.Request.Context(), req.ID); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
|
||||||
|
internalGroup := engine.Group("/internal/voice-room/rocket")
|
||||||
|
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
|
internalGroup.POST("/gift-event", func(c *gin.Context) {
|
||||||
|
body, err := c.GetRawData()
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, voiceroomrocket.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ProcessGiftPayload(c.Request.Context(), string(body))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt64(value string) int64 {
|
||||||
|
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
@ -68,6 +68,16 @@ func registerSpecifiedGiftWeeklyRankRoutes(group *gin.RouterGroup, javaClient au
|
|||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
group.GET("/current-rank", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := weekStarService.CurrentRanking(c.Request.Context(), c.Query("sysOrigin"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
|
||||||
group.GET("/snapshot/page", func(c *gin.Context) {
|
group.GET("/snapshot/page", func(c *gin.Context) {
|
||||||
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
@ -98,7 +108,7 @@ func registerSpecifiedGiftWeeklyRankRoutes(group *gin.RouterGroup, javaClient au
|
|||||||
writeError(c, weekstar.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
writeError(c, weekstar.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := weekStarService.RetryRewardRecord(c.Request.Context(), req.ID)
|
resp, err := weekStarService.RetryRewardRecord(c.Request.Context(), req.ID.Int64())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err)
|
writeError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
428
internal/router/wheel_routes.go
Normal file
428
internal/router/wheel_routes.go
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/service/wheel"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const legacyGoldRewardCoverURL = "static/img/yumi-gold-coin.png"
|
||||||
|
|
||||||
|
// registerWheelRoutes registers app, admin, and copied H5-compatible wheel APIs.
|
||||||
|
func registerWheelRoutes(engine *gin.Engine, cfg config.Config, javaClient authGateway, wheelService *wheel.Service) {
|
||||||
|
if wheelService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appGroup := engine.Group("/app/wheel")
|
||||||
|
appGroup.Use(authMiddleware(javaClient))
|
||||||
|
appGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := wheelService.GetAppConfig(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.POST("/draw", func(c *gin.Context) {
|
||||||
|
var req wheel.DrawRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, wheel.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.IdempotencyKey = requestIdempotencyKey(c, req.IdempotencyKey)
|
||||||
|
resp, err := wheelService.Draw(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/history", func(c *gin.Context) {
|
||||||
|
cursor, limit := parseCursorLimit(c)
|
||||||
|
user := mustAuthUser(c)
|
||||||
|
resp, err := wheelService.PageRecords(c.Request.Context(), user.SysOrigin, c.Query("category"), user.UserID, "SUCCESS", cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
appGroup.GET("/hints", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
resp, err := wheelService.Hints(c.Request.Context(), mustAuthUser(c), c.Query("category"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"values": resp})
|
||||||
|
})
|
||||||
|
|
||||||
|
residentGroup := engine.Group("/resident-activity/wheel")
|
||||||
|
residentGroup.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
residentGroup.GET("/config", func(c *gin.Context) {
|
||||||
|
resp, err := wheelService.GetConfig(c.Request.Context(), c.Query("sysOrigin"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
residentGroup.POST("/config/save", func(c *gin.Context) {
|
||||||
|
var req wheel.SaveConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, wheel.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := wheelService.SaveConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
residentGroup.POST("/config/category/save", func(c *gin.Context) {
|
||||||
|
var req wheel.SaveCategoryConfigRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, wheel.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := wheelService.SaveCategoryConfig(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
residentGroup.GET("/record/page", func(c *gin.Context) {
|
||||||
|
cursor, limit := parseCursorLimit(c)
|
||||||
|
resp, err := wheelService.PageAdminRecords(
|
||||||
|
c.Request.Context(),
|
||||||
|
c.Query("sysOrigin"),
|
||||||
|
c.Query("category"),
|
||||||
|
wheel.ParseUserID(c.Query("userId")),
|
||||||
|
strings.TrimSpace(c.Query("status")),
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
c.Query("startTime"),
|
||||||
|
c.Query("endTime"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
residentGroup.POST("/record/retry", func(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
writeError(c, wheel.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := wheelService.RetryDrawRecord(c.Request.Context(), req.ID); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"success": true})
|
||||||
|
})
|
||||||
|
|
||||||
|
internalGroup := engine.Group("/internal/wheel")
|
||||||
|
internalGroup.Use(internalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
|
internalGroup.POST("/records/retry-failed", func(c *gin.Context) {
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "50")))
|
||||||
|
processed, err := wheelService.ProcessFailedRecords(c.Request.Context(), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"processed": processed})
|
||||||
|
})
|
||||||
|
|
||||||
|
registerWheelLegacyH5Routes(engine, javaClient, wheelService)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerWheelLegacyH5Routes(engine *gin.Engine, javaClient authGateway, wheelService *wheel.Service) {
|
||||||
|
legacyGroup := engine.Group("/client/api/v1")
|
||||||
|
legacyGroup.Use(authMiddleware(javaClient))
|
||||||
|
legacyGroup.GET("/config/mobile", func(c *gin.Context) {
|
||||||
|
resp, err := wheelService.GetAppConfig(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
legacyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
classic := legacyCategoryConfig(resp, "CLASSIC")
|
||||||
|
luxury := legacyCategoryConfig(resp, "LUXURY")
|
||||||
|
advanced := legacyCategoryConfig(resp, "ADVANCED")
|
||||||
|
legacyOK(c, gin.H{
|
||||||
|
"luckyBoxEnabled": classic.Enabled,
|
||||||
|
"middleLuckyBoxEnabled": luxury.Enabled,
|
||||||
|
"advancedLuckyBoxEnabled": advanced.Enabled,
|
||||||
|
"luckyBoxDrawAmount": classic.PriceOneGold,
|
||||||
|
"middleLuckyBoxDrawAmount": luxury.PriceOneGold,
|
||||||
|
"advancedLuckyBoxDrawAmount": advanced.PriceOneGold,
|
||||||
|
"luckyBoxDraw10Amount": classic.PriceTenGold,
|
||||||
|
"middleLuckyBoxDraw10Amount": luxury.PriceTenGold,
|
||||||
|
"advancedLuckyBoxDraw10Amount": advanced.PriceTenGold,
|
||||||
|
"luckyBoxDraw50Amount": classic.PriceFiftyGold,
|
||||||
|
"middleLuckyBoxDraw50Amount": luxury.PriceFiftyGold,
|
||||||
|
"advancedLuckyBoxDraw50Amount": advanced.PriceFiftyGold,
|
||||||
|
"wheelDrawAmount": classic.PriceOneGold,
|
||||||
|
"wheelDraw10Amount": classic.PriceTenGold,
|
||||||
|
"wheelDraw50Amount": classic.PriceFiftyGold,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
legacyGroup.POST("/wallet/list", func(c *gin.Context) {
|
||||||
|
balance, err := wheelService.WalletBalances(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
legacyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
legacyOK(c, []gin.H{
|
||||||
|
{"walletType": "COIN", "balance": balance},
|
||||||
|
{"walletType": "DCOIN", "balance": 0},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
probabilityGroup := legacyGroup.Group("/probability")
|
||||||
|
for _, prefix := range []string{"lucky-box", "middle-lucky-box", "advanced-lucky-box"} {
|
||||||
|
pathPrefix := "/" + prefix
|
||||||
|
category := legacyCategoryFromPrefix(prefix)
|
||||||
|
probabilityGroup.GET(pathPrefix+"/rewards", legacyRewardsHandler(wheelService, category))
|
||||||
|
probabilityGroup.POST(pathPrefix+"/rewards", legacyRewardsHandler(wheelService, category))
|
||||||
|
probabilityGroup.GET(pathPrefix+"/history", legacyHistoryHandler(wheelService, category))
|
||||||
|
probabilityGroup.POST(pathPrefix+"/history", legacyHistoryHandler(wheelService, category))
|
||||||
|
probabilityGroup.GET(pathPrefix+"/hints", legacyHintsHandler(wheelService, category))
|
||||||
|
probabilityGroup.POST(pathPrefix+"/hints", legacyHintsHandler(wheelService, category))
|
||||||
|
probabilityGroup.POST(pathPrefix+"/draw", legacyDrawHandler(wheelService, category))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRewardsHandler(wheelService *wheel.Service, category string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
resp, err := wheelService.GetAppConfig(c.Request.Context(), mustAuthUser(c))
|
||||||
|
if err != nil {
|
||||||
|
legacyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pool := legacyCategoryConfig(resp, category)
|
||||||
|
if !pool.Enabled {
|
||||||
|
legacyOK(c, []gin.H{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := make([]gin.H, 0, len(pool.Rewards))
|
||||||
|
for _, reward := range pool.Rewards {
|
||||||
|
items = append(items, gin.H{
|
||||||
|
"id": reward.ID,
|
||||||
|
"animationUrl": reward.ResourceURL,
|
||||||
|
"resourceUrl": reward.ResourceURL,
|
||||||
|
"value": legacyRewardValue(reward),
|
||||||
|
"merchandise": gin.H{
|
||||||
|
"animationUrl": reward.ResourceURL,
|
||||||
|
"coverUrl": legacyRewardCover(reward),
|
||||||
|
"name": legacyRewardName(reward),
|
||||||
|
"resourceUrl": reward.ResourceURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
legacyOK(c, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyDrawHandler(wheelService *wheel.Service, category string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var req wheel.DrawRequest
|
||||||
|
_ = c.ShouldBindJSON(&req)
|
||||||
|
req.Category = category
|
||||||
|
req.IdempotencyKey = requestIdempotencyKey(c, req.IdempotencyKey)
|
||||||
|
resp, err := wheelService.Draw(c.Request.Context(), mustAuthUser(c), req)
|
||||||
|
if err != nil {
|
||||||
|
legacyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rewards := make([]gin.H, 0, len(resp.Rewards))
|
||||||
|
for _, reward := range resp.Rewards {
|
||||||
|
rewards = append(rewards, gin.H{
|
||||||
|
"animationUrl": reward.ResourceURL,
|
||||||
|
"coverUrl": legacyDrawRewardCover(reward),
|
||||||
|
"count": reward.Count,
|
||||||
|
"name": legacyDrawRewardName(reward),
|
||||||
|
"resourceUrl": reward.ResourceURL,
|
||||||
|
"value": reward.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
legacyOK(c, gin.H{
|
||||||
|
"drawNo": resp.DrawNo,
|
||||||
|
"roomId": resp.RoomID,
|
||||||
|
"rewardValue": resp.RewardValue,
|
||||||
|
"rewards": rewards,
|
||||||
|
"balanceAfter": resp.BalanceAfter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyHistoryHandler(wheelService *wheel.Service, category string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
cursor, limit := parseCursorLimit(c)
|
||||||
|
user := mustAuthUser(c)
|
||||||
|
resp, err := wheelService.PageRecords(c.Request.Context(), user.SysOrigin, category, user.UserID, "SUCCESS", cursor, limit)
|
||||||
|
if err != nil {
|
||||||
|
legacyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
records := make([]gin.H, 0, len(resp.Records))
|
||||||
|
for _, record := range resp.Records {
|
||||||
|
records = append(records, gin.H{
|
||||||
|
"createdTime": record.CreateTime,
|
||||||
|
"drawNo": record.DrawNo,
|
||||||
|
"drawTimes": record.DrawTimes,
|
||||||
|
"rewardValue": legacyRecordValue(record),
|
||||||
|
"rewards": []gin.H{{
|
||||||
|
"animationUrl": record.ResourceURL,
|
||||||
|
"coverUrl": legacyRecordCover(record),
|
||||||
|
"count": 1,
|
||||||
|
"name": legacyRecordName(record),
|
||||||
|
"resourceUrl": record.ResourceURL,
|
||||||
|
"value": legacyRecordValue(record),
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
legacyOK(c, gin.H{"records": records, "total": resp.Total})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyHintsHandler(wheelService *wheel.Service, category string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
resp, err := wheelService.Hints(c.Request.Context(), mustAuthUser(c), category, 20)
|
||||||
|
if err != nil {
|
||||||
|
legacyError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
values := make([]gin.H, 0, len(resp))
|
||||||
|
for index, reward := range resp {
|
||||||
|
values = append(values, gin.H{
|
||||||
|
"avatar": reward.CoverURL,
|
||||||
|
"nickname": "User" + strconv.Itoa(index+1),
|
||||||
|
"value": legacyDrawRewardName(reward),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
legacyOK(c, gin.H{"values": values})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCursorLimit(c *gin.Context) (int, int) {
|
||||||
|
cursor, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("cursor", "1")))
|
||||||
|
limit, _ := strconv.Atoi(strings.TrimSpace(c.DefaultQuery("limit", "20")))
|
||||||
|
return cursor, limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyOK(c *gin.Context, data any) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": data})
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyError(c *gin.Context, err error) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "data": nil})
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyCategoryFromPrefix(prefix string) string {
|
||||||
|
switch prefix {
|
||||||
|
case "middle-lucky-box":
|
||||||
|
return "LUXURY"
|
||||||
|
case "advanced-lucky-box":
|
||||||
|
return "ADVANCED"
|
||||||
|
default:
|
||||||
|
return "CLASSIC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyCategoryConfig(resp *wheel.ConfigResponse, category string) wheel.CategoryConfigPayload {
|
||||||
|
for _, item := range resp.Categories {
|
||||||
|
if strings.EqualFold(item.Category, category) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wheel.CategoryConfigPayload{Category: category, Rewards: []wheel.RewardConfigPayload{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRewardValue(reward wheel.RewardConfigPayload) int64 {
|
||||||
|
if strings.EqualFold(reward.RewardType, "GOLD") {
|
||||||
|
return reward.GoldAmount
|
||||||
|
}
|
||||||
|
return reward.DisplayGoldAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRewardCover(reward wheel.RewardConfigPayload) string {
|
||||||
|
if strings.TrimSpace(reward.CoverURL) != "" {
|
||||||
|
return reward.CoverURL
|
||||||
|
}
|
||||||
|
if strings.EqualFold(reward.RewardType, "GOLD") {
|
||||||
|
return legacyGoldRewardCoverURL
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyDrawRewardCover(reward wheel.DrawRewardPayload) string {
|
||||||
|
if strings.TrimSpace(reward.CoverURL) != "" {
|
||||||
|
return reward.CoverURL
|
||||||
|
}
|
||||||
|
if strings.EqualFold(reward.RewardType, "GOLD") {
|
||||||
|
return legacyGoldRewardCoverURL
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRecordCover(record wheel.DrawRecordPayload) string {
|
||||||
|
if strings.TrimSpace(record.CoverURL) != "" {
|
||||||
|
return record.CoverURL
|
||||||
|
}
|
||||||
|
if strings.EqualFold(record.RewardType, "GOLD") {
|
||||||
|
return legacyGoldRewardCoverURL
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRecordValue(record wheel.DrawRecordPayload) int64 {
|
||||||
|
if strings.EqualFold(record.RewardType, "GOLD") {
|
||||||
|
return record.GoldAmount
|
||||||
|
}
|
||||||
|
return record.DisplayGoldAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRewardName(reward wheel.RewardConfigPayload) string {
|
||||||
|
if reward.ResourceName != "" {
|
||||||
|
return reward.ResourceName
|
||||||
|
}
|
||||||
|
if reward.RewardType == "GOLD" {
|
||||||
|
return "金币"
|
||||||
|
}
|
||||||
|
return "Reward"
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyDrawRewardName(reward wheel.DrawRewardPayload) string {
|
||||||
|
if reward.Name != "" {
|
||||||
|
return reward.Name
|
||||||
|
}
|
||||||
|
if reward.ResourceName != "" {
|
||||||
|
return reward.ResourceName
|
||||||
|
}
|
||||||
|
if reward.RewardType == "GOLD" {
|
||||||
|
return "金币"
|
||||||
|
}
|
||||||
|
return "Reward"
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyRecordName(record wheel.DrawRecordPayload) string {
|
||||||
|
if record.ResourceName != "" {
|
||||||
|
return record.ResourceName
|
||||||
|
}
|
||||||
|
if record.RewardType == "GOLD" {
|
||||||
|
return "金币"
|
||||||
|
}
|
||||||
|
return "Reward"
|
||||||
|
}
|
||||||
118
internal/router/wheel_routes_test.go
Normal file
118
internal/router/wheel_routes_test.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/repo"
|
||||||
|
wheelservice "chatapp3-golang/internal/service/wheel"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWheelLegacyRewardsExposeDynamicResourceURL(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
db := newRouterWheelDB(t)
|
||||||
|
now := time.Now()
|
||||||
|
resourceID := int64(2045029471274201089)
|
||||||
|
resourceURL := "https://cdn.example.com/reward.svga"
|
||||||
|
coverURL := "https://cdn.example.com/reward.png"
|
||||||
|
|
||||||
|
if err := db.Create(&model.WheelConfig{
|
||||||
|
ID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "Asia/Shanghai",
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create wheel config: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.WheelPoolConfig{
|
||||||
|
ID: 10,
|
||||||
|
ConfigID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: "CLASSIC",
|
||||||
|
Enabled: true,
|
||||||
|
PriceOneGold: 500,
|
||||||
|
Sort: 1,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create wheel pool: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.WheelRewardConfig{
|
||||||
|
ID: 100,
|
||||||
|
ConfigID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Category: "CLASSIC",
|
||||||
|
RewardType: "RESOURCE",
|
||||||
|
ResourceID: &resourceID,
|
||||||
|
ResourceType: "GIFT",
|
||||||
|
ResourceName: "Dynamic Gift",
|
||||||
|
ResourceURL: resourceURL,
|
||||||
|
CoverURL: coverURL,
|
||||||
|
DurationDays: 7,
|
||||||
|
DisplayGoldAmount: 1000,
|
||||||
|
Probability: 1000000,
|
||||||
|
Sort: 1,
|
||||||
|
Enabled: true,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create wheel reward: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := wheelservice.NewService(config.Config{}, db, nil)
|
||||||
|
engine := NewRouter(config.Config{}, &repo.Repository{}, appPopupAuthStub{}, Services{Wheel: service})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/client/api/v1/probability/lucky-box/rewards", 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)
|
||||||
|
}
|
||||||
|
if payload["code"] != float64(0) {
|
||||||
|
t.Fatalf("code = %#v body = %s", payload["code"], rec.Body.String())
|
||||||
|
}
|
||||||
|
data, ok := payload["data"].([]any)
|
||||||
|
if !ok || len(data) == 0 {
|
||||||
|
t.Fatalf("data = %#v, want rewards", payload["data"])
|
||||||
|
}
|
||||||
|
item := data[0].(map[string]any)
|
||||||
|
if item["resourceUrl"] != resourceURL || item["animationUrl"] != resourceURL {
|
||||||
|
t.Fatalf("dynamic urls = resource:%#v animation:%#v", item["resourceUrl"], item["animationUrl"])
|
||||||
|
}
|
||||||
|
merchandise := item["merchandise"].(map[string]any)
|
||||||
|
if merchandise["resourceUrl"] != resourceURL || merchandise["animationUrl"] != resourceURL {
|
||||||
|
t.Fatalf("merchandise dynamic urls = resource:%#v animation:%#v", merchandise["resourceUrl"], merchandise["animationUrl"])
|
||||||
|
}
|
||||||
|
if merchandise["coverUrl"] != coverURL {
|
||||||
|
t.Fatalf("coverUrl = %#v, want %q", merchandise["coverUrl"], coverURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterWheelDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.WheelConfig{}, &model.WheelPoolConfig{}, &model.WheelRewardConfig{}); err != nil {
|
||||||
|
t.Fatalf("migrate wheel tables: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
335
internal/router/yumi_gift_challenge_routes.go
Normal file
335
internal/router/yumi_gift_challenge_routes.go
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/service/yumigiftchallenge"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const yumiGiftChallengeMenuAlias = "ResidentYumiGiftChallenge"
|
||||||
|
|
||||||
|
// registerYumiGiftChallengeRoutes 注册 Yumi H5 用户接口与 Console 配置、结算、核账接口。
|
||||||
|
func registerYumiGiftChallengeRoutes(engine *gin.Engine, cfg config.Config, javaClient authGateway, service *yumigiftchallenge.Service) {
|
||||||
|
if service == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internal := engine.Group("/internal/yumi-gift-challenge")
|
||||||
|
// 结算会产生真实奖励副作用;密钥缺失也必须拒绝,不能沿用历史内部接口的空密钥兼容行为。
|
||||||
|
internal.Use(requiredInternalSecretMiddleware(cfg.HTTP.InternalCallbackSecret))
|
||||||
|
internal.POST("/settle-due", func(c *gin.Context) {
|
||||||
|
batch, err := parseOptionalInt(c.Query("batch"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.SettleDue(c.Request.Context(), batch)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
h5 := engine.Group("/app/h5/gift-challenge/yumi")
|
||||||
|
h5.Use(authMiddleware(javaClient))
|
||||||
|
h5.GET("/detail", func(c *gin.Context) {
|
||||||
|
activityID, err := parseOptionalInt64(c.Query("activityId"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.GetDetail(c.Request.Context(), mustAuthUser(c), activityID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
h5.POST("/enter", func(c *gin.Context) {
|
||||||
|
var request yumigiftchallenge.EnterRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.Enter(c.Request.Context(), mustAuthUser(c), request)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
h5.GET("/ranking", func(c *gin.Context) {
|
||||||
|
activityID, err := parseOptionalInt64(c.Query("activityId"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit, err := parseOptionalInt(c.Query("limit"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
period := firstNonEmptyString(c.Query("periodType"), c.Query("period"))
|
||||||
|
response, err := service.GetRanking(c.Request.Context(), mustAuthUser(c), activityID, period, c.Query("statDate"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
h5.POST("/tasks/:taskCode/claim", func(c *gin.Context) {
|
||||||
|
var request yumigiftchallenge.ClaimRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.ClaimTask(c.Request.Context(), mustAuthUser(c), c.Param("taskCode"), request)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
|
||||||
|
admin := engine.Group("/resident-activity/yumi-gift-challenge")
|
||||||
|
admin.Use(consoleAuthMiddleware(javaClient))
|
||||||
|
read := admin.Group("")
|
||||||
|
read.Use(consoleMenuMiddleware(javaClient, yumiGiftChallengeMenuAlias))
|
||||||
|
read.GET("/list", func(c *gin.Context) {
|
||||||
|
page, pageErr := parseOptionalInt(c.Query("page"))
|
||||||
|
size, sizeErr := parseOptionalInt(c.Query("size"))
|
||||||
|
if pageErr != nil || sizeErr != nil {
|
||||||
|
writeError(c, firstParseError(pageErr, sizeErr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.ListActivities(c.Request.Context(), c.Query("sysOrigin"), page, size)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
read.GET("/detail/:id", func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.GetAdminDetail(c.Request.Context(), activityID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
read.GET("/tasks/:id", func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.GetAdminDetail(c.Request.Context(), activityID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response.Tasks)
|
||||||
|
})
|
||||||
|
read.GET("/rank-rewards/:id", func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.GetAdminDetail(c.Request.Context(), activityID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response.RankRewards)
|
||||||
|
})
|
||||||
|
read.GET("/ranking/:id", func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit, err := parseOptionalInt(c.Query("limit"))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
period := firstNonEmptyString(c.Query("periodType"), c.Query("period"))
|
||||||
|
response, err := service.GetAdminRanking(c.Request.Context(), activityID, period, c.Query("statDate"), limit)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
read.GET("/settlement-records/:id", func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page, pageErr := parseOptionalInt(c.Query("page"))
|
||||||
|
size, sizeErr := parseOptionalInt(c.Query("size"))
|
||||||
|
if pageErr != nil || sizeErr != nil {
|
||||||
|
writeError(c, firstParseError(pageErr, sizeErr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.PageSettlementRecords(c.Request.Context(), activityID, c.Query("periodType"), c.Query("statDate"), c.Query("deliveryStatus"), page, size)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
|
||||||
|
admin.POST("/save", consolePermissionMiddleware(javaClient, "resident-activity:yumi-gift-challenge:edit"), func(c *gin.Context) {
|
||||||
|
var request yumigiftchallenge.SaveRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.SaveActivity(c.Request.Context(), request)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
admin.PUT("/enable/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-gift-challenge:enable"), func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enabled, err := strconv.ParseBool(strings.TrimSpace(c.Query("enabled")))
|
||||||
|
if err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.SetEnabled(c.Request.Context(), activityID, enabled); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"id": strconv.FormatInt(activityID, 10), "enabled": enabled})
|
||||||
|
})
|
||||||
|
admin.PUT("/tasks/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-gift-challenge:edit"), func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request yumigiftchallenge.TaskSaveRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.ActivityID = yumigiftchallenge.FlexibleInt64(activityID)
|
||||||
|
response, err := service.SaveTasks(c.Request.Context(), activityID, request)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
admin.PUT("/rank-rewards/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-gift-challenge:edit"), func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request yumigiftchallenge.RankRewardSaveRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.ActivityID = yumigiftchallenge.FlexibleInt64(activityID)
|
||||||
|
response, err := service.SaveRankRewards(c.Request.Context(), activityID, request)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, response)
|
||||||
|
})
|
||||||
|
admin.POST("/settlement/:id", consolePermissionMiddleware(javaClient, "resident-activity:yumi-gift-challenge:settle"), func(c *gin.Context) {
|
||||||
|
activityID, ok := requirePathInt64(c, "id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request yumigiftchallenge.SettlementRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.Settle(c.Request.Context(), activityID, request); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"settled": true})
|
||||||
|
})
|
||||||
|
admin.POST("/delivery-item/resolve/:itemId", consolePermissionMiddleware(javaClient, "resident-activity:yumi-gift-challenge:reconcile"), func(c *gin.Context) {
|
||||||
|
itemID, ok := requirePathInt64(c, "itemId")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request yumigiftchallenge.ResolveDeliveryRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
writeBadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.ResolveUnknownDeliveryItem(c.Request.Context(), itemID, request.Delivered); err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, gin.H{"id": strconv.FormatInt(itemID, 10), "delivered": request.Delivered})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptionalInt64(value string) (int64, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil || parsed <= 0 {
|
||||||
|
return 0, yumigiftchallenge.NewAppError(http.StatusBadRequest, "invalid_id", "id must be a positive int64 string")
|
||||||
|
}
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptionalInt(value string) (int, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
parsed, err := strconv.Atoi(value)
|
||||||
|
if err != nil || parsed < 0 {
|
||||||
|
return 0, yumigiftchallenge.NewAppError(http.StatusBadRequest, "invalid_number", "query number must be a non-negative integer")
|
||||||
|
}
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requirePathInt64(c *gin.Context, name string) (int64, bool) {
|
||||||
|
value, err := parseOptionalInt64(c.Param(name))
|
||||||
|
if err != nil || value <= 0 {
|
||||||
|
if err == nil {
|
||||||
|
err = yumigiftchallenge.NewAppError(http.StatusBadRequest, "invalid_id", name+" must be a positive int64 string")
|
||||||
|
}
|
||||||
|
writeError(c, err)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstParseError(values ...error) error {
|
||||||
|
for _, err := range values {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return yumigiftchallenge.NewAppError(http.StatusBadRequest, "bad_request", "invalid query")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBadRequest(c *gin.Context, err error) {
|
||||||
|
writeError(c, yumigiftchallenge.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
||||||
|
}
|
||||||
237
internal/service/apppopup/admin.go
Normal file
237
internal/service/apppopup/admin.go
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
package apppopup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/utils"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PageAdminConfigs 返回后台首页弹窗配置分页。
|
||||||
|
func (s *Service) PageAdminConfigs(ctx context.Context, sysOrigin, scene, popupKey string, cursor, limit int) (*AdminConfigPageResponse, error) {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "app_popup_unavailable", "app popup service is unavailable")
|
||||||
|
}
|
||||||
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||||
|
scene = normalizeScene(scene)
|
||||||
|
cursor = normalizePageCursor(cursor)
|
||||||
|
limit = normalizePageLimit(limit)
|
||||||
|
|
||||||
|
query := s.db.WithContext(ctx).
|
||||||
|
Model(&model.AppPopupConfig{}).
|
||||||
|
Where("sys_origin = ? AND scene = ?", sysOrigin, scene)
|
||||||
|
if key := normalizePopupKey(popupKey); key != "" {
|
||||||
|
query = query.Where("popup_key LIKE ?", "%"+key+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []model.AppPopupConfig
|
||||||
|
if err := query.
|
||||||
|
Order("priority DESC").
|
||||||
|
Order("id ASC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset((cursor - 1) * limit).
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]AdminConfigItem, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
items = append(items, adminItemFromModel(row))
|
||||||
|
}
|
||||||
|
return &AdminConfigPageResponse{
|
||||||
|
Records: items,
|
||||||
|
Total: total,
|
||||||
|
Current: cursor,
|
||||||
|
Size: limit,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAdminConfig 创建或更新后台首页弹窗配置。
|
||||||
|
func (s *Service) SaveAdminConfig(ctx context.Context, req SaveAdminConfigRequest) (*AdminConfigItem, error) {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "app_popup_unavailable", "app popup service is unavailable")
|
||||||
|
}
|
||||||
|
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
||||||
|
scene := normalizeScene(req.Scene)
|
||||||
|
popupKey := normalizePopupKey(req.PopupKey)
|
||||||
|
if popupKey == "" {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "popup_key_required", "popupKey is required")
|
||||||
|
}
|
||||||
|
if req.LimitDays < 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_limit_days", "limitDays must be greater than or equal to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedID int64
|
||||||
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var row model.AppPopupConfig
|
||||||
|
requestID := req.ID.Int64()
|
||||||
|
if requestID > 0 {
|
||||||
|
err := tx.Where("id = ?", requestID).First(&row).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return NewAppError(http.StatusNotFound, "app_popup_config_not_found", "config not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
id, err := utils.NextID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
row.ID = id
|
||||||
|
row.CreateTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicate model.AppPopupConfig
|
||||||
|
err := tx.Where("sys_origin = ? AND scene = ? AND popup_key = ? AND id <> ?", sysOrigin, scene, popupKey, row.ID).
|
||||||
|
First(&duplicate).Error
|
||||||
|
if err == nil {
|
||||||
|
return NewAppError(http.StatusConflict, "duplicate_popup_key", "popupKey already exists")
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
row.SysOrigin = sysOrigin
|
||||||
|
row.Scene = scene
|
||||||
|
row.PopupKey = popupKey
|
||||||
|
row.Name = defaultIfBlank(req.Name, popupKey)
|
||||||
|
row.Enabled = req.Enabled
|
||||||
|
row.LimitDays = req.LimitDays
|
||||||
|
row.Priority = req.Priority
|
||||||
|
row.Version = normalizeVersion(req.Version)
|
||||||
|
row.ContentJSON = mustContentJSON(req.Title, req.Image)
|
||||||
|
row.JumpType = strings.TrimSpace(req.JumpType)
|
||||||
|
row.JumpURL = strings.TrimSpace(req.JumpURL)
|
||||||
|
row.Remark = strings.TrimSpace(req.Description)
|
||||||
|
row.PlatformsJSON = normalizeJSONField(req.PlatformsJSON)
|
||||||
|
row.MinAppVersion = strings.TrimSpace(req.MinAppVersion)
|
||||||
|
row.MaxAppVersion = strings.TrimSpace(req.MaxAppVersion)
|
||||||
|
row.UserScopeType = "ALL"
|
||||||
|
if strings.TrimSpace(row.UserScopeJSON) == "" {
|
||||||
|
row.UserScopeJSON = "{}"
|
||||||
|
}
|
||||||
|
row.UpdateTime = now
|
||||||
|
if row.CreateTime.IsZero() {
|
||||||
|
row.CreateTime = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Save(&row).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
savedID = row.ID
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ClearConfigCache(ctx, sysOrigin, scene)
|
||||||
|
var saved model.AppPopupConfig
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ?", savedID).First(&saved).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item := adminItemFromModel(saved)
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAdminConfig 删除后台首页弹窗配置。
|
||||||
|
func (s *Service) DeleteAdminConfig(ctx context.Context, id int64) error {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return NewAppError(http.StatusServiceUnavailable, "app_popup_unavailable", "app popup service is unavailable")
|
||||||
|
}
|
||||||
|
if id <= 0 {
|
||||||
|
return NewAppError(http.StatusBadRequest, "invalid_id", "id is required")
|
||||||
|
}
|
||||||
|
var row model.AppPopupConfig
|
||||||
|
if err := s.db.WithContext(ctx).Where("id = ?", id).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return NewAppError(http.StatusNotFound, "app_popup_config_not_found", "config not found")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.db.WithContext(ctx).Delete(&row).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.ClearConfigCache(ctx, row.SysOrigin, row.Scene)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearConfigCache 清理指定场景的配置缓存,供后台配置保存后调用。
|
||||||
|
func (s *Service) ClearConfigCache(ctx context.Context, sysOrigin, scene string) {
|
||||||
|
if s == nil || s.cache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.cache.Del(ctx, appPopupConfigCacheKey(sysOrigin, scene)).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePageCursor(cursor int) int {
|
||||||
|
if cursor <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePageLimit(limit int) int {
|
||||||
|
if limit <= 0 {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultIfBlank(value, fallback string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustContentJSON(title, image string) string {
|
||||||
|
payload := map[string]any{}
|
||||||
|
if title = strings.TrimSpace(title); title != "" {
|
||||||
|
payload["title"] = title
|
||||||
|
}
|
||||||
|
if image = strings.TrimSpace(image); image != "" {
|
||||||
|
payload["image"] = image
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeJSONField(raw string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
var value any
|
||||||
|
if err := json.Unmarshal([]byte(raw), &value); err != nil {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
return string(encoded)
|
||||||
|
}
|
||||||
299
internal/service/apppopup/query.go
Normal file
299
internal/service/apppopup/query.go
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
package apppopup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryEntryPopups 返回 APP 进场弹窗配置。服务端不做用户展示频控。
|
||||||
|
func (s *Service) QueryEntryPopups(ctx context.Context, user AuthUser, req EntryQueryRequest) (*EntryQueryResponse, error) {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "app_popup_unavailable", "app popup service is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
sysOrigin := normalizeSysOrigin(user.SysOrigin)
|
||||||
|
scene := normalizeScene(req.Scene)
|
||||||
|
configs, err := s.loadConfigs(ctx, sysOrigin, scene)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := s.now()
|
||||||
|
resp := &EntryQueryResponse{
|
||||||
|
RequestID: newRequestID(),
|
||||||
|
Queue: []string{},
|
||||||
|
Items: map[string]PopupItem{},
|
||||||
|
}
|
||||||
|
for _, cfg := range configs {
|
||||||
|
key := normalizePopupKey(cfg.PopupKey)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item := s.buildPopupItem(cfg, req, user.UserID, now)
|
||||||
|
resp.set(key, item)
|
||||||
|
if item.Value {
|
||||||
|
resp.Queue = append(resp.Queue, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.SliceStable(resp.Queue, func(i, j int) bool {
|
||||||
|
left := resp.Items[resp.Queue[i]]
|
||||||
|
right := resp.Items[resp.Queue[j]]
|
||||||
|
if left.Priority == right.Priority {
|
||||||
|
return resp.Queue[i] < resp.Queue[j]
|
||||||
|
}
|
||||||
|
return left.Priority > right.Priority
|
||||||
|
})
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadConfigs(ctx context.Context, sysOrigin, scene string) ([]cachedConfig, error) {
|
||||||
|
cacheKey := appPopupConfigCacheKey(sysOrigin, scene)
|
||||||
|
if s.cache != nil {
|
||||||
|
raw, err := s.cache.Get(ctx, cacheKey).Result()
|
||||||
|
if err == nil && strings.TrimSpace(raw) != "" {
|
||||||
|
var configs []cachedConfig
|
||||||
|
if jsonErr := json.Unmarshal([]byte(raw), &configs); jsonErr == nil {
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
|
} else if err != nil && !errors.Is(err, redis.Nil) {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "app_popup_cache_failed", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []model.AppPopupConfig
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND scene = ?", sysOrigin, scene).
|
||||||
|
Order("priority DESC").
|
||||||
|
Order("id ASC").
|
||||||
|
Find(&rows).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return []cachedConfig{}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configs := make([]cachedConfig, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
configs = append(configs, toCachedConfig(row))
|
||||||
|
}
|
||||||
|
if s.cache != nil {
|
||||||
|
if encoded, err := json.Marshal(configs); err == nil {
|
||||||
|
_ = s.cache.Set(ctx, cacheKey, string(encoded), configCacheExpiration).Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appPopupConfigCacheKey(sysOrigin, scene string) string {
|
||||||
|
return "app_popup:configs:" + normalizeSysOrigin(sysOrigin) + ":" + normalizeScene(scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) buildPopupItem(cfg cachedConfig, req EntryQueryRequest, userID int64, now time.Time) PopupItem {
|
||||||
|
content := parsePopupContent(cfg.ContentJSON)
|
||||||
|
item := PopupItem{
|
||||||
|
Value: true,
|
||||||
|
Limit: normalizeLimitDays(cfg.LimitDays),
|
||||||
|
PopupID: cfg.ID,
|
||||||
|
Version: normalizeVersion(cfg.Version),
|
||||||
|
Priority: cfg.Priority,
|
||||||
|
Title: content.Title,
|
||||||
|
Image: content.Image,
|
||||||
|
JumpType: strings.TrimSpace(cfg.JumpType),
|
||||||
|
JumpURL: strings.TrimSpace(cfg.JumpURL),
|
||||||
|
Reason: "",
|
||||||
|
Content: content.Raw,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Enabled {
|
||||||
|
item.Value = false
|
||||||
|
item.Reason = reasonDisabled
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if cfg.StartTime != nil && !cfg.StartTime.IsZero() && now.Before(*cfg.StartTime) {
|
||||||
|
item.Value = false
|
||||||
|
item.Reason = reasonNotStarted
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if cfg.EndTime != nil && !cfg.EndTime.IsZero() && !now.Before(*cfg.EndTime) {
|
||||||
|
item.Value = false
|
||||||
|
item.Reason = reasonExpired
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if !matchPlatforms(cfg.PlatformsJSON, req.Platform) {
|
||||||
|
item.Value = false
|
||||||
|
item.Reason = reasonPlatform
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if !matchMinVersion(req.AppVersion, cfg.MinAppVersion) {
|
||||||
|
item.Value = false
|
||||||
|
item.Reason = reasonMinVersion
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if !matchMaxVersion(req.AppVersion, cfg.MaxAppVersion) {
|
||||||
|
item.Value = false
|
||||||
|
item.Reason = reasonMaxVersion
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if !matchUserScope(cfg.UserScopeType, cfg.UserScopeJSON, userID) {
|
||||||
|
item.Value = false
|
||||||
|
item.Reason = reasonUserScope
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeLimitDays(limit int) int {
|
||||||
|
if limit < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeVersion(version int) int {
|
||||||
|
if version <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchPlatforms(raw, platform string) bool {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
platform = normalizePlatform(platform)
|
||||||
|
if raw == "" || raw == "null" || platform == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var platforms []string
|
||||||
|
if err := json.Unmarshal([]byte(raw), &platforms); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(platforms) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, item := range platforms {
|
||||||
|
if normalizePlatform(item) == platform {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchMinVersion(appVersion, minVersion string) bool {
|
||||||
|
appVersion = strings.TrimSpace(appVersion)
|
||||||
|
minVersion = strings.TrimSpace(minVersion)
|
||||||
|
if appVersion == "" || minVersion == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return compareVersion(appVersion, minVersion) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchMaxVersion(appVersion, maxVersion string) bool {
|
||||||
|
appVersion = strings.TrimSpace(appVersion)
|
||||||
|
maxVersion = strings.TrimSpace(maxVersion)
|
||||||
|
if appVersion == "" || maxVersion == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return compareVersion(appVersion, maxVersion) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareVersion(left, right string) int {
|
||||||
|
leftParts := parseVersion(left)
|
||||||
|
rightParts := parseVersion(right)
|
||||||
|
maxLen := len(leftParts)
|
||||||
|
if len(rightParts) > maxLen {
|
||||||
|
maxLen = len(rightParts)
|
||||||
|
}
|
||||||
|
for i := 0; i < maxLen; i++ {
|
||||||
|
var lv, rv int
|
||||||
|
if i < len(leftParts) {
|
||||||
|
lv = leftParts[i]
|
||||||
|
}
|
||||||
|
if i < len(rightParts) {
|
||||||
|
rv = rightParts[i]
|
||||||
|
}
|
||||||
|
if lv > rv {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if lv < rv {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVersion(version string) []int {
|
||||||
|
version = strings.TrimSpace(version)
|
||||||
|
if version == "" {
|
||||||
|
return []int{0}
|
||||||
|
}
|
||||||
|
parts := strings.Split(version, ".")
|
||||||
|
result := make([]int, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part == "" {
|
||||||
|
result = append(result, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
digits := strings.Builder{}
|
||||||
|
for _, r := range part {
|
||||||
|
if r < '0' || r > '9' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
digits.WriteRune(r)
|
||||||
|
}
|
||||||
|
if digits.Len() == 0 {
|
||||||
|
result = append(result, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, err := strconv.Atoi(digits.String())
|
||||||
|
if err != nil {
|
||||||
|
result = append(result, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchUserScope(scopeType, raw string, userID int64) bool {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(scopeType)) {
|
||||||
|
case "", "ALL":
|
||||||
|
return true
|
||||||
|
case "WHITELIST":
|
||||||
|
return userID > 0 && userScopeContains(raw, userID)
|
||||||
|
case "BLACKLIST":
|
||||||
|
return userID <= 0 || !userScopeContains(raw, userID)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userScopeContains(raw string, userID int64) bool {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" || raw == "null" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
UserIDs []int64 `json:"userIds"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, item := range payload.UserIDs {
|
||||||
|
if item == userID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
351
internal/service/apppopup/query_test.go
Normal file
351
internal/service/apppopup/query_test.go
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
package apppopup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQueryEntryPopupsReturnsConfigWithoutUserCooldown(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC)
|
||||||
|
mustCreatePopupConfig(t, db, model.AppPopupConfig{
|
||||||
|
ID: 1001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "invite",
|
||||||
|
Name: "邀请活动弹窗",
|
||||||
|
Scene: defaultScene,
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 100,
|
||||||
|
Version: 3,
|
||||||
|
ContentJSON: `{"title":"Invite friends","image":"https://cdn.example.com/invite.png"}`,
|
||||||
|
JumpType: "H5",
|
||||||
|
JumpURL: "/h5/app-invite/landing.html",
|
||||||
|
})
|
||||||
|
mustCreatePopupConfig(t, db, model.AppPopupConfig{
|
||||||
|
ID: 1002,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "game",
|
||||||
|
Name: "游戏引导弹窗",
|
||||||
|
Scene: defaultScene,
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 0,
|
||||||
|
Priority: 90,
|
||||||
|
Version: 1,
|
||||||
|
ContentJSON: `{"title":"Play games","image":"https://cdn.example.com/game.png"}`,
|
||||||
|
JumpType: "APP_ROUTE",
|
||||||
|
JumpURL: "/game-center",
|
||||||
|
})
|
||||||
|
|
||||||
|
service := NewService(config.Config{}, db, nil)
|
||||||
|
service.now = func() time.Time { return now }
|
||||||
|
resp, err := service.QueryEntryPopups(context.Background(), AuthUser{
|
||||||
|
UserID: 123,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, EntryQueryRequest{Scene: defaultScene, Platform: "ios", AppVersion: "3.12.0"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("QueryEntryPopups() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Queue) != 2 || resp.Queue[0] != "invite" || resp.Queue[1] != "game" {
|
||||||
|
t.Fatalf("queue = %#v, want [invite game]", resp.Queue)
|
||||||
|
}
|
||||||
|
invite := resp.Items["invite"]
|
||||||
|
if !invite.Value || invite.Limit != 7 || invite.PopupID != 1001 || invite.Version != 3 {
|
||||||
|
t.Fatalf("invite = %+v, want enabled limit 7 id 1001 version 3", invite)
|
||||||
|
}
|
||||||
|
if invite.Title != "Invite friends" || invite.Image != "https://cdn.example.com/invite.png" {
|
||||||
|
t.Fatalf("invite content = title %q image %q", invite.Title, invite.Image)
|
||||||
|
}
|
||||||
|
game := resp.Items["game"]
|
||||||
|
if !game.Value || game.Limit != 0 {
|
||||||
|
t.Fatalf("game = %+v, want enabled limit 0", game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryEntryPopupsFiltersByConfigRulesOnly(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC)
|
||||||
|
future := now.Add(time.Hour)
|
||||||
|
past := now.Add(-time.Hour)
|
||||||
|
mustCreatePopupConfig(t, db, model.AppPopupConfig{
|
||||||
|
ID: 1001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "invite",
|
||||||
|
Scene: defaultScene,
|
||||||
|
Enabled: false,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 100,
|
||||||
|
Version: 1,
|
||||||
|
UserScopeType: "ALL",
|
||||||
|
})
|
||||||
|
mustCreatePopupConfig(t, db, model.AppPopupConfig{
|
||||||
|
ID: 1002,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "game",
|
||||||
|
Scene: defaultScene,
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 90,
|
||||||
|
Version: 1,
|
||||||
|
PlatformsJSON: `["android"]`,
|
||||||
|
UserScopeType: "ALL",
|
||||||
|
})
|
||||||
|
mustCreatePopupConfig(t, db, model.AppPopupConfig{
|
||||||
|
ID: 1003,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "future",
|
||||||
|
Scene: defaultScene,
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 80,
|
||||||
|
Version: 1,
|
||||||
|
StartTime: &future,
|
||||||
|
UserScopeType: "ALL",
|
||||||
|
})
|
||||||
|
mustCreatePopupConfig(t, db, model.AppPopupConfig{
|
||||||
|
ID: 1004,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "expired",
|
||||||
|
Scene: defaultScene,
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 70,
|
||||||
|
Version: 1,
|
||||||
|
EndTime: &past,
|
||||||
|
UserScopeType: "ALL",
|
||||||
|
})
|
||||||
|
|
||||||
|
service := NewService(config.Config{}, db, nil)
|
||||||
|
service.now = func() time.Time { return now }
|
||||||
|
resp, err := service.QueryEntryPopups(context.Background(), AuthUser{
|
||||||
|
UserID: 123,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, EntryQueryRequest{Platform: "ios", AppVersion: "3.12.0"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("QueryEntryPopups() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Queue) != 0 {
|
||||||
|
t.Fatalf("queue = %#v, want empty", resp.Queue)
|
||||||
|
}
|
||||||
|
assertReason(t, resp.Items["invite"], reasonDisabled)
|
||||||
|
assertReason(t, resp.Items["game"], reasonPlatform)
|
||||||
|
assertReason(t, resp.Items["future"], reasonNotStarted)
|
||||||
|
assertReason(t, resp.Items["expired"], reasonExpired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryEntryPopupsVersionBoundaries(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
mustCreatePopupConfig(t, db, model.AppPopupConfig{
|
||||||
|
ID: 1001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
PopupKey: "invite",
|
||||||
|
Scene: defaultScene,
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 100,
|
||||||
|
Version: 1,
|
||||||
|
MinAppVersion: "3.9.8",
|
||||||
|
MaxAppVersion: "3.13.0",
|
||||||
|
UserScopeType: "ALL",
|
||||||
|
})
|
||||||
|
|
||||||
|
service := NewService(config.Config{}, db, nil)
|
||||||
|
resp, err := service.QueryEntryPopups(context.Background(), AuthUser{
|
||||||
|
UserID: 123,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, EntryQueryRequest{AppVersion: "3.12.0"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("QueryEntryPopups() error = %v", err)
|
||||||
|
}
|
||||||
|
if !resp.Items["invite"].Value {
|
||||||
|
t.Fatalf("invite = %+v, want value true", resp.Items["invite"])
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = service.QueryEntryPopups(context.Background(), AuthUser{
|
||||||
|
UserID: 123,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, EntryQueryRequest{AppVersion: "3.8.9"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("QueryEntryPopups() error = %v", err)
|
||||||
|
}
|
||||||
|
assertReason(t, resp.Items["invite"], reasonMinVersion)
|
||||||
|
|
||||||
|
resp, err = service.QueryEntryPopups(context.Background(), AuthUser{
|
||||||
|
UserID: 123,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, EntryQueryRequest{AppVersion: "3.14.0"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("QueryEntryPopups() error = %v", err)
|
||||||
|
}
|
||||||
|
assertReason(t, resp.Items["invite"], reasonMaxVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryQueryResponseMarshalFlattensPopupKeys(t *testing.T) {
|
||||||
|
resp := EntryQueryResponse{
|
||||||
|
RequestID: "req-1",
|
||||||
|
Queue: []string{"invite"},
|
||||||
|
Items: map[string]PopupItem{
|
||||||
|
"invite": {Value: true, Limit: 7},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("MarshalJSON() error = %v", err)
|
||||||
|
}
|
||||||
|
var decoded map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||||
|
t.Fatalf("Unmarshal response error = %v", err)
|
||||||
|
}
|
||||||
|
if decoded["requestId"] != "req-1" {
|
||||||
|
t.Fatalf("requestId = %v, want req-1", decoded["requestId"])
|
||||||
|
}
|
||||||
|
invite, ok := decoded["invite"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("invite field missing in %s", raw)
|
||||||
|
}
|
||||||
|
if invite["value"] != true || invite["limit"] != float64(7) {
|
||||||
|
t.Fatalf("invite = %#v, want value true limit 7", invite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminConfigCRUD(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
service := NewService(config.Config{}, db, nil)
|
||||||
|
|
||||||
|
saved, err := service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Scene: defaultScene,
|
||||||
|
PopupKey: "invite",
|
||||||
|
Name: "邀请弹窗",
|
||||||
|
Enabled: true,
|
||||||
|
LimitDays: 7,
|
||||||
|
Priority: 100,
|
||||||
|
Version: 1,
|
||||||
|
Title: "Invite friends",
|
||||||
|
Image: "https://cdn.example.com/invite.png",
|
||||||
|
JumpType: "H5",
|
||||||
|
JumpURL: "/h5/app-invite/landing.html",
|
||||||
|
Description: "邀请活动首页弹窗",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveAdminConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if saved.ID == "" || saved.PopupKey != "invite" || saved.LimitDays != 7 || saved.Description != "邀请活动首页弹窗" {
|
||||||
|
t.Fatalf("saved = %+v", saved)
|
||||||
|
}
|
||||||
|
savedID, err := strconv.ParseInt(saved.ID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse saved id: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := service.PageAdminConfigs(context.Background(), "LIKEI", defaultScene, "inv", 1, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PageAdminConfigs() error = %v", err)
|
||||||
|
}
|
||||||
|
if page.Total != 1 || len(page.Records) != 1 {
|
||||||
|
t.Fatalf("page = %+v, want one record", page)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{
|
||||||
|
ID: flexibleInt64{value: savedID},
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Scene: defaultScene,
|
||||||
|
PopupKey: "invite",
|
||||||
|
Name: "邀请弹窗",
|
||||||
|
Enabled: false,
|
||||||
|
LimitDays: 0,
|
||||||
|
Priority: 90,
|
||||||
|
Version: 2,
|
||||||
|
Description: "每次进入都弹",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveAdminConfig(update) error = %v", err)
|
||||||
|
}
|
||||||
|
if updated.Enabled || updated.LimitDays != 0 || updated.Version != 2 {
|
||||||
|
t.Fatalf("updated = %+v, want disabled limit 0 version 2", updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.DeleteAdminConfig(context.Background(), savedID); err != nil {
|
||||||
|
t.Fatalf("DeleteAdminConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
page, err = service.PageAdminConfigs(context.Background(), "LIKEI", defaultScene, "", 1, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PageAdminConfigs(after delete) error = %v", err)
|
||||||
|
}
|
||||||
|
if page.Total != 0 {
|
||||||
|
t.Fatalf("total = %d, want 0", page.Total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminConfigRejectsDuplicateKey(t *testing.T) {
|
||||||
|
db := newTestDB(t)
|
||||||
|
service := NewService(config.Config{}, db, nil)
|
||||||
|
_, err := service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Scene: defaultScene,
|
||||||
|
PopupKey: "invite",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveAdminConfig(first) error = %v", err)
|
||||||
|
}
|
||||||
|
_, err = service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Scene: defaultScene,
|
||||||
|
PopupKey: "invite",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("SaveAdminConfig(duplicate) error = nil, want error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&model.AppPopupConfig{}); err != nil {
|
||||||
|
t.Fatalf("migrate app popup config: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustCreatePopupConfig(t *testing.T, db *gorm.DB, row model.AppPopupConfig) {
|
||||||
|
t.Helper()
|
||||||
|
if row.Scene == "" {
|
||||||
|
row.Scene = defaultScene
|
||||||
|
}
|
||||||
|
if row.SysOrigin == "" {
|
||||||
|
row.SysOrigin = defaultSysOrigin
|
||||||
|
}
|
||||||
|
if row.Version == 0 {
|
||||||
|
row.Version = 1
|
||||||
|
}
|
||||||
|
if row.UserScopeType == "" {
|
||||||
|
row.UserScopeType = "ALL"
|
||||||
|
}
|
||||||
|
if err := db.Create(&row).Error; err != nil {
|
||||||
|
t.Fatalf("create popup config %s: %v", row.PopupKey, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertReason(t *testing.T, item PopupItem, want string) {
|
||||||
|
t.Helper()
|
||||||
|
if item.Value {
|
||||||
|
t.Fatalf("item = %+v, want value false", item)
|
||||||
|
}
|
||||||
|
if item.Reason != want {
|
||||||
|
t.Fatalf("reason = %q, want %q", item.Reason, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
353
internal/service/apppopup/types.go
Normal file
353
internal/service/apppopup/types.go
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
package apppopup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/utils"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultScene = "APP_ENTER"
|
||||||
|
defaultSysOrigin = "LIKEI"
|
||||||
|
adminTimeLayout = "2006-01-02 15:04:05"
|
||||||
|
|
||||||
|
reasonDisabled = "DISABLED"
|
||||||
|
reasonNotStarted = "NOT_STARTED"
|
||||||
|
reasonExpired = "EXPIRED"
|
||||||
|
reasonPlatform = "PLATFORM_NOT_MATCH"
|
||||||
|
reasonMinVersion = "MIN_VERSION_NOT_MATCH"
|
||||||
|
reasonMaxVersion = "MAX_VERSION_NOT_MATCH"
|
||||||
|
reasonUserScope = "USER_SCOPE_NOT_MATCH"
|
||||||
|
reasonInvalidConfig = "INVALID_CONFIG"
|
||||||
|
configCacheExpiration = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppError = common.AppError
|
||||||
|
type AuthUser = common.AuthUser
|
||||||
|
|
||||||
|
var NewAppError = common.NewAppError
|
||||||
|
|
||||||
|
type dbHandle interface {
|
||||||
|
WithContext(ctx context.Context) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 负责 APP 进场弹窗配置查询。
|
||||||
|
type Service struct {
|
||||||
|
cfg config.Config
|
||||||
|
db dbHandle
|
||||||
|
cache redis.Cmdable
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService 创建 APP 进场弹窗服务。
|
||||||
|
func NewService(cfg config.Config, db dbHandle, cache redis.Cmdable) *Service {
|
||||||
|
return &Service{
|
||||||
|
cfg: cfg,
|
||||||
|
db: db,
|
||||||
|
cache: cache,
|
||||||
|
now: time.Now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryQueryRequest 是 App 查询进场弹窗的请求。
|
||||||
|
type EntryQueryRequest struct {
|
||||||
|
Scene string `json:"scene"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
AppVersion string `json:"appVersion"`
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryQueryResponse 是 App 查询进场弹窗的返回。
|
||||||
|
type EntryQueryResponse struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
Queue []string `json:"queue"`
|
||||||
|
Items map[string]PopupItem `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON 把弹窗 key 展平成 response 顶层字段,例如 invite/game。
|
||||||
|
func (r EntryQueryResponse) MarshalJSON() ([]byte, error) {
|
||||||
|
body := map[string]any{
|
||||||
|
"requestId": r.RequestID,
|
||||||
|
"queue": r.Queue,
|
||||||
|
}
|
||||||
|
for key, item := range r.Items {
|
||||||
|
if key == "" || key == "requestId" || key == "queue" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body[key] = item
|
||||||
|
}
|
||||||
|
return json.Marshal(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EntryQueryResponse) set(key string, item PopupItem) {
|
||||||
|
if r.Items == nil {
|
||||||
|
r.Items = make(map[string]PopupItem)
|
||||||
|
}
|
||||||
|
r.Items[key] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopupItem 是单个弹窗对 App 的返回结构。
|
||||||
|
type PopupItem struct {
|
||||||
|
Value bool `json:"value"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
PopupID int64 `json:"popupId,omitempty"`
|
||||||
|
Version int `json:"version,omitempty"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
JumpType string `json:"jumpType,omitempty"`
|
||||||
|
JumpURL string `json:"jumpUrl,omitempty"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Content map[string]any `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type popupContent struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Raw map[string]any `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeScene(scene string) string {
|
||||||
|
scene = strings.ToUpper(strings.TrimSpace(scene))
|
||||||
|
if scene == "" {
|
||||||
|
return defaultScene
|
||||||
|
}
|
||||||
|
return scene
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSysOrigin(sysOrigin string) string {
|
||||||
|
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
||||||
|
if sysOrigin == "" {
|
||||||
|
return defaultSysOrigin
|
||||||
|
}
|
||||||
|
return sysOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePopupKey(key string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePlatform(platform string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(platform))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequestID() string {
|
||||||
|
id, err := utils.NextID()
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Sprintf("%d", id)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePopupContent(raw string) popupContent {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return popupContent{}
|
||||||
|
}
|
||||||
|
var values map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(raw), &values); err != nil {
|
||||||
|
return popupContent{}
|
||||||
|
}
|
||||||
|
return popupContent{
|
||||||
|
Title: stringFromMap(values, "title"),
|
||||||
|
Image: firstStringFromMap(values, "image", "imageUrl", "cover"),
|
||||||
|
Raw: values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringFromMap(values map[string]any, key string) string {
|
||||||
|
if values == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
value, _ := values[key].(string)
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstStringFromMap(values map[string]any, keys ...string) string {
|
||||||
|
for _, key := range keys {
|
||||||
|
if value := stringFromMap(values, key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type cachedConfig struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
PopupKey string `json:"popupKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Scene string `json:"scene"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
LimitDays int `json:"limitDays"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
StartTime *time.Time `json:"startTime"`
|
||||||
|
EndTime *time.Time `json:"endTime"`
|
||||||
|
PlatformsJSON string `json:"platformsJson"`
|
||||||
|
MinAppVersion string `json:"minAppVersion"`
|
||||||
|
MaxAppVersion string `json:"maxAppVersion"`
|
||||||
|
UserScopeType string `json:"userScopeType"`
|
||||||
|
UserScopeJSON string `json:"userScopeJson"`
|
||||||
|
ContentJSON string `json:"contentJson"`
|
||||||
|
JumpType string `json:"jumpType"`
|
||||||
|
JumpURL string `json:"jumpUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminConfigPageResponse 是后台首页弹窗配置分页响应。
|
||||||
|
type AdminConfigPageResponse struct {
|
||||||
|
Records []AdminConfigItem `json:"records"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Current int `json:"current"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminConfigItem 是后台首页弹窗配置项。
|
||||||
|
type AdminConfigItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Scene string `json:"scene"`
|
||||||
|
PopupKey string `json:"popupKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
LimitDays int `json:"limitDays"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
JumpType string `json:"jumpType"`
|
||||||
|
JumpURL string `json:"jumpUrl"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StartTime string `json:"startTime,omitempty"`
|
||||||
|
EndTime string `json:"endTime,omitempty"`
|
||||||
|
PlatformsJSON string `json:"platformsJson,omitempty"`
|
||||||
|
MinAppVersion string `json:"minAppVersion,omitempty"`
|
||||||
|
MaxAppVersion string `json:"maxAppVersion,omitempty"`
|
||||||
|
CreateTime string `json:"createTime,omitempty"`
|
||||||
|
UpdateTime string `json:"updateTime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAdminConfigRequest 是后台保存首页弹窗配置请求。
|
||||||
|
type SaveAdminConfigRequest struct {
|
||||||
|
ID flexibleInt64 `json:"id"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Scene string `json:"scene"`
|
||||||
|
PopupKey string `json:"popupKey"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
LimitDays int `json:"limitDays"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
JumpType string `json:"jumpType"`
|
||||||
|
JumpURL string `json:"jumpUrl"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
PlatformsJSON string `json:"platformsJson"`
|
||||||
|
MinAppVersion string `json:"minAppVersion"`
|
||||||
|
MaxAppVersion string `json:"maxAppVersion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type flexibleInt64 struct {
|
||||||
|
value int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *flexibleInt64) UnmarshalJSON(data []byte) error {
|
||||||
|
var raw any
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch value := raw.(type) {
|
||||||
|
case float64:
|
||||||
|
v.value = int64(value)
|
||||||
|
case string:
|
||||||
|
parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
v.value = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v.value = parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v flexibleInt64) Int64() int64 {
|
||||||
|
return v.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCachedConfig(row model.AppPopupConfig) cachedConfig {
|
||||||
|
return cachedConfig{
|
||||||
|
ID: row.ID,
|
||||||
|
SysOrigin: row.SysOrigin,
|
||||||
|
PopupKey: row.PopupKey,
|
||||||
|
Name: row.Name,
|
||||||
|
Scene: row.Scene,
|
||||||
|
Enabled: row.Enabled,
|
||||||
|
LimitDays: row.LimitDays,
|
||||||
|
Priority: row.Priority,
|
||||||
|
Version: row.Version,
|
||||||
|
StartTime: row.StartTime,
|
||||||
|
EndTime: row.EndTime,
|
||||||
|
PlatformsJSON: row.PlatformsJSON,
|
||||||
|
MinAppVersion: row.MinAppVersion,
|
||||||
|
MaxAppVersion: row.MaxAppVersion,
|
||||||
|
UserScopeType: row.UserScopeType,
|
||||||
|
UserScopeJSON: row.UserScopeJSON,
|
||||||
|
ContentJSON: row.ContentJSON,
|
||||||
|
JumpType: row.JumpType,
|
||||||
|
JumpURL: row.JumpURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminItemFromModel(row model.AppPopupConfig) AdminConfigItem {
|
||||||
|
content := parsePopupContent(row.ContentJSON)
|
||||||
|
return AdminConfigItem{
|
||||||
|
ID: strconv.FormatInt(row.ID, 10),
|
||||||
|
SysOrigin: row.SysOrigin,
|
||||||
|
Scene: row.Scene,
|
||||||
|
PopupKey: row.PopupKey,
|
||||||
|
Name: row.Name,
|
||||||
|
Enabled: row.Enabled,
|
||||||
|
LimitDays: row.LimitDays,
|
||||||
|
Priority: row.Priority,
|
||||||
|
Version: normalizeVersion(row.Version),
|
||||||
|
Title: content.Title,
|
||||||
|
Image: content.Image,
|
||||||
|
JumpType: row.JumpType,
|
||||||
|
JumpURL: row.JumpURL,
|
||||||
|
Description: row.Remark,
|
||||||
|
StartTime: formatAdminTimePtr(row.StartTime),
|
||||||
|
EndTime: formatAdminTimePtr(row.EndTime),
|
||||||
|
PlatformsJSON: row.PlatformsJSON,
|
||||||
|
MinAppVersion: row.MinAppVersion,
|
||||||
|
MaxAppVersion: row.MaxAppVersion,
|
||||||
|
CreateTime: formatAdminTime(row.CreateTime),
|
||||||
|
UpdateTime: formatAdminTime(row.UpdateTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAdminTime(t time.Time) string {
|
||||||
|
if t.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format(adminTimeLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAdminTimePtr(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return formatAdminTime(*t)
|
||||||
|
}
|
||||||
@ -14,10 +14,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const adminTimeLayout = "2006-01-02 15:04:05"
|
const adminTimeLayout = "2006-01-02 15:04:05"
|
||||||
|
const baishunRoomCategory = "CHAT_ROOM"
|
||||||
|
|
||||||
type adminGameRow struct {
|
type adminGameRow struct {
|
||||||
ID int64
|
ID int64
|
||||||
SysOrigin string
|
SysOrigin string
|
||||||
|
Profile string
|
||||||
InternalGameID string
|
InternalGameID string
|
||||||
Name string
|
Name string
|
||||||
Category string
|
Category string
|
||||||
@ -57,16 +59,21 @@ type adminGameRow struct {
|
|||||||
func (s *BaishunService) PageAdminGames(
|
func (s *BaishunService) PageAdminGames(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sysOrigin string,
|
sysOrigin string,
|
||||||
|
profile string,
|
||||||
keyword string,
|
keyword string,
|
||||||
showcase *bool,
|
showcase *bool,
|
||||||
cursor int,
|
cursor int,
|
||||||
limit int,
|
limit int,
|
||||||
) (*AdminBaishunGamePageResponse, error) {
|
) (*AdminBaishunGamePageResponse, error) {
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cursor = normalizePageCursor(cursor)
|
cursor = normalizePageCursor(cursor)
|
||||||
limit = normalizePageLimit(limit)
|
limit = normalizePageLimit(limit)
|
||||||
|
|
||||||
countQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase)
|
countQuery := s.buildAdminGameQuery(ctx, sysOrigin, profile, keyword, showcase)
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := countQuery.Count(&total).Error; err != nil {
|
if err := countQuery.Count(&total).Error; err != nil {
|
||||||
@ -74,11 +81,12 @@ func (s *BaishunService) PageAdminGames(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rows []adminGameRow
|
var rows []adminGameRow
|
||||||
listQuery := s.buildAdminGameQuery(ctx, sysOrigin, keyword, showcase)
|
listQuery := s.buildAdminGameQuery(ctx, sysOrigin, profile, keyword, showcase)
|
||||||
if err := listQuery.
|
if err := listQuery.
|
||||||
Select(`
|
Select(`
|
||||||
cfg.id AS id,
|
cfg.id AS id,
|
||||||
cfg.sys_origin AS sys_origin,
|
cfg.sys_origin AS sys_origin,
|
||||||
|
ext.profile AS profile,
|
||||||
cfg.game_id AS internal_game_id,
|
cfg.game_id AS internal_game_id,
|
||||||
cfg.name AS name,
|
cfg.name AS name,
|
||||||
cfg.category AS category,
|
cfg.category AS category,
|
||||||
@ -138,12 +146,16 @@ func (s *BaishunService) PageAdminGames(
|
|||||||
// SaveAdminGame 保存后台百顺游戏配置。
|
// SaveAdminGame 保存后台百顺游戏配置。
|
||||||
func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) {
|
func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishunGameRequest) (*AdminBaishunGameItem, error) {
|
||||||
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
||||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
profile, err := s.requestProfile(ctx, sysOrigin, req.Profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
catalog, err := s.findCatalogForAdmin(ctx, sysOrigin, req.VendorGameID, req.GameID)
|
catalog, err := s.findCatalogForAdmin(ctx, sysOrigin, profile, req.VendorGameID, req.GameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -166,7 +178,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
return nil, NewAppError(http.StatusBadRequest, "name_required", "name is required")
|
return nil, NewAppError(http.StatusBadRequest, "name_required", "name is required")
|
||||||
}
|
}
|
||||||
category := defaultIfBlank(strings.TrimSpace(req.Category), "OTHER")
|
category := defaultIfBlank(strings.TrimSpace(req.Category), baishunRoomCategory)
|
||||||
cover := defaultIfBlank(strings.TrimSpace(req.Cover), defaultIfBlank(catalog.Cover, catalog.PreviewURL))
|
cover := defaultIfBlank(strings.TrimSpace(req.Cover), defaultIfBlank(catalog.Cover, catalog.PreviewURL))
|
||||||
clientOrigin := defaultIfBlank(strings.TrimSpace(req.ClientOrigin), "COMMON")
|
clientOrigin := defaultIfBlank(strings.TrimSpace(req.ClientOrigin), "COMMON")
|
||||||
gameCode := defaultIfBlank(strings.TrimSpace(req.GameCode), strconv.Itoa(req.VendorGameID))
|
gameCode := defaultIfBlank(strings.TrimSpace(req.GameCode), strconv.Itoa(req.VendorGameID))
|
||||||
@ -197,11 +209,12 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cfg, err := s.findAdminConfigForSave(tx, sysOrigin, req.ID, gameID, req.VendorGameID)
|
cfg, err := s.findAdminConfigForSave(tx, sysOrigin, profile, req.ID, gameID, req.VendorGameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
showcase := resolveAdminShowcase(req.Showcase, cfg.Showcase, cfg.ID == 0)
|
||||||
|
|
||||||
cfgValues := map[string]any{
|
cfgValues := map[string]any{
|
||||||
"sys_origin": sysOrigin,
|
"sys_origin": sysOrigin,
|
||||||
@ -212,7 +225,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
"game_code": gameCode,
|
"game_code": gameCode,
|
||||||
"cover": cover,
|
"cover": cover,
|
||||||
"amounts": strings.TrimSpace(req.Amounts),
|
"amounts": strings.TrimSpace(req.Amounts),
|
||||||
"is_showcase": req.Showcase,
|
"is_showcase": showcase,
|
||||||
"sort": req.Sort,
|
"sort": req.Sort,
|
||||||
"height": req.Height,
|
"height": req.Height,
|
||||||
"width": req.Width,
|
"width": req.Width,
|
||||||
@ -238,7 +251,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
GameCode: gameCode,
|
GameCode: gameCode,
|
||||||
Cover: cover,
|
Cover: cover,
|
||||||
Amounts: strings.TrimSpace(req.Amounts),
|
Amounts: strings.TrimSpace(req.Amounts),
|
||||||
Showcase: req.Showcase,
|
Showcase: showcase,
|
||||||
Sort: req.Sort,
|
Sort: req.Sort,
|
||||||
Height: req.Height,
|
Height: req.Height,
|
||||||
Width: req.Width,
|
Width: req.Width,
|
||||||
@ -261,7 +274,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ext model.SysGameListVendorExt
|
var ext model.SysGameListVendorExt
|
||||||
err = tx.Where("game_list_config_id = ? AND vendor_type = ?", cfg.ID, baishunVendorType).First(&ext).Error
|
err = tx.Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", cfg.ID, baishunVendorType, profile).First(&ext).Error
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -279,6 +292,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
ID: nextID,
|
ID: nextID,
|
||||||
GameListConfigID: cfg.ID,
|
GameListConfigID: cfg.ID,
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
|
Profile: profile,
|
||||||
VendorType: baishunVendorType,
|
VendorType: baishunVendorType,
|
||||||
VendorGameID: strconv.Itoa(req.VendorGameID),
|
VendorGameID: strconv.Itoa(req.VendorGameID),
|
||||||
LaunchMode: launchMode,
|
LaunchMode: launchMode,
|
||||||
@ -303,6 +317,7 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
Where("id = ?", ext.ID).
|
Where("id = ?", ext.ID).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"sys_origin": sysOrigin,
|
"sys_origin": sysOrigin,
|
||||||
|
"profile": profile,
|
||||||
"vendor_game_id": strconv.Itoa(req.VendorGameID),
|
"vendor_game_id": strconv.Itoa(req.VendorGameID),
|
||||||
"launch_mode": launchMode,
|
"launch_mode": launchMode,
|
||||||
"package_version": packageVersion,
|
"package_version": packageVersion,
|
||||||
@ -325,40 +340,63 @@ func (s *BaishunService) SaveAdminGame(ctx context.Context, req SaveAdminBaishun
|
|||||||
if err := tx.Commit().Error; err != nil {
|
if err := tx.Commit().Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.getAdminGame(ctx, sysOrigin, cfg.ID)
|
s.clearJavaGameListCache(ctx)
|
||||||
|
return s.getAdminGame(ctx, sysOrigin, profile, cfg.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAdminGame 删除后台百顺游戏配置,不影响目录表。
|
// DeleteAdminGame 删除后台百顺游戏配置,不影响目录表。
|
||||||
func (s *BaishunService) DeleteAdminGame(ctx context.Context, sysOrigin string, id int64) error {
|
func (s *BaishunService) DeleteAdminGame(ctx context.Context, sysOrigin string, profile string, id int64) error {
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
return NewAppError(http.StatusBadRequest, "id_required", "id is required")
|
return NewAppError(http.StatusBadRequest, "id_required", "id is required")
|
||||||
}
|
}
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
tx := s.repo.DB.WithContext(ctx).Begin()
|
tx := s.repo.DB.WithContext(ctx).Begin()
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
return tx.Error
|
return tx.Error
|
||||||
}
|
}
|
||||||
if err := tx.Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType).
|
if err := tx.Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", id, baishunVendorType, profile).
|
||||||
Delete(&model.SysGameListVendorExt{}).Error; err != nil {
|
Delete(&model.SysGameListVendorExt{}).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var remaining int64
|
||||||
|
if err := tx.Model(&model.SysGameListVendorExt{}).
|
||||||
|
Where("game_list_config_id = ? AND vendor_type = ?", id, baishunVendorType).
|
||||||
|
Count(&remaining).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if remaining == 0 {
|
||||||
if err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).
|
if err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).
|
||||||
Delete(&model.SysGameListConfig{}).Error; err != nil {
|
Delete(&model.SysGameListConfig{}).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.Commit().Error
|
}
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.clearJavaGameListCache(ctx)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportCatalogGames 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。
|
// ImportCatalogGames 把本地目录中尚未进入后台列表的百顺游戏补齐成可配置记录。
|
||||||
func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, vendorGameIDs []int) (*ImportCatalogResponse, error) {
|
func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin string, profile string, vendorGameIDs []int, defaultShowcase ...bool) (*ImportCatalogResponse, error) {
|
||||||
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
tx := s.repo.DB.WithContext(ctx).Begin()
|
tx := s.repo.DB.WithContext(ctx).Begin()
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
return nil, tx.Error
|
return nil, tx.Error
|
||||||
}
|
}
|
||||||
resp, err := s.importCatalogGamesTx(tx, normalizeAdminSysOrigin(sysOrigin), buildVendorGameIDFilter(vendorGameIDs))
|
resp, err := s.importCatalogGamesTx(tx, sysOrigin, profile, buildVendorGameIDFilter(vendorGameIDs), resolveImportDefaultShowcase(defaultShowcase...))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -366,6 +404,7 @@ func (s *BaishunService) ImportCatalogGames(ctx context.Context, sysOrigin strin
|
|||||||
if err := tx.Commit().Error; err != nil {
|
if err := tx.Commit().Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
s.clearJavaGameListCache(ctx)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,7 +414,7 @@ func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.VendorGameIDs)
|
importResp, err := s.ImportCatalogGames(ctx, req.SysOrigin, req.Profile, req.VendorGameIDs, resolveBoolPtrDefault(req.DefaultShowcase, true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -388,13 +427,14 @@ func (s *BaishunService) SyncAdminGames(ctx context.Context, req SyncCatalogRequ
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, keyword string, showcase *bool) *gorm.DB {
|
func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, profile, keyword string, showcase *bool) *gorm.DB {
|
||||||
query := s.repo.DB.WithContext(ctx).
|
query := s.repo.DB.WithContext(ctx).
|
||||||
Table("sys_game_list_config AS cfg").
|
Table("sys_game_list_config AS cfg").
|
||||||
Joins("LEFT JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.vendor_type = ?", baishunVendorType).
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.vendor_type = ? AND ext.profile = ?", baishunVendorType, profile).
|
||||||
Joins(`
|
Joins(`
|
||||||
LEFT JOIN baishun_game_catalog cat
|
LEFT JOIN baishun_game_catalog cat
|
||||||
ON cat.sys_origin = cfg.sys_origin
|
ON cat.sys_origin = cfg.sys_origin
|
||||||
|
AND cat.profile = ext.profile
|
||||||
AND (
|
AND (
|
||||||
CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id
|
CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id
|
||||||
OR cat.internal_game_id = cfg.game_id
|
OR cat.internal_game_id = cfg.game_id
|
||||||
@ -417,13 +457,14 @@ func (s *BaishunService) buildAdminGameQuery(ctx context.Context, sysOrigin, key
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, id int64) (*AdminBaishunGameItem, error) {
|
func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, profile string, id int64) (*AdminBaishunGameItem, error) {
|
||||||
var rows []adminGameRow
|
var rows []adminGameRow
|
||||||
err := s.buildAdminGameQuery(ctx, sysOrigin, "", nil).
|
err := s.buildAdminGameQuery(ctx, sysOrigin, profile, "", nil).
|
||||||
Where("cfg.id = ?", id).
|
Where("cfg.id = ?", id).
|
||||||
Select(`
|
Select(`
|
||||||
cfg.id AS id,
|
cfg.id AS id,
|
||||||
cfg.sys_origin AS sys_origin,
|
cfg.sys_origin AS sys_origin,
|
||||||
|
ext.profile AS profile,
|
||||||
cfg.game_id AS internal_game_id,
|
cfg.game_id AS internal_game_id,
|
||||||
cfg.name AS name,
|
cfg.name AS name,
|
||||||
cfg.category AS category,
|
cfg.category AS category,
|
||||||
@ -470,8 +511,8 @@ func (s *BaishunService) getAdminGame(ctx context.Context, sysOrigin string, id
|
|||||||
return nil, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
return nil, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin string, vendorGameID int, gameID string) (model.BaishunGameCatalog, error) {
|
func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin string, profile string, vendorGameID int, gameID string) (model.BaishunGameCatalog, error) {
|
||||||
query := s.repo.DB.WithContext(ctx).Where("sys_origin = ?", sysOrigin)
|
query := s.repo.DB.WithContext(ctx).Where("sys_origin = ? AND profile = ?", sysOrigin, profile)
|
||||||
switch {
|
switch {
|
||||||
case vendorGameID > 0:
|
case vendorGameID > 0:
|
||||||
query = query.Where("vendor_game_id = ?", vendorGameID)
|
query = query.Where("vendor_game_id = ?", vendorGameID)
|
||||||
@ -491,19 +532,31 @@ func (s *BaishunService) findCatalogForAdmin(ctx context.Context, sysOrigin stri
|
|||||||
return catalog, nil
|
return catalog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, id int64, gameID string, vendorGameID int) (model.SysGameListConfig, error) {
|
func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, profile string, id int64, gameID string, vendorGameID int) (model.SysGameListConfig, error) {
|
||||||
var cfg model.SysGameListConfig
|
var cfg model.SysGameListConfig
|
||||||
if id > 0 {
|
if id > 0 {
|
||||||
err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).First(&cfg).Error
|
err := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", id, sysOrigin, baishunVendorType).First(&cfg).Error
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
|
var extCount int64
|
||||||
|
err = tx.Model(&model.SysGameListVendorExt{}).
|
||||||
|
Where("game_list_config_id = ? AND vendor_type = ? AND profile = ?", cfg.ID, baishunVendorType, profile).
|
||||||
|
Count(&extCount).Error
|
||||||
|
if err != nil {
|
||||||
|
return model.SysGameListConfig{}, err
|
||||||
|
}
|
||||||
|
if extCount == 0 {
|
||||||
|
return model.SysGameListConfig{}, NewAppError(http.StatusNotFound, "game_not_found", "admin baishun game not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if vendorGameID > 0 {
|
if vendorGameID > 0 {
|
||||||
var ext model.SysGameListVendorExt
|
var ext model.SysGameListVendorExt
|
||||||
err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, strconv.Itoa(vendorGameID)).First(&ext).Error
|
err := tx.Where("sys_origin = ? AND profile = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, profile, baishunVendorType, strconv.Itoa(vendorGameID)).First(&ext).Error
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
return model.SysGameListConfig{}, err
|
return model.SysGameListConfig{}, err
|
||||||
}
|
}
|
||||||
@ -521,23 +574,31 @@ func (s *BaishunService) findAdminConfigForSave(tx *gorm.DB, sysOrigin string, i
|
|||||||
if strings.TrimSpace(gameID) == "" {
|
if strings.TrimSpace(gameID) == "" {
|
||||||
return model.SysGameListConfig{}, nil
|
return model.SysGameListConfig{}, nil
|
||||||
}
|
}
|
||||||
err := tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, strings.TrimSpace(gameID)).First(&cfg).Error
|
err := tx.Table("sys_game_list_config AS cfg").
|
||||||
if err == gorm.ErrRecordNotFound {
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.vendor_type = ? AND ext.profile = ?", baishunVendorType, profile).
|
||||||
|
Where("cfg.sys_origin = ? AND cfg.game_origin = ? AND cfg.game_id = ?", sysOrigin, baishunVendorType, strings.TrimSpace(gameID)).
|
||||||
|
Select("cfg.*").
|
||||||
|
Limit(1).
|
||||||
|
Scan(&cfg).Error
|
||||||
|
if err != nil {
|
||||||
|
return model.SysGameListConfig{}, err
|
||||||
|
}
|
||||||
|
if cfg.ID == 0 {
|
||||||
return model.SysGameListConfig{}, nil
|
return model.SysGameListConfig{}, nil
|
||||||
}
|
}
|
||||||
return cfg, err
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, filter map[int]struct{}) (*ImportCatalogResponse, error) {
|
func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, profile string, filter map[int]struct{}, defaultShowcase bool) (*ImportCatalogResponse, error) {
|
||||||
ctx := tx.Statement.Context
|
ctx := tx.Statement.Context
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
query := tx.Where("sys_origin = ? AND status = ?", sysOrigin, baishunCatalogEnabled)
|
query := tx.Where("sys_origin = ? AND profile = ? AND status = ?", sysOrigin, profile, baishunCatalogEnabled)
|
||||||
if len(filter) > 0 {
|
if len(filter) > 0 {
|
||||||
ids := make([]int, 0, len(filter))
|
ids := make([]int, 0, len(filter))
|
||||||
for id := range filter {
|
for id := range filter {
|
||||||
@ -557,35 +618,36 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
var ext model.SysGameListVendorExt
|
var ext model.SysGameListVendorExt
|
||||||
err := tx.Where("sys_origin = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, baishunVendorType, vendorGameID).First(&ext).Error
|
err := tx.Where("sys_origin = ? AND profile = ? AND vendor_type = ? AND vendor_game_id = ?", sysOrigin, profile, baishunVendorType, vendorGameID).First(&ext).Error
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err == nil && ext.GameListConfigID > 0 {
|
if err == nil && ext.GameListConfigID > 0 {
|
||||||
|
var cfg model.SysGameListConfig
|
||||||
|
cfgErr := tx.Where("id = ? AND sys_origin = ? AND game_origin = ?", ext.GameListConfigID, sysOrigin, baishunVendorType).First(&cfg).Error
|
||||||
|
if cfgErr == nil {
|
||||||
resp.Existing++
|
resp.Existing++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if cfgErr != gorm.ErrRecordNotFound {
|
||||||
var cfg model.SysGameListConfig
|
return nil, cfgErr
|
||||||
err = tx.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", sysOrigin, baishunVendorType, catalog.InternalGameID).First(&cfg).Error
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if err == gorm.ErrRecordNotFound {
|
}
|
||||||
nextID, idErr := utils.NextID()
|
|
||||||
|
nextCfgID, idErr := utils.NextID()
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return nil, idErr
|
return nil, idErr
|
||||||
}
|
}
|
||||||
cfg = model.SysGameListConfig{
|
cfg := model.SysGameListConfig{
|
||||||
ID: nextID,
|
ID: nextCfgID,
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
GameOrigin: baishunVendorType,
|
GameOrigin: baishunVendorType,
|
||||||
GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
GameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
||||||
Name: catalog.Name,
|
Name: catalog.Name,
|
||||||
Category: "OTHER",
|
Category: baishunRoomCategory,
|
||||||
GameCode: vendorGameID,
|
GameCode: vendorGameID,
|
||||||
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
Cover: defaultIfBlank(catalog.Cover, catalog.PreviewURL),
|
||||||
Showcase: false,
|
Showcase: defaultShowcase,
|
||||||
Sort: int64(catalog.VendorGameID),
|
Sort: int64(catalog.VendorGameID),
|
||||||
FullScreen: true,
|
FullScreen: true,
|
||||||
ClientOrigin: "COMMON",
|
ClientOrigin: "COMMON",
|
||||||
@ -594,16 +656,42 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
|
|||||||
if err := tx.Create(&cfg).Error; err != nil {
|
if err := tx.Create(&cfg).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
nextID, idErr := utils.NextID()
|
nextID, idErr := utils.NextID()
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return nil, idErr
|
return nil, idErr
|
||||||
}
|
}
|
||||||
|
extraJSON := updateExtExtraJSONWithGameType(ext.ExtraJSON, nil, baishunVendorType, ext.ID != 0)
|
||||||
|
extValues := map[string]any{
|
||||||
|
"game_list_config_id": cfg.ID,
|
||||||
|
"sys_origin": sysOrigin,
|
||||||
|
"profile": profile,
|
||||||
|
"vendor_type": baishunVendorType,
|
||||||
|
"vendor_game_id": vendorGameID,
|
||||||
|
"launch_mode": baishunLaunchModeRemote,
|
||||||
|
"package_version": catalog.PackageVersion,
|
||||||
|
"package_url": catalog.DownloadURL,
|
||||||
|
"preview_url": catalog.PreviewURL,
|
||||||
|
"orientation": catalog.Orientation,
|
||||||
|
"safe_height": catalog.SafeHeight,
|
||||||
|
"gsp": defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()),
|
||||||
|
"bridge_schema_version": "1.0",
|
||||||
|
"extra_json": extraJSON,
|
||||||
|
"enabled": true,
|
||||||
|
"update_time": now,
|
||||||
|
}
|
||||||
|
if ext.ID > 0 {
|
||||||
|
if err := tx.Model(&model.SysGameListVendorExt{}).
|
||||||
|
Where("id = ?", ext.ID).
|
||||||
|
Updates(extValues).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
newExt := model.SysGameListVendorExt{
|
newExt := model.SysGameListVendorExt{
|
||||||
ID: nextID,
|
ID: nextID,
|
||||||
GameListConfigID: cfg.ID,
|
GameListConfigID: cfg.ID,
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
|
Profile: profile,
|
||||||
VendorType: baishunVendorType,
|
VendorType: baishunVendorType,
|
||||||
VendorGameID: vendorGameID,
|
VendorGameID: vendorGameID,
|
||||||
LaunchMode: baishunLaunchModeRemote,
|
LaunchMode: baishunLaunchModeRemote,
|
||||||
@ -614,7 +702,7 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
|
|||||||
SafeHeight: catalog.SafeHeight,
|
SafeHeight: catalog.SafeHeight,
|
||||||
GSP: defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()),
|
GSP: defaultAdminGSP(catalog.GSP, runtimeCfg.gsp()),
|
||||||
BridgeSchemaVersion: "1.0",
|
BridgeSchemaVersion: "1.0",
|
||||||
ExtraJSON: updateExtExtraJSONWithGameType("", nil, baishunVendorType, false),
|
ExtraJSON: extraJSON,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
CreateTime: now,
|
CreateTime: now,
|
||||||
UpdateTime: now,
|
UpdateTime: now,
|
||||||
@ -622,15 +710,31 @@ func (s *BaishunService) importCatalogGamesTx(tx *gorm.DB, sysOrigin string, fil
|
|||||||
if err := tx.Create(&newExt).Error; err != nil {
|
if err := tx.Create(&newExt).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
resp.Imported++
|
resp.Imported++
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveImportDefaultShowcase(values ...bool) bool {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return values[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveBoolPtrDefault(value *bool, fallback bool) bool {
|
||||||
|
if value == nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
||||||
func (r adminGameRow) toAdminItem() AdminBaishunGameItem {
|
func (r adminGameRow) toAdminItem() AdminBaishunGameItem {
|
||||||
return AdminBaishunGameItem{
|
return AdminBaishunGameItem{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
SysOrigin: r.SysOrigin,
|
SysOrigin: r.SysOrigin,
|
||||||
|
Profile: normalizeBaishunProfile(r.Profile),
|
||||||
GameID: r.InternalGameID,
|
GameID: r.InternalGameID,
|
||||||
GameType: gameTypeFromExtraJSON(r.ExtExtraJSON, baishunVendorType),
|
GameType: gameTypeFromExtraJSON(r.ExtExtraJSON, baishunVendorType),
|
||||||
VendorGameID: parseAdminVendorGameID(r.VendorGameID, r.InternalGameID),
|
VendorGameID: parseAdminVendorGameID(r.VendorGameID, r.InternalGameID),
|
||||||
@ -687,6 +791,16 @@ func normalizePageLimit(limit int) int {
|
|||||||
return limit
|
return limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveAdminShowcase(showcase *bool, current bool, isCreate bool) bool {
|
||||||
|
if showcase != nil {
|
||||||
|
return *showcase
|
||||||
|
}
|
||||||
|
if isCreate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
func defaultAdminGSP(catalogGSP string, fallback int) string {
|
func defaultAdminGSP(catalogGSP string, fallback int) string {
|
||||||
if strings.TrimSpace(catalogGSP) != "" {
|
if strings.TrimSpace(catalogGSP) != "" {
|
||||||
return strings.TrimSpace(catalogGSP)
|
return strings.TrimSpace(catalogGSP)
|
||||||
|
|||||||
313
internal/service/baishun/admin_test.go
Normal file
313
internal/service/baishun/admin_test.go
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
package baishun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveAdminShowcase(t *testing.T) {
|
||||||
|
t.Run("create defaults to enabled", func(t *testing.T) {
|
||||||
|
if got := resolveAdminShowcase(nil, false, true); !got {
|
||||||
|
t.Fatalf("resolveAdminShowcase(nil, false, true) = %v, want true", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update keeps current when omitted", func(t *testing.T) {
|
||||||
|
if got := resolveAdminShowcase(nil, false, false); got {
|
||||||
|
t.Fatalf("resolveAdminShowcase(nil, false, false) = %v, want false", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit false remains false", func(t *testing.T) {
|
||||||
|
showcase := false
|
||||||
|
if got := resolveAdminShowcase(&showcase, true, true); got {
|
||||||
|
t.Fatalf("resolveAdminShowcase(false, true, true) = %v, want false", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchPlatformGamesUsesConfiguredGameListURL(t *testing.T) {
|
||||||
|
service, _ := newTestBaishunService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var gotPath string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"code":0,"message":"ok","data":[{"game_id":1021,"name":"LuckyGift","download_url":"https://cdn.example.com/lucky/index.html","preview_url":"https://cdn.example.com/lucky.png","game_version":"1.0.0","game_mode":[1],"game_orientation":1,"safe_height":0,"venue_level":[1]}]}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
service.httpClient = server.Client()
|
||||||
|
|
||||||
|
games, err := service.fetchPlatformGames(ctx, baishunRuntimeConfig{
|
||||||
|
PlatformBaseURL: "https://unused.example.com",
|
||||||
|
GameListURL: server.URL + "/custom/game-list",
|
||||||
|
AppID: 1001,
|
||||||
|
AppChannel: "test",
|
||||||
|
AppKey: "test-key",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fetchPlatformGames() error = %v", err)
|
||||||
|
}
|
||||||
|
if gotPath != "/custom/game-list" {
|
||||||
|
t.Fatalf("request path = %q, want configured game list path", gotPath)
|
||||||
|
}
|
||||||
|
if len(games) != 1 || games[0].GameID != 1021 {
|
||||||
|
t.Fatalf("games = %+v, want fetched game 1021", games)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportAndListRoomGamesUseActiveProfile(t *testing.T) {
|
||||||
|
service, db := newTestBaishunService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if err := db.Create(&[]model.BaishunProviderConfig{
|
||||||
|
{
|
||||||
|
ID: 1001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileProd,
|
||||||
|
Active: true,
|
||||||
|
PlatformBaseURL: "https://prod.example.com",
|
||||||
|
AppID: 111,
|
||||||
|
AppChannel: "prod",
|
||||||
|
AppKey: "prod-key",
|
||||||
|
GSP: 101,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 1002,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileTest,
|
||||||
|
Active: false,
|
||||||
|
PlatformBaseURL: "https://test.example.com",
|
||||||
|
AppID: 222,
|
||||||
|
AppChannel: "test",
|
||||||
|
AppKey: "test-key",
|
||||||
|
GSP: 102,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
},
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create provider configs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&[]model.BaishunGameCatalog{
|
||||||
|
{
|
||||||
|
ID: 2001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileProd,
|
||||||
|
InternalGameID: "bs_9001",
|
||||||
|
VendorGameID: 9001,
|
||||||
|
Name: "Prod Game",
|
||||||
|
Cover: "https://cdn.example.com/prod.png",
|
||||||
|
DownloadURL: "https://cdn.example.com/prod/index.html",
|
||||||
|
Status: baishunCatalogEnabled,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2002,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileTest,
|
||||||
|
InternalGameID: "bs_9001",
|
||||||
|
VendorGameID: 9001,
|
||||||
|
Name: "Test Game",
|
||||||
|
Cover: "https://cdn.example.com/test.png",
|
||||||
|
DownloadURL: "https://cdn.example.com/test/index.html",
|
||||||
|
Status: baishunCatalogEnabled,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
},
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create catalogs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileProd, []int{9001}); err != nil {
|
||||||
|
t.Fatalf("import prod catalog: %v", err)
|
||||||
|
} else if resp.Imported != 1 {
|
||||||
|
t.Fatalf("prod imported = %d, want 1", resp.Imported)
|
||||||
|
}
|
||||||
|
var prodConfig model.SysGameListConfig
|
||||||
|
if err := db.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", "LIKEI", baishunVendorType, "bs_9001").First(&prodConfig).Error; err != nil {
|
||||||
|
t.Fatalf("load imported prod config: %v", err)
|
||||||
|
}
|
||||||
|
if prodConfig.Category != baishunRoomCategory {
|
||||||
|
t.Fatalf("imported prod category = %q, want %q", prodConfig.Category, baishunRoomCategory)
|
||||||
|
}
|
||||||
|
if !prodConfig.Showcase {
|
||||||
|
t.Fatalf("imported prod showcase = false, want true by default")
|
||||||
|
}
|
||||||
|
if resp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileTest, []int{9001}); err != nil {
|
||||||
|
t.Fatalf("import test catalog: %v", err)
|
||||||
|
} else if resp.Imported != 1 {
|
||||||
|
t.Fatalf("test imported = %d, want 1", resp.Imported)
|
||||||
|
}
|
||||||
|
|
||||||
|
prodList, err := service.ListRoomGames(ctx, AuthUser{SysOrigin: "LIKEI"}, "room-1", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list prod games: %v", err)
|
||||||
|
}
|
||||||
|
if len(prodList.Items) != 1 || prodList.Items[0].Name != "Prod Game" {
|
||||||
|
t.Fatalf("prod list = %+v, want only Prod Game", prodList.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := service.ActivateProviderProfile(ctx, ActivateBaishunProviderProfileRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileTest,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("activate test profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testList, err := service.ListRoomGames(ctx, AuthUser{SysOrigin: "LIKEI"}, "room-1", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list test games: %v", err)
|
||||||
|
}
|
||||||
|
if len(testList.Items) != 1 || testList.Items[0].Name != "Test Game" {
|
||||||
|
t.Fatalf("test list = %+v, want only Test Game", testList.Items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportCatalogGamesCanDefaultToHidden(t *testing.T) {
|
||||||
|
service, db := newTestBaishunService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if err := db.Create(&model.BaishunProviderConfig{
|
||||||
|
ID: 1101,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileProd,
|
||||||
|
Active: true,
|
||||||
|
PlatformBaseURL: "https://prod.example.com",
|
||||||
|
AppID: 111,
|
||||||
|
AppChannel: "prod",
|
||||||
|
AppKey: "prod-key",
|
||||||
|
GSP: 101,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create provider config: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.BaishunGameCatalog{
|
||||||
|
ID: 2101,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileProd,
|
||||||
|
InternalGameID: "bs_9101",
|
||||||
|
VendorGameID: 9101,
|
||||||
|
Name: "Hidden Game",
|
||||||
|
Status: baishunCatalogEnabled,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create catalog: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileProd, []int{9101}, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("import hidden catalog: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Imported != 1 {
|
||||||
|
t.Fatalf("imported = %d, want 1", resp.Imported)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg model.SysGameListConfig
|
||||||
|
if err := db.Where("sys_origin = ? AND game_origin = ? AND game_id = ?", "LIKEI", baishunVendorType, "bs_9101").First(&cfg).Error; err != nil {
|
||||||
|
t.Fatalf("load imported config: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Showcase {
|
||||||
|
t.Fatalf("imported showcase = true, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogAddedRequiresExistingGameConfigAndImportRepairsDanglingExt(t *testing.T) {
|
||||||
|
service, db := newTestBaishunService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if err := db.Create(&model.BaishunGameCatalog{
|
||||||
|
ID: 3001,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileProd,
|
||||||
|
InternalGameID: "bs_1021",
|
||||||
|
VendorGameID: 1021,
|
||||||
|
Name: "LuckyGift",
|
||||||
|
Cover: "https://cdn.example.com/lucky.png",
|
||||||
|
DownloadURL: "https://cdn.example.com/lucky/index.html",
|
||||||
|
Status: baishunCatalogEnabled,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create catalog: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&model.SysGameListVendorExt{
|
||||||
|
ID: 4001,
|
||||||
|
GameListConfigID: 999999,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileProd,
|
||||||
|
VendorType: baishunVendorType,
|
||||||
|
VendorGameID: "1021",
|
||||||
|
Enabled: true,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create dangling ext: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if catalogAddedForVendor(t, service, ctx, "LIKEI", baishunProfileProd, 1021) {
|
||||||
|
t.Fatalf("catalog added before import = true, want false for dangling ext")
|
||||||
|
}
|
||||||
|
|
||||||
|
importResp, err := service.ImportCatalogGames(ctx, "LIKEI", baishunProfileProd, []int{1021})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("import catalog with dangling ext: %v", err)
|
||||||
|
}
|
||||||
|
if importResp.Imported != 1 || importResp.Existing != 0 {
|
||||||
|
t.Fatalf("import response = %+v, want imported 1 existing 0", importResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ext model.SysGameListVendorExt
|
||||||
|
if err := db.Where("id = ?", int64(4001)).First(&ext).Error; err != nil {
|
||||||
|
t.Fatalf("load repaired ext: %v", err)
|
||||||
|
}
|
||||||
|
if ext.GameListConfigID == 999999 || ext.GameListConfigID == 0 {
|
||||||
|
t.Fatalf("ext game_list_config_id = %d, want repaired config id", ext.GameListConfigID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg model.SysGameListConfig
|
||||||
|
if err := db.Where("id = ? AND sys_origin = ? AND game_origin = ?", ext.GameListConfigID, "LIKEI", baishunVendorType).First(&cfg).Error; err != nil {
|
||||||
|
t.Fatalf("load repaired config: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Name != "LuckyGift" {
|
||||||
|
t.Fatalf("repaired config name = %q, want LuckyGift", cfg.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !catalogAddedForVendor(t, service, ctx, "LIKEI", baishunProfileProd, 1021) {
|
||||||
|
t.Fatalf("catalog added after import = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func catalogAddedForVendor(t *testing.T, service *BaishunService, ctx context.Context, sysOrigin string, profile string, vendorGameID int) bool {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var rows []struct {
|
||||||
|
AddedConfigID *int64
|
||||||
|
}
|
||||||
|
err := service.buildAdminCatalogQuery(ctx, sysOrigin, profile, "").
|
||||||
|
Where("cat.vendor_game_id = ?", vendorGameID).
|
||||||
|
Select("cfg.id AS added_config_id").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("query catalog added flag: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("catalog rows = %d, want 1", len(rows))
|
||||||
|
}
|
||||||
|
return rows[0].AddedConfigID != nil && *rows[0].AddedConfigID > 0
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package baishun
|
|||||||
import (
|
import (
|
||||||
"chatapp3-golang/internal/integration"
|
"chatapp3-golang/internal/integration"
|
||||||
"chatapp3-golang/internal/model"
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
"chatapp3-golang/internal/utils"
|
"chatapp3-golang/internal/utils"
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
@ -10,6 +11,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -237,22 +239,29 @@ func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunCha
|
|||||||
}
|
}
|
||||||
s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusSuccess, rawJSON, utils.MustJSONString(resp, ""), nil)
|
s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusSuccess, rawJSON, utils.MustJSONString(resp, ""), nil)
|
||||||
s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message)
|
s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message)
|
||||||
|
s.reportGameConsumeTaskEvent(ctx, session, req)
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleReport 处理百顺上报类回调,目前仅记录日志并返回成功。
|
func (s *BaishunService) reportGameConsumeTaskEvent(ctx context.Context, session model.BaishunLaunchSession, req BaishunChangeBalanceRequest) {
|
||||||
func (s *BaishunService) HandleReport(ctx context.Context, payload map[string]any, rawJSON string) baishunStandardResponse {
|
if s.taskReporter == nil || req.CurrencyDiff >= 0 {
|
||||||
userID := toString(payload["user_id"])
|
return
|
||||||
orderID := toString(payload["order_id"])
|
}
|
||||||
gameRoundID := toString(payload["game_round_id"])
|
deltaValue := abs64(req.CurrencyDiff)
|
||||||
resp := baishunStandardResponse{
|
if deltaValue <= 0 {
|
||||||
Code: 0,
|
return
|
||||||
Message: "succeed",
|
}
|
||||||
UniqueID: s.uniqueID(),
|
payload := map[string]any{
|
||||||
Data: map[string]any{},
|
"provider": "BAISHUN",
|
||||||
|
"orderId": strings.TrimSpace(req.OrderID),
|
||||||
|
"gameId": req.GameID,
|
||||||
|
"gameRoundId": strings.TrimSpace(req.GameRoundID),
|
||||||
|
"msgType": strings.TrimSpace(req.MsgType),
|
||||||
|
"diffMsg": strings.TrimSpace(req.DiffMsg),
|
||||||
|
}
|
||||||
|
if err := s.taskReporter.ReportSimpleEvent(ctx, session.SysOrigin, fmt.Sprintf("BAISHUN_GAME_CONSUME:%s:%s", session.SysOrigin, strings.TrimSpace(req.OrderID)), taskcenter.EventTypeGameConsumeGold, session.UserID, deltaValue, payload); err != nil {
|
||||||
|
log.Printf("report baishun task event failed: %v", err)
|
||||||
}
|
}
|
||||||
s.saveCallbackLog(ctx, "report", rawJSON, utils.MustJSONString(resp, ""), "", userID, stringPtr(orderID), stringPtr(gameRoundID), baishunCallbackStatusSuccess, 0, resp.Message)
|
|
||||||
return resp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleBalanceInfo 处理百顺查询余额回调。
|
// HandleBalanceInfo 处理百顺查询余额回调。
|
||||||
|
|||||||
@ -18,11 +18,12 @@ import (
|
|||||||
|
|
||||||
// SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。
|
// SyncCatalog 从百顺平台拉取游戏目录并同步到本地表。
|
||||||
func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest) (*SyncCatalogResponse, error) {
|
||||||
sysOrigin := defaultIfBlank(req.SysOrigin, "LIKEI")
|
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
||||||
if strings.TrimSpace(sysOrigin) == "" {
|
profile, err := s.requestProfile(ctx, sysOrigin, req.Profile)
|
||||||
sysOrigin = "LIKEI"
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
runtimeCfg, err := s.requireProviderConfig(ctx, sysOrigin)
|
runtimeCfg, err := s.requireProviderConfigForProfile(ctx, sysOrigin, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -51,6 +52,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
|||||||
record := model.BaishunGameCatalog{
|
record := model.BaishunGameCatalog{
|
||||||
ID: id,
|
ID: id,
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
|
Profile: profile,
|
||||||
InternalGameID: fmt.Sprintf("bs_%d", game.GameID),
|
InternalGameID: fmt.Sprintf("bs_%d", game.GameID),
|
||||||
VendorGameID: game.GameID,
|
VendorGameID: game.GameID,
|
||||||
Name: game.Name,
|
Name: game.Name,
|
||||||
@ -69,7 +71,7 @@ func (s *BaishunService) SyncCatalog(ctx context.Context, req SyncCatalogRequest
|
|||||||
UpdateTime: time.Now(),
|
UpdateTime: time.Now(),
|
||||||
}
|
}
|
||||||
err = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
err = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "vendor_game_id"}},
|
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}, {Name: "vendor_game_id"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{
|
DoUpdates: clause.AssignmentColumns([]string{
|
||||||
"internal_game_id", "name", "cover", "preview_url", "download_url",
|
"internal_game_id", "name", "cover", "preview_url", "download_url",
|
||||||
"package_version", "game_mode_json", "orientation", "safe_height",
|
"package_version", "game_mode_json", "orientation", "safe_height",
|
||||||
@ -104,7 +106,7 @@ func (s *BaishunService) fetchPlatformGames(ctx context.Context, runtimeCfg bais
|
|||||||
payload["app_name"] = runtimeCfg.AppName
|
payload["app_name"] = runtimeCfg.AppName
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
endpoint := strings.TrimRight(runtimeCfg.PlatformBaseURL, "/") + "/v1/api/gamelist"
|
endpoint := runtimeCfg.gameListURL()
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
type adminCatalogRow struct {
|
type adminCatalogRow struct {
|
||||||
VendorGameID int
|
VendorGameID int
|
||||||
|
Profile string
|
||||||
InternalGameID string
|
InternalGameID string
|
||||||
Name string
|
Name string
|
||||||
Cover string
|
Cover string
|
||||||
@ -28,24 +29,30 @@ type adminCatalogRow struct {
|
|||||||
func (s *BaishunService) PageCatalog(
|
func (s *BaishunService) PageCatalog(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sysOrigin string,
|
sysOrigin string,
|
||||||
|
profile string,
|
||||||
keyword string,
|
keyword string,
|
||||||
cursor int,
|
cursor int,
|
||||||
limit int,
|
limit int,
|
||||||
) (*AdminBaishunCatalogPageResponse, error) {
|
) (*AdminBaishunCatalogPageResponse, error) {
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.requestProfile(ctx, sysOrigin, profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
cursor = normalizePageCursor(cursor)
|
cursor = normalizePageCursor(cursor)
|
||||||
limit = normalizePageLimit(limit)
|
limit = normalizePageLimit(limit)
|
||||||
|
|
||||||
countQuery := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword)
|
countQuery := s.buildAdminCatalogQuery(ctx, sysOrigin, profile, keyword)
|
||||||
var total int64
|
var total int64
|
||||||
if err := countQuery.Count(&total).Error; err != nil {
|
if err := countQuery.Count(&total).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []adminCatalogRow
|
var rows []adminCatalogRow
|
||||||
err := s.buildAdminCatalogQuery(ctx, sysOrigin, keyword).
|
err = s.buildAdminCatalogQuery(ctx, sysOrigin, profile, keyword).
|
||||||
Select(`
|
Select(`
|
||||||
cat.vendor_game_id AS vendor_game_id,
|
cat.vendor_game_id AS vendor_game_id,
|
||||||
|
cat.profile AS profile,
|
||||||
cat.internal_game_id AS internal_game_id,
|
cat.internal_game_id AS internal_game_id,
|
||||||
cat.name AS name,
|
cat.name AS name,
|
||||||
cat.cover AS cover,
|
cat.cover AS cover,
|
||||||
@ -56,7 +63,7 @@ func (s *BaishunService) PageCatalog(
|
|||||||
cat.status AS status,
|
cat.status AS status,
|
||||||
cat.safe_height AS safe_height,
|
cat.safe_height AS safe_height,
|
||||||
cat.orientation AS orientation,
|
cat.orientation AS orientation,
|
||||||
ext.game_list_config_id AS added_config_id,
|
cfg.id AS added_config_id,
|
||||||
cfg.is_showcase AS showcase,
|
cfg.is_showcase AS showcase,
|
||||||
DATE_FORMAT(cat.update_time, '%Y-%m-%d %H:%i:%s') AS update_time
|
DATE_FORMAT(cat.update_time, '%Y-%m-%d %H:%i:%s') AS update_time
|
||||||
`).
|
`).
|
||||||
@ -72,6 +79,7 @@ func (s *BaishunService) PageCatalog(
|
|||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
items = append(items, AdminBaishunCatalogItem{
|
items = append(items, AdminBaishunCatalogItem{
|
||||||
VendorGameID: row.VendorGameID,
|
VendorGameID: row.VendorGameID,
|
||||||
|
Profile: normalizeBaishunProfile(row.Profile),
|
||||||
GameID: row.InternalGameID,
|
GameID: row.InternalGameID,
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
Cover: row.Cover,
|
Cover: row.Cover,
|
||||||
@ -100,18 +108,24 @@ func (s *BaishunService) FetchAdminCatalog(ctx context.Context, req SyncCatalogR
|
|||||||
return s.SyncCatalog(ctx, req)
|
return s.SyncCatalog(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) buildAdminCatalogQuery(ctx context.Context, sysOrigin string, keyword string) *gorm.DB {
|
func (s *BaishunService) buildAdminCatalogQuery(ctx context.Context, sysOrigin string, profile string, keyword string) *gorm.DB {
|
||||||
query := s.repo.DB.WithContext(ctx).
|
query := s.repo.DB.WithContext(ctx).
|
||||||
Table("baishun_game_catalog AS cat").
|
Table("baishun_game_catalog AS cat").
|
||||||
Joins(`
|
Joins(`
|
||||||
LEFT JOIN sys_game_list_vendor_ext ext
|
LEFT JOIN sys_game_list_vendor_ext ext
|
||||||
ON ext.sys_origin = cat.sys_origin
|
ON ext.sys_origin = cat.sys_origin
|
||||||
|
AND ext.profile = cat.profile
|
||||||
AND ext.vendor_type = ?
|
AND ext.vendor_type = ?
|
||||||
AND ext.vendor_game_id = CAST(cat.vendor_game_id AS CHAR)
|
AND ext.vendor_game_id = CAST(cat.vendor_game_id AS CHAR)
|
||||||
AND ext.enabled = 1
|
AND ext.enabled = 1
|
||||||
`, baishunVendorType).
|
`, baishunVendorType).
|
||||||
Joins("LEFT JOIN sys_game_list_config cfg ON cfg.id = ext.game_list_config_id").
|
Joins(`
|
||||||
Where("cat.sys_origin = ?", sysOrigin)
|
LEFT JOIN sys_game_list_config cfg
|
||||||
|
ON cfg.id = ext.game_list_config_id
|
||||||
|
AND cfg.sys_origin = cat.sys_origin
|
||||||
|
AND cfg.game_origin = ?
|
||||||
|
`, baishunVendorType).
|
||||||
|
Where("cat.sys_origin = ? AND cat.profile = ?", sysOrigin, profile)
|
||||||
keyword = strings.TrimSpace(keyword)
|
keyword = strings.TrimSpace(keyword)
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
like := "%" + keyword + "%"
|
like := "%" + keyword + "%"
|
||||||
@ -134,3 +148,14 @@ func (s *BaishunService) requireProviderConfig(ctx context.Context, sysOrigin st
|
|||||||
}
|
}
|
||||||
return runtimeCfg, nil
|
return runtimeCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) requireProviderConfigForProfile(ctx context.Context, sysOrigin string, profile string) (baishunRuntimeConfig, error) {
|
||||||
|
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||||
|
if err != nil {
|
||||||
|
return baishunRuntimeConfig{}, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(runtimeCfg.PlatformBaseURL) == "" || runtimeCfg.AppID <= 0 || strings.TrimSpace(runtimeCfg.AppKey) == "" {
|
||||||
|
return baishunRuntimeConfig{}, NewAppError(http.StatusBadRequest, "baishun_config_missing", "baishun provider config is missing")
|
||||||
|
}
|
||||||
|
return runtimeCfg, nil
|
||||||
|
}
|
||||||
|
|||||||
25
internal/service/baishun/game_list_cache.go
Normal file
25
internal/service/baishun/game_list_cache.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package baishun
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
const javaGameListCachePattern = "GAME_LIST*"
|
||||||
|
|
||||||
|
func (s *BaishunService) clearJavaGameListCache(ctx context.Context) {
|
||||||
|
if s.repo.Redis == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var cursor uint64
|
||||||
|
for {
|
||||||
|
keys, nextCursor, err := s.repo.Redis.Scan(ctx, cursor, javaGameListCachePattern, 100).Result()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
_ = s.repo.Redis.Del(ctx, keys...).Err()
|
||||||
|
}
|
||||||
|
if nextCursor == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cursor = nextCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
122
internal/service/baishun/launch_fallback_test.go
Normal file
122
internal/service/baishun/launch_fallback_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package baishun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubGateway struct{}
|
||||||
|
|
||||||
|
func (stubGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) {
|
||||||
|
return integration.UserProfile{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubGateway) MapGoldBalance(context.Context, []int64) (map[int64]int64, error) {
|
||||||
|
return map[int64]int64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubGateway) GetUserLanguage(context.Context, int64) (string, error) {
|
||||||
|
return "en", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubGateway) ExistsGoldEvent(context.Context, string) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stubGateway) ChangeGoldBalance(context.Context, integration.GoldReceiptCommand) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestBaishunService(t *testing.T) (*BaishunService, *gorm.DB) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(
|
||||||
|
&model.SysGameListConfig{},
|
||||||
|
&model.SysGameListVendorExt{},
|
||||||
|
&model.BaishunProviderConfig{},
|
||||||
|
&model.BaishunGameCatalog{},
|
||||||
|
&model.BaishunLaunchSession{},
|
||||||
|
&model.BaishunCallbackLog{},
|
||||||
|
&model.BaishunRoomState{},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := NewBaishunService(config.Config{
|
||||||
|
Baishun: config.BaishunConfig{
|
||||||
|
PlatformBaseURL: "https://mesh-channels-online.jieyou.shop",
|
||||||
|
AppID: 7485318192,
|
||||||
|
AppName: "yumiparty_app",
|
||||||
|
AppChannel: "yumiparty",
|
||||||
|
AppKey: "test-app-key",
|
||||||
|
GSP: 111,
|
||||||
|
LaunchCodeTTLSeconds: 300,
|
||||||
|
SSTokenTTLSeconds: 86400,
|
||||||
|
},
|
||||||
|
}, db, nil, stubGateway{})
|
||||||
|
return service, db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLaunchGameFallsBackToCatalogWhenConfigMissing(t *testing.T) {
|
||||||
|
service, db := newTestBaishunService(t)
|
||||||
|
|
||||||
|
if err := db.Create(&model.BaishunGameCatalog{
|
||||||
|
ID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Profile: baishunProfileProd,
|
||||||
|
InternalGameID: "bs_1146",
|
||||||
|
VendorGameID: 1146,
|
||||||
|
Name: "LordOfOlympus",
|
||||||
|
Cover: "https://cdn.example.com/cover.png",
|
||||||
|
PreviewURL: "https://cdn.example.com/preview.png",
|
||||||
|
DownloadURL: "https://cdn.example.com/game/index.html",
|
||||||
|
PackageVersion: "1.0.0",
|
||||||
|
SafeHeight: 24,
|
||||||
|
GSP: "222",
|
||||||
|
Status: baishunCatalogEnabled,
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("create catalog: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.LaunchGame(context.Background(), AuthUser{
|
||||||
|
UserID: 1234567,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, BaishunLaunchAppRequest{
|
||||||
|
RoomID: "room-1",
|
||||||
|
GameID: "bs_1146",
|
||||||
|
SceneMode: 1,
|
||||||
|
ClientOrigin: "COMMON",
|
||||||
|
}, "127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LaunchGame() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.GameID != "bs_1146" {
|
||||||
|
t.Fatalf("gameId = %q, want bs_1146", resp.GameID)
|
||||||
|
}
|
||||||
|
if resp.VendorGameID != 1146 {
|
||||||
|
t.Fatalf("vendorGameId = %d, want 1146", resp.VendorGameID)
|
||||||
|
}
|
||||||
|
if resp.Entry.EntryURL != "https://cdn.example.com/game/index.html" {
|
||||||
|
t.Fatalf("entryUrl = %q, want catalog download url", resp.Entry.EntryURL)
|
||||||
|
}
|
||||||
|
if resp.BridgeConfig.GSP != 222 {
|
||||||
|
t.Fatalf("gsp = %d, want 222", resp.BridgeConfig.GSP)
|
||||||
|
}
|
||||||
|
if resp.RoomState.State != baishunRoomStatePlaying {
|
||||||
|
t.Fatalf("roomState.state = %q, want %q", resp.RoomState.State, baishunRoomStatePlaying)
|
||||||
|
}
|
||||||
|
}
|
||||||
61
internal/service/baishun/profile.go
Normal file
61
internal/service/baishun/profile.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package baishun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baishunProfileProd = "PROD"
|
||||||
|
baishunProfileTest = "TEST"
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalizeBaishunProfile(profile string) string {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(profile)) {
|
||||||
|
case "", "PROD", "PRODUCT", "PRODUCTION", "ONLINE", "OFFICIAL", "FORMAL":
|
||||||
|
return baishunProfileProd
|
||||||
|
case "TEST", "TESTING", "SANDBOX", "DEV":
|
||||||
|
return baishunProfileTest
|
||||||
|
default:
|
||||||
|
return strings.ToUpper(strings.TrimSpace(profile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateBaishunProfile(profile string) (string, error) {
|
||||||
|
profile = normalizeBaishunProfile(profile)
|
||||||
|
switch profile {
|
||||||
|
case baishunProfileProd, baishunProfileTest:
|
||||||
|
return profile, nil
|
||||||
|
default:
|
||||||
|
return "", NewAppError(http.StatusBadRequest, "profile_invalid", "profile must be PROD or TEST")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) activeProfile(ctx context.Context, sysOrigin string) (string, error) {
|
||||||
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
|
||||||
|
var row model.BaishunProviderConfig
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND active = ?", sysOrigin, true).
|
||||||
|
Order("update_time DESC").
|
||||||
|
Limit(1).
|
||||||
|
First(&row).Error
|
||||||
|
if err == nil {
|
||||||
|
return normalizeBaishunProfile(row.Profile), nil
|
||||||
|
}
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return baishunProfileProd, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) requestProfile(ctx context.Context, sysOrigin string, profile string) (string, error) {
|
||||||
|
if strings.TrimSpace(profile) != "" {
|
||||||
|
return validateBaishunProfile(profile)
|
||||||
|
}
|
||||||
|
return s.activeProfile(ctx, sysOrigin)
|
||||||
|
}
|
||||||
@ -14,7 +14,10 @@ import (
|
|||||||
|
|
||||||
type baishunRuntimeConfig struct {
|
type baishunRuntimeConfig struct {
|
||||||
SysOrigin string
|
SysOrigin string
|
||||||
|
Profile string
|
||||||
PlatformBaseURL string
|
PlatformBaseURL string
|
||||||
|
GameListURL string
|
||||||
|
LuckyGiftURL string
|
||||||
AppID int64
|
AppID int64
|
||||||
AppName string
|
AppName string
|
||||||
AppChannel string
|
AppChannel string
|
||||||
@ -28,6 +31,14 @@ func (c baishunRuntimeConfig) appChannel() string {
|
|||||||
return defaultIfBlank(strings.TrimSpace(c.AppChannel), "skychat")
|
return defaultIfBlank(strings.TrimSpace(c.AppChannel), "skychat")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c baishunRuntimeConfig) gameListURL() string {
|
||||||
|
return resolveBaishunGameListURL(c.PlatformBaseURL, c.GameListURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c baishunRuntimeConfig) luckyGiftURL() string {
|
||||||
|
return resolveBaishunLuckyGiftURL(c.PlatformBaseURL, c.LuckyGiftURL)
|
||||||
|
}
|
||||||
|
|
||||||
func (c baishunRuntimeConfig) gsp() int {
|
func (c baishunRuntimeConfig) gsp() int {
|
||||||
if c.GSP > 0 {
|
if c.GSP > 0 {
|
||||||
return c.GSP
|
return c.GSP
|
||||||
@ -50,9 +61,13 @@ func (c baishunRuntimeConfig) ssTokenTTLSeconds() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeConfig {
|
func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeConfig {
|
||||||
|
platformBaseURL := strings.TrimSpace(s.cfg.Baishun.PlatformBaseURL)
|
||||||
return baishunRuntimeConfig{
|
return baishunRuntimeConfig{
|
||||||
SysOrigin: normalizeAdminSysOrigin(sysOrigin),
|
SysOrigin: normalizeAdminSysOrigin(sysOrigin),
|
||||||
PlatformBaseURL: strings.TrimSpace(s.cfg.Baishun.PlatformBaseURL),
|
Profile: baishunProfileProd,
|
||||||
|
PlatformBaseURL: platformBaseURL,
|
||||||
|
GameListURL: resolveBaishunGameListURL(platformBaseURL, s.cfg.Baishun.GameListURL),
|
||||||
|
LuckyGiftURL: resolveBaishunLuckyGiftURL(platformBaseURL, s.cfg.Baishun.LuckyGiftURL),
|
||||||
AppID: s.cfg.Baishun.AppID,
|
AppID: s.cfg.Baishun.AppID,
|
||||||
AppName: strings.TrimSpace(s.cfg.Baishun.AppName),
|
AppName: strings.TrimSpace(s.cfg.Baishun.AppName),
|
||||||
AppChannel: strings.TrimSpace(s.cfg.Baishun.AppChannel),
|
AppChannel: strings.TrimSpace(s.cfg.Baishun.AppChannel),
|
||||||
@ -65,9 +80,20 @@ func (s *BaishunService) defaultRuntimeConfig(sysOrigin string) baishunRuntimeCo
|
|||||||
|
|
||||||
func applyProviderRow(base baishunRuntimeConfig, row model.BaishunProviderConfig) baishunRuntimeConfig {
|
func applyProviderRow(base baishunRuntimeConfig, row model.BaishunProviderConfig) baishunRuntimeConfig {
|
||||||
base.SysOrigin = normalizeAdminSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin))
|
base.SysOrigin = normalizeAdminSysOrigin(defaultIfBlank(row.SysOrigin, base.SysOrigin))
|
||||||
|
base.Profile = normalizeBaishunProfile(defaultIfBlank(row.Profile, base.Profile))
|
||||||
if strings.TrimSpace(row.PlatformBaseURL) != "" {
|
if strings.TrimSpace(row.PlatformBaseURL) != "" {
|
||||||
base.PlatformBaseURL = strings.TrimSpace(row.PlatformBaseURL)
|
base.PlatformBaseURL = strings.TrimSpace(row.PlatformBaseURL)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(row.GameListURL) != "" {
|
||||||
|
base.GameListURL = strings.TrimSpace(row.GameListURL)
|
||||||
|
} else {
|
||||||
|
base.GameListURL = resolveBaishunGameListURL(base.PlatformBaseURL, "")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(row.LuckyGiftURL) != "" {
|
||||||
|
base.LuckyGiftURL = strings.TrimSpace(row.LuckyGiftURL)
|
||||||
|
} else {
|
||||||
|
base.LuckyGiftURL = resolveBaishunLuckyGiftURL(base.PlatformBaseURL, "")
|
||||||
|
}
|
||||||
if row.AppID > 0 {
|
if row.AppID > 0 {
|
||||||
base.AppID = row.AppID
|
base.AppID = row.AppID
|
||||||
}
|
}
|
||||||
@ -94,11 +120,25 @@ func applyProviderRow(base baishunRuntimeConfig, row model.BaishunProviderConfig
|
|||||||
|
|
||||||
func (s *BaishunService) resolveRuntimeConfig(ctx context.Context, sysOrigin string) (baishunRuntimeConfig, error) {
|
func (s *BaishunService) resolveRuntimeConfig(ctx context.Context, sysOrigin string) (baishunRuntimeConfig, error) {
|
||||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return baishunRuntimeConfig{}, err
|
||||||
|
}
|
||||||
|
return s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) resolveRuntimeConfigForProfile(ctx context.Context, sysOrigin string, profile string) (baishunRuntimeConfig, error) {
|
||||||
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := validateBaishunProfile(profile)
|
||||||
|
if err != nil {
|
||||||
|
return baishunRuntimeConfig{}, err
|
||||||
|
}
|
||||||
base := s.defaultRuntimeConfig(sysOrigin)
|
base := s.defaultRuntimeConfig(sysOrigin)
|
||||||
|
base.Profile = profile
|
||||||
|
|
||||||
var row model.BaishunProviderConfig
|
var row model.BaishunProviderConfig
|
||||||
err := s.repo.DB.WithContext(ctx).
|
err = s.repo.DB.WithContext(ctx).
|
||||||
Where("sys_origin = ?", sysOrigin).
|
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||||
Limit(1).
|
Limit(1).
|
||||||
First(&row).Error
|
First(&row).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -133,7 +173,11 @@ func (s *BaishunService) resolveRuntimeConfigByApp(ctx context.Context, appID in
|
|||||||
|
|
||||||
for _, build := range queries {
|
for _, build := range queries {
|
||||||
var row model.BaishunProviderConfig
|
var row model.BaishunProviderConfig
|
||||||
err := build(s.repo.DB.WithContext(ctx)).Limit(1).First(&row).Error
|
err := build(s.repo.DB.WithContext(ctx)).
|
||||||
|
Order("active DESC").
|
||||||
|
Order("update_time DESC").
|
||||||
|
Limit(1).
|
||||||
|
First(&row).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return applyProviderRow(s.defaultRuntimeConfig(row.SysOrigin), row), nil
|
return applyProviderRow(s.defaultRuntimeConfig(row.SysOrigin), row), nil
|
||||||
}
|
}
|
||||||
@ -144,22 +188,36 @@ func (s *BaishunService) resolveRuntimeConfigByApp(ctx context.Context, appID in
|
|||||||
return base, nil
|
return base, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string) (*BaishunProviderConfigItem, error) {
|
func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string, profile string) (*BaishunProviderConfigItem, error) {
|
||||||
runtimeCfg, err := s.resolveRuntimeConfig(ctx, sysOrigin)
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
activeProfile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
profile, err = s.requestProfile(ctx, sysOrigin, profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
runtimeCfg, err := s.resolveRuntimeConfigForProfile(ctx, sysOrigin, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var row model.BaishunProviderConfig
|
var row model.BaishunProviderConfig
|
||||||
_ = s.repo.DB.WithContext(ctx).
|
_ = s.repo.DB.WithContext(ctx).
|
||||||
Where("sys_origin = ?", normalizeAdminSysOrigin(sysOrigin)).
|
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||||
Limit(1).
|
Limit(1).
|
||||||
First(&row).Error
|
First(&row).Error
|
||||||
|
|
||||||
return &BaishunProviderConfigItem{
|
return &BaishunProviderConfigItem{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
SysOrigin: runtimeCfg.SysOrigin,
|
SysOrigin: runtimeCfg.SysOrigin,
|
||||||
|
Profile: runtimeCfg.Profile,
|
||||||
|
ActiveProfile: activeProfile,
|
||||||
|
Active: runtimeCfg.Profile == activeProfile,
|
||||||
PlatformBaseURL: runtimeCfg.PlatformBaseURL,
|
PlatformBaseURL: runtimeCfg.PlatformBaseURL,
|
||||||
|
GameListURL: runtimeCfg.gameListURL(),
|
||||||
|
LuckyGiftURL: runtimeCfg.luckyGiftURL(),
|
||||||
AppID: runtimeCfg.AppID,
|
AppID: runtimeCfg.AppID,
|
||||||
AppName: runtimeCfg.AppName,
|
AppName: runtimeCfg.AppName,
|
||||||
AppChannel: runtimeCfg.appChannel(),
|
AppChannel: runtimeCfg.appChannel(),
|
||||||
@ -173,13 +231,27 @@ func (s *BaishunService) GetProviderConfig(ctx context.Context, sysOrigin string
|
|||||||
|
|
||||||
func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishunProviderConfigRequest) (*BaishunProviderConfigItem, error) {
|
func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishunProviderConfigRequest) (*BaishunProviderConfigItem, error) {
|
||||||
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
||||||
|
profile, err := validateBaishunProfile(req.Profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
platformBaseURL := strings.TrimSpace(req.PlatformBaseURL)
|
platformBaseURL := strings.TrimSpace(req.PlatformBaseURL)
|
||||||
|
gameListURL := strings.TrimSpace(req.GameListURL)
|
||||||
|
luckyGiftURL := strings.TrimSpace(req.LuckyGiftURL)
|
||||||
appChannel := strings.TrimSpace(req.AppChannel)
|
appChannel := strings.TrimSpace(req.AppChannel)
|
||||||
appKey := strings.TrimSpace(req.AppKey)
|
appKey := strings.TrimSpace(req.AppKey)
|
||||||
|
if gameListURL == "" {
|
||||||
|
gameListURL = resolveBaishunGameListURL(platformBaseURL, "")
|
||||||
|
}
|
||||||
|
if luckyGiftURL == "" {
|
||||||
|
luckyGiftURL = resolveBaishunLuckyGiftURL(platformBaseURL, "")
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case platformBaseURL == "":
|
case platformBaseURL == "":
|
||||||
return nil, NewAppError(http.StatusBadRequest, "platform_base_url_required", "platformBaseUrl is required")
|
return nil, NewAppError(http.StatusBadRequest, "platform_base_url_required", "platformBaseUrl is required")
|
||||||
|
case luckyGiftURL == "":
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "lucky_gift_url_required", "luckyGiftUrl is required")
|
||||||
case req.AppID <= 0:
|
case req.AppID <= 0:
|
||||||
return nil, NewAppError(http.StatusBadRequest, "app_id_required", "appId is required")
|
return nil, NewAppError(http.StatusBadRequest, "app_id_required", "appId is required")
|
||||||
case appChannel == "":
|
case appChannel == "":
|
||||||
@ -196,7 +268,10 @@ func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishun
|
|||||||
row := model.BaishunProviderConfig{
|
row := model.BaishunProviderConfig{
|
||||||
ID: id,
|
ID: id,
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
|
Profile: profile,
|
||||||
PlatformBaseURL: platformBaseURL,
|
PlatformBaseURL: platformBaseURL,
|
||||||
|
GameListURL: gameListURL,
|
||||||
|
LuckyGiftURL: luckyGiftURL,
|
||||||
AppID: req.AppID,
|
AppID: req.AppID,
|
||||||
AppName: strings.TrimSpace(req.AppName),
|
AppName: strings.TrimSpace(req.AppName),
|
||||||
AppChannel: appChannel,
|
AppChannel: appChannel,
|
||||||
@ -216,15 +291,91 @@ func (s *BaishunService) SaveProviderConfig(ctx context.Context, req SaveBaishun
|
|||||||
if row.SSTokenTTLSeconds <= 0 {
|
if row.SSTokenTTLSeconds <= 0 {
|
||||||
row.SSTokenTTLSeconds = 86400
|
row.SSTokenTTLSeconds = 86400
|
||||||
}
|
}
|
||||||
|
var activeCount int64
|
||||||
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
|
Model(&model.BaishunProviderConfig{}).
|
||||||
|
Where("sys_origin = ? AND active = ?", sysOrigin, true).
|
||||||
|
Count(&activeCount).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
row.Active = activeCount == 0
|
||||||
|
|
||||||
if err := s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
if err := s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "sys_origin"}},
|
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "profile"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{
|
DoUpdates: clause.AssignmentColumns([]string{
|
||||||
"platform_base_url", "app_id", "app_name", "app_channel", "app_key",
|
"platform_base_url", "game_list_url", "lucky_gift_url", "app_id", "app_name", "app_channel", "app_key",
|
||||||
"gsp", "launch_code_ttl_seconds", "ss_token_ttl_seconds", "update_time",
|
"gsp", "launch_code_ttl_seconds", "ss_token_ttl_seconds", "update_time",
|
||||||
}),
|
}),
|
||||||
}).Create(&row).Error; err != nil {
|
}).Create(&row).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.GetProviderConfig(ctx, sysOrigin)
|
if row.Active {
|
||||||
|
s.clearJavaGameListCache(ctx)
|
||||||
|
}
|
||||||
|
return s.GetProviderConfig(ctx, sysOrigin, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveBaishunGameListURL(platformBaseURL string, gameListURL string) string {
|
||||||
|
gameListURL = strings.TrimSpace(gameListURL)
|
||||||
|
if gameListURL != "" {
|
||||||
|
return gameListURL
|
||||||
|
}
|
||||||
|
platformBaseURL = strings.TrimRight(strings.TrimSpace(platformBaseURL), "/")
|
||||||
|
if platformBaseURL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return platformBaseURL + "/v1/api/gamelist"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveBaishunLuckyGiftURL(platformBaseURL string, luckyGiftURL string) string {
|
||||||
|
luckyGiftURL = strings.TrimSpace(luckyGiftURL)
|
||||||
|
if luckyGiftURL != "" {
|
||||||
|
return luckyGiftURL
|
||||||
|
}
|
||||||
|
platformBaseURL = strings.TrimRight(strings.TrimSpace(platformBaseURL), "/")
|
||||||
|
if platformBaseURL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return platformBaseURL + "/lucky_gift/start_game"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) ActivateProviderProfile(ctx context.Context, req ActivateBaishunProviderProfileRequest) (*BaishunProviderConfigItem, error) {
|
||||||
|
sysOrigin := normalizeAdminSysOrigin(req.SysOrigin)
|
||||||
|
profile, err := validateBaishunProfile(req.Profile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var row model.BaishunProviderConfig
|
||||||
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||||
|
Limit(1).
|
||||||
|
First(&row).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "profile_config_missing", "profile config is missing")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Model(&model.BaishunProviderConfig{}).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"active": false,
|
||||||
|
"update_time": now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Model(&model.BaishunProviderConfig{}).
|
||||||
|
Where("sys_origin = ? AND profile = ?", sysOrigin, profile).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"active": true,
|
||||||
|
"update_time": now,
|
||||||
|
}).Error
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.clearJavaGameListCache(ctx)
|
||||||
|
return s.GetProviderConfig(ctx, sysOrigin, profile)
|
||||||
}
|
}
|
||||||
|
|||||||
291
internal/service/baishun/report.go
Normal file
291
internal/service/baishun/report.go
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
package baishun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/service/highwin"
|
||||||
|
"chatapp3-golang/internal/utils"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baishunReportTypeStart = "game_start"
|
||||||
|
baishunReportTypeSettle = "game_settle"
|
||||||
|
baishunReportBetTTL = 24 * time.Hour
|
||||||
|
baishunReportBetRedisPrefix = "baishun:report:bet"
|
||||||
|
)
|
||||||
|
|
||||||
|
type baishunGameBetRecord struct {
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
RoomID string `json:"roomId"`
|
||||||
|
GameID int64 `json:"gameId"`
|
||||||
|
GameRoundID string `json:"gameRoundId"`
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
Bet int64 `json:"bet"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) HandleReport(ctx context.Context, req BaishunReportRequest, rawJSON string) baishunStandardResponse {
|
||||||
|
resp := baishunStandardResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "succeed",
|
||||||
|
UniqueID: s.uniqueID(),
|
||||||
|
Data: map[string]any{},
|
||||||
|
}
|
||||||
|
|
||||||
|
sysOrigin := ""
|
||||||
|
if session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID); ok {
|
||||||
|
sysOrigin = session.SysOrigin
|
||||||
|
if err := s.handleReportHighWins(ctx, session.SysOrigin, req); err != nil {
|
||||||
|
log.Printf("handle baishun report high win failed userId=%s reportType=%s: %v", req.UserID, req.ReportType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.saveCallbackLog(
|
||||||
|
ctx,
|
||||||
|
"report",
|
||||||
|
rawJSON,
|
||||||
|
utils.MustJSONString(resp, ""),
|
||||||
|
sysOrigin,
|
||||||
|
req.UserID,
|
||||||
|
nil,
|
||||||
|
stringPtr(reportGameRoundID(req.ReportMsg)),
|
||||||
|
baishunCallbackStatusSuccess,
|
||||||
|
0,
|
||||||
|
resp.Message,
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) handleReportHighWins(ctx context.Context, sysOrigin string, req BaishunReportRequest) error {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(req.ReportType)) {
|
||||||
|
case baishunReportTypeStart:
|
||||||
|
return s.rememberBaishunGameBets(ctx, sysOrigin, req.ReportMsg)
|
||||||
|
case baishunReportTypeSettle:
|
||||||
|
return s.publishBaishunGameHighWins(ctx, sysOrigin, req.ReportMsg)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) rememberBaishunGameBets(ctx context.Context, sysOrigin string, payload map[string]any) error {
|
||||||
|
if s == nil || s.repo.Redis == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
roomID := reportText(payload, "room_id", "roomId")
|
||||||
|
gameRoundID := reportGameRoundID(payload)
|
||||||
|
if strings.TrimSpace(roomID) == "" || strings.TrimSpace(gameRoundID) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gameID := reportInt64(payload, "game_id", "gameId")
|
||||||
|
for _, player := range reportPlayers(payload) {
|
||||||
|
if reportInt64(player, "is_ai", "isAi") != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userID := reportInt64(player, "user_id", "userId")
|
||||||
|
bet := reportInt64(player, "bet")
|
||||||
|
if userID <= 0 || bet <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
record := baishunGameBetRecord{
|
||||||
|
SysOrigin: normalizeAdminSysOrigin(sysOrigin),
|
||||||
|
RoomID: roomID,
|
||||||
|
GameID: gameID,
|
||||||
|
GameRoundID: gameRoundID,
|
||||||
|
UserID: userID,
|
||||||
|
Bet: bet,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.repo.Redis.Set(ctx, baishunGameBetKey(record.SysOrigin, gameRoundID, userID), string(body), baishunReportBetTTL).Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) publishBaishunGameHighWins(ctx context.Context, sysOrigin string, payload map[string]any) error {
|
||||||
|
if s == nil || s.highWinBroadcaster == nil || s.repo.Redis == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
roomID := reportText(payload, "room_id", "roomId")
|
||||||
|
gameRoundID := reportGameRoundID(payload)
|
||||||
|
if strings.TrimSpace(roomID) == "" || strings.TrimSpace(gameRoundID) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gameID := reportInt64(payload, "game_id", "gameId")
|
||||||
|
gameCover := s.resolveBaishunReportGameCover(ctx, sysOrigin, roomID, gameID)
|
||||||
|
|
||||||
|
for _, player := range reportPlayers(payload) {
|
||||||
|
if reportInt64(player, "is_ai", "isAi") != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userID := reportInt64(player, "user_id", "userId")
|
||||||
|
reward := reportInt64(player, "reward")
|
||||||
|
if userID <= 0 || reward <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
betRecord, ok, err := s.loadBaishunGameBet(ctx, sysOrigin, gameRoundID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok || betRecord.Bet <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
multiple := highwin.Multiple(reward, betRecord.Bet)
|
||||||
|
if !highwin.ShouldBroadcast(multiple, reward) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := map[string]any{
|
||||||
|
"gameId": gameID,
|
||||||
|
"gameRoundId": gameRoundID,
|
||||||
|
"gameUrl": gameCover,
|
||||||
|
"betAmount": betRecord.Bet,
|
||||||
|
"currencyDiff": reward,
|
||||||
|
"rank": reportInt64(player, "rank"),
|
||||||
|
"score": reportInt64(player, "score"),
|
||||||
|
}
|
||||||
|
if err := highwin.SendRegionBroadcast(ctx, s.highWinBroadcaster, highwin.BroadcastEvent{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Type: highwin.MessageTypeBaishunWin,
|
||||||
|
UserID: userID,
|
||||||
|
RoomID: roomID,
|
||||||
|
Multiple: multiple,
|
||||||
|
Amount: reward,
|
||||||
|
Data: data,
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("send baishun game high win region broadcast failed roundId=%s userId=%d: %v", gameRoundID, userID, err)
|
||||||
|
}
|
||||||
|
_ = s.repo.Redis.Del(ctx, baishunGameBetKey(sysOrigin, gameRoundID, userID)).Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) loadBaishunGameBet(ctx context.Context, sysOrigin, gameRoundID string, userID int64) (baishunGameBetRecord, bool, error) {
|
||||||
|
raw, err := s.repo.Redis.Get(ctx, baishunGameBetKey(sysOrigin, gameRoundID, userID)).Result()
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return baishunGameBetRecord{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return baishunGameBetRecord{}, false, err
|
||||||
|
}
|
||||||
|
var record baishunGameBetRecord
|
||||||
|
if err := json.Unmarshal([]byte(raw), &record); err != nil {
|
||||||
|
return baishunGameBetRecord{}, false, err
|
||||||
|
}
|
||||||
|
return record, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) resolveBaishunReportGameCover(ctx context.Context, sysOrigin, roomID string, gameID int64) string {
|
||||||
|
var state model.BaishunRoomState
|
||||||
|
if strings.TrimSpace(roomID) != "" {
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND room_id = ?", normalizeAdminSysOrigin(sysOrigin), roomID).
|
||||||
|
First(&state).Error
|
||||||
|
if err == nil && strings.TrimSpace(state.CurrentGameCover) != "" {
|
||||||
|
return strings.TrimSpace(state.CurrentGameCover)
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gameID <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var catalog model.BaishunGameCatalog
|
||||||
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND profile = ? AND vendor_game_id = ?", normalizeAdminSysOrigin(sysOrigin), profile, gameID).
|
||||||
|
First(&catalog).Error; err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(catalog.Cover)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportGameRoundID(payload map[string]any) string {
|
||||||
|
return reportText(payload, "game_round_id", "gameRoundId")
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportPlayers(payload map[string]any) []map[string]any {
|
||||||
|
raw, exists := payload["players"]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch typed := raw.(type) {
|
||||||
|
case []map[string]any:
|
||||||
|
return typed
|
||||||
|
case []any:
|
||||||
|
players := make([]map[string]any, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
if player, ok := item.(map[string]any); ok {
|
||||||
|
players = append(players, player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return players
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportText(payload map[string]any, keys ...string) string {
|
||||||
|
for _, key := range keys {
|
||||||
|
if value, exists := payload[key]; exists {
|
||||||
|
text := strings.TrimSpace(toString(value))
|
||||||
|
if text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportInt64(payload map[string]any, keys ...string) int64 {
|
||||||
|
for _, key := range keys {
|
||||||
|
value, exists := payload[key]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(typed)
|
||||||
|
case int64:
|
||||||
|
return typed
|
||||||
|
case float64:
|
||||||
|
return int64(typed)
|
||||||
|
case json.Number:
|
||||||
|
parsed, _ := typed.Int64()
|
||||||
|
return parsed
|
||||||
|
case string:
|
||||||
|
parsed, _ := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||||
|
return parsed
|
||||||
|
default:
|
||||||
|
parsed, _ := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(typed)), 10, 64)
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func baishunGameBetKey(sysOrigin, gameRoundID string, userID int64) string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
baishunReportBetRedisPrefix,
|
||||||
|
normalizeAdminSysOrigin(sysOrigin),
|
||||||
|
strings.TrimSpace(gameRoundID),
|
||||||
|
strconv.FormatInt(userID, 10),
|
||||||
|
}, ":")
|
||||||
|
}
|
||||||
115
internal/service/baishun/report_test.go
Normal file
115
internal/service/baishun/report_test.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package baishun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
|
||||||
|
"github.com/alicebob/miniredis/v2"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleReportPublishesRegionHighWinAfterStartAndSettle(t *testing.T) {
|
||||||
|
service, db := newTestBaishunService(t)
|
||||||
|
mr := miniredis.RunT(t)
|
||||||
|
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||||
|
t.Cleanup(func() { _ = redisClient.Close() })
|
||||||
|
service.repo.Redis = redisClient
|
||||||
|
broadcaster := &fakeBaishunRegionBroadcaster{}
|
||||||
|
service.SetHighWinBroadcaster(broadcaster)
|
||||||
|
|
||||||
|
expireAt := time.Now().Add(time.Hour)
|
||||||
|
token := "ss-token-1001"
|
||||||
|
if err := db.Create(&model.BaishunLaunchSession{
|
||||||
|
ID: 1,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
RoomID: "9001",
|
||||||
|
UserID: 1001,
|
||||||
|
InternalGameID: "bs_1146",
|
||||||
|
VendorGameID: 1146,
|
||||||
|
GameSessionID: "session-1",
|
||||||
|
SSToken: &token,
|
||||||
|
SSTokenExpireTime: &expireAt,
|
||||||
|
Status: baishunSessionActive,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
UpdateTime: time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed session: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&model.BaishunRoomState{
|
||||||
|
ID: 2,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
RoomID: "9001",
|
||||||
|
HostUserID: 1001,
|
||||||
|
CurrentGameID: "bs_1146",
|
||||||
|
CurrentVendorGameID: intPtr(1146),
|
||||||
|
CurrentGameName: "LordOfOlympus",
|
||||||
|
CurrentGameCover: "https://cdn.example.com/game.png",
|
||||||
|
GameSessionID: "session-1",
|
||||||
|
State: baishunRoomStatePlaying,
|
||||||
|
CreateTime: time.Now(),
|
||||||
|
UpdateTime: time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed room state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
startResp := service.HandleReport(context.Background(), BaishunReportRequest{
|
||||||
|
ReportType: baishunReportTypeStart,
|
||||||
|
UserID: "1001",
|
||||||
|
SSToken: token,
|
||||||
|
ReportMsg: map[string]any{
|
||||||
|
"game_id": 1146,
|
||||||
|
"room_id": "9001",
|
||||||
|
"game_round_id": "round-1",
|
||||||
|
"players": []any{
|
||||||
|
map[string]any{"user_id": "1001", "is_ai": 0, "bet": 100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, `{"report_type":"game_start"}`)
|
||||||
|
if startResp.Code != 0 {
|
||||||
|
t.Fatalf("start response = %+v", startResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
settleResp := service.HandleReport(context.Background(), BaishunReportRequest{
|
||||||
|
ReportType: baishunReportTypeSettle,
|
||||||
|
UserID: "1001",
|
||||||
|
SSToken: token,
|
||||||
|
ReportMsg: map[string]any{
|
||||||
|
"game_id": 1146,
|
||||||
|
"room_id": "9001",
|
||||||
|
"game_round_id": "round-1",
|
||||||
|
"players": []any{
|
||||||
|
map[string]any{"user_id": "1001", "is_ai": 0, "reward": 1000, "rank": 1, "score": 88},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, `{"report_type":"game_settle"}`)
|
||||||
|
if settleResp.Code != 0 {
|
||||||
|
t.Fatalf("settle response = %+v", settleResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(broadcaster.requests) != 1 {
|
||||||
|
t.Fatalf("requests = %d, want 1", len(broadcaster.requests))
|
||||||
|
}
|
||||||
|
req := broadcaster.requests[0]
|
||||||
|
if req.Type != "GAME_BAISHUN_WIN" {
|
||||||
|
t.Fatalf("type = %q, want GAME_BAISHUN_WIN", req.Type)
|
||||||
|
}
|
||||||
|
if req.Data["userId"] != "1001" || req.Data["roomId"] != "9001" ||
|
||||||
|
req.Data["multiple"] != int64(10) || req.Data["winAmount"] != int64(1000) ||
|
||||||
|
req.Data["currencyDiff"] != int64(1000) ||
|
||||||
|
req.Data["gameUrl"] != "https://cdn.example.com/game.png" {
|
||||||
|
t.Fatalf("payload = %+v", req.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeBaishunRegionBroadcaster struct {
|
||||||
|
requests []regionimgroup.RegionBroadcastRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBaishunRegionBroadcaster) SendRegionBroadcast(_ context.Context, req regionimgroup.RegionBroadcastRequest) (*regionimgroup.RegionBroadcastResponse, error) {
|
||||||
|
f.requests = append(f.requests, req)
|
||||||
|
return ®ionimgroup.RegionBroadcastResponse{Type: req.Type}, nil
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package baishun
|
|||||||
import (
|
import (
|
||||||
"chatapp3-golang/internal/model"
|
"chatapp3-golang/internal/model"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -12,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// ListShortcutGames 返回房间里的快捷游戏列表,数量上限为 5 个。
|
// ListShortcutGames 返回房间里的快捷游戏列表,数量上限为 5 个。
|
||||||
func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
||||||
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, "")
|
items, err := s.listRoomGames(ctx, user.SysOrigin, user.RegionID, roomID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -24,7 +25,7 @@ func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, r
|
|||||||
|
|
||||||
// ListRoomGames 返回房间可启动的完整游戏列表。
|
// ListRoomGames 返回房间可启动的完整游戏列表。
|
||||||
func (s *BaishunService) ListRoomGames(ctx context.Context, user AuthUser, roomID string, category string) (*RoomGameListResponse, error) {
|
func (s *BaishunService) ListRoomGames(ctx context.Context, user AuthUser, roomID string, category string) (*RoomGameListResponse, error) {
|
||||||
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, category)
|
items, err := s.listRoomGames(ctx, user.SysOrigin, user.RegionID, roomID, category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -58,13 +59,19 @@ func (s *BaishunService) GetRoomState(ctx context.Context, user AuthUser, roomID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listRoomGames 联表读取房间可启动游戏,并按展示规则排序。
|
// listRoomGames 联表读取房间可启动游戏,并按展示规则排序。
|
||||||
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, category string) ([]RoomGameListItem, error) {
|
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, regionID, roomID, category string) ([]RoomGameListItem, error) {
|
||||||
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var rows []gameListRow
|
var rows []gameListRow
|
||||||
query := s.repo.DB.WithContext(ctx).
|
query := s.repo.DB.WithContext(ctx).
|
||||||
Table("sys_game_list_config AS cfg").
|
Table("sys_game_list_config AS cfg").
|
||||||
Select(`
|
Select(`
|
||||||
cfg.id AS config_id,
|
cfg.id AS config_id,
|
||||||
cfg.sys_origin AS sys_origin,
|
cfg.sys_origin AS sys_origin,
|
||||||
|
ext.profile AS profile,
|
||||||
cfg.game_id AS internal_game_id,
|
cfg.game_id AS internal_game_id,
|
||||||
cfg.name AS name,
|
cfg.name AS name,
|
||||||
cfg.category AS category,
|
cfg.category AS category,
|
||||||
@ -92,9 +99,13 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
|||||||
cat.safe_height AS catalog_safe_height,
|
cat.safe_height AS catalog_safe_height,
|
||||||
cat.status AS catalog_status
|
cat.status AS catalog_status
|
||||||
`).
|
`).
|
||||||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1").
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
|
||||||
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
||||||
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType)
|
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType)
|
||||||
|
if strings.TrimSpace(regionID) != "" {
|
||||||
|
// regions 为空表示全区域;有值时按用户区域 ID 精确匹配,避免土耳其用户看到其他区域游戏。
|
||||||
|
query = query.Where("(COALESCE(TRIM(cfg.regions), '') = '' OR FIND_IN_SET(?, REPLACE(COALESCE(cfg.regions, ''), ' ', '')) > 0)", strings.TrimSpace(regionID))
|
||||||
|
}
|
||||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") {
|
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") {
|
||||||
query = query.Where("cfg.category = ?", category)
|
query = query.Where("cfg.category = ?", category)
|
||||||
}
|
}
|
||||||
@ -111,12 +122,18 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
|||||||
|
|
||||||
// findGameRow 按内部游戏 ID 查询游戏聚合信息。
|
// findGameRow 按内部游戏 ID 查询游戏聚合信息。
|
||||||
func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID string) (gameListRow, error) {
|
func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID string) (gameListRow, error) {
|
||||||
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return gameListRow{}, err
|
||||||
|
}
|
||||||
var row gameListRow
|
var row gameListRow
|
||||||
err := s.repo.DB.WithContext(ctx).
|
err = s.repo.DB.WithContext(ctx).
|
||||||
Table("sys_game_list_config AS cfg").
|
Table("sys_game_list_config AS cfg").
|
||||||
Select(`
|
Select(`
|
||||||
cfg.id AS config_id,
|
cfg.id AS config_id,
|
||||||
cfg.sys_origin AS sys_origin,
|
cfg.sys_origin AS sys_origin,
|
||||||
|
ext.profile AS profile,
|
||||||
cfg.game_id AS internal_game_id,
|
cfg.game_id AS internal_game_id,
|
||||||
cfg.name AS name,
|
cfg.name AS name,
|
||||||
cfg.category AS category,
|
cfg.category AS category,
|
||||||
@ -144,8 +161,8 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
|||||||
cat.safe_height AS catalog_safe_height,
|
cat.safe_height AS catalog_safe_height,
|
||||||
cat.status AS catalog_status
|
cat.status AS catalog_status
|
||||||
`).
|
`).
|
||||||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1").
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
|
||||||
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
||||||
Where("cfg.sys_origin = ? AND cfg.game_id = ? AND ext.vendor_type = ?", sysOrigin, gameID, baishunVendorType).
|
Where("cfg.sys_origin = ? AND cfg.game_id = ? AND ext.vendor_type = ?", sysOrigin, gameID, baishunVendorType).
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Scan(&row).Error
|
Scan(&row).Error
|
||||||
@ -155,17 +172,51 @@ func (s *BaishunService) findGameRow(ctx context.Context, sysOrigin, gameID stri
|
|||||||
if strings.TrimSpace(row.InternalGameID) != "" {
|
if strings.TrimSpace(row.InternalGameID) != "" {
|
||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var catalog model.BaishunGameCatalog
|
||||||
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND profile = ? AND internal_game_id = ?", sysOrigin, profile, gameID).
|
||||||
|
First(&catalog).Error; err == nil {
|
||||||
|
return gameListRow{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Profile: profile,
|
||||||
|
InternalGameID: defaultIfBlank(catalog.InternalGameID, fmt.Sprintf("bs_%d", catalog.VendorGameID)),
|
||||||
|
Name: catalog.Name,
|
||||||
|
Cover: catalog.Cover,
|
||||||
|
VendorType: baishunVendorType,
|
||||||
|
VendorGameID: strconv.Itoa(catalog.VendorGameID),
|
||||||
|
LaunchMode: baishunLaunchModeRemote,
|
||||||
|
ExtGSP: catalog.GSP,
|
||||||
|
CatalogCover: catalog.Cover,
|
||||||
|
CatalogPreviewURL: catalog.PreviewURL,
|
||||||
|
CatalogDownloadURL: catalog.DownloadURL,
|
||||||
|
CatalogPackageVersion: catalog.PackageVersion,
|
||||||
|
CatalogSafeHeight: catalog.SafeHeight,
|
||||||
|
CatalogOrientation: catalog.Orientation,
|
||||||
|
CatalogStatus: catalog.Status,
|
||||||
|
GameModeRaw: formatAdminGameModeRaw(baishunFixedGameMode),
|
||||||
|
}, nil
|
||||||
|
} else if err != gorm.ErrRecordNotFound {
|
||||||
|
return gameListRow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found")
|
return gameListRow{}, NewAppError(http.StatusNotFound, "game_not_found", "room game config not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// findGameRowByConfigID 按系统游戏配置主键查询游戏聚合信息。
|
// findGameRowByConfigID 按系统游戏配置主键查询游戏聚合信息。
|
||||||
func (s *BaishunService) findGameRowByConfigID(ctx context.Context, sysOrigin string, configID int64) (gameListRow, error) {
|
func (s *BaishunService) findGameRowByConfigID(ctx context.Context, sysOrigin string, configID int64) (gameListRow, error) {
|
||||||
|
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||||
|
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return gameListRow{}, err
|
||||||
|
}
|
||||||
var row gameListRow
|
var row gameListRow
|
||||||
err := s.repo.DB.WithContext(ctx).
|
err = s.repo.DB.WithContext(ctx).
|
||||||
Table("sys_game_list_config AS cfg").
|
Table("sys_game_list_config AS cfg").
|
||||||
Select(`
|
Select(`
|
||||||
cfg.id AS config_id,
|
cfg.id AS config_id,
|
||||||
cfg.sys_origin AS sys_origin,
|
cfg.sys_origin AS sys_origin,
|
||||||
|
ext.profile AS profile,
|
||||||
cfg.game_id AS internal_game_id,
|
cfg.game_id AS internal_game_id,
|
||||||
cfg.name AS name,
|
cfg.name AS name,
|
||||||
cfg.category AS category,
|
cfg.category AS category,
|
||||||
@ -193,8 +244,8 @@ func (s *BaishunService) findGameRowByConfigID(ctx context.Context, sysOrigin st
|
|||||||
cat.safe_height AS catalog_safe_height,
|
cat.safe_height AS catalog_safe_height,
|
||||||
cat.status AS catalog_status
|
cat.status AS catalog_status
|
||||||
`).
|
`).
|
||||||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1").
|
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
|
||||||
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
||||||
Where("cfg.sys_origin = ? AND cfg.id = ? AND ext.vendor_type = ?", sysOrigin, configID, baishunVendorType).
|
Where("cfg.sys_origin = ? AND cfg.id = ? AND ext.vendor_type = ?", sysOrigin, configID, baishunVendorType).
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Scan(&row).Error
|
Scan(&row).Error
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package baishun
|
|||||||
import (
|
import (
|
||||||
"chatapp3-golang/internal/config"
|
"chatapp3-golang/internal/config"
|
||||||
"chatapp3-golang/internal/integration"
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/service/highwin"
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -50,6 +51,10 @@ type baishunGateway interface {
|
|||||||
ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error
|
ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type taskEventReporter interface {
|
||||||
|
ReportSimpleEvent(ctx context.Context, sysOrigin, eventID, eventType string, userID, deltaValue int64, payload map[string]any) error
|
||||||
|
}
|
||||||
|
|
||||||
type baishunPorts struct {
|
type baishunPorts struct {
|
||||||
DB baishunDB
|
DB baishunDB
|
||||||
Redis redis.Cmdable
|
Redis redis.Cmdable
|
||||||
@ -61,6 +66,8 @@ type BaishunService struct {
|
|||||||
repo baishunPorts
|
repo baishunPorts
|
||||||
java baishunGateway
|
java baishunGateway
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
taskReporter taskEventReporter
|
||||||
|
highWinBroadcaster highwin.RegionBroadcaster
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBaishunService 创建百顺接入服务实例。
|
// NewBaishunService 创建百顺接入服务实例。
|
||||||
@ -75,6 +82,14 @@ func NewBaishunService(cfg config.Config, db baishunDB, cache redis.Cmdable, jav
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) SetTaskEventReporter(reporter taskEventReporter) {
|
||||||
|
s.taskReporter = reporter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaishunService) SetHighWinBroadcaster(broadcaster highwin.RegionBroadcaster) {
|
||||||
|
s.highWinBroadcaster = broadcaster
|
||||||
|
}
|
||||||
|
|
||||||
// RoomGameListItem 是房间游戏列表中的单个游戏项。
|
// RoomGameListItem 是房间游戏列表中的单个游戏项。
|
||||||
type RoomGameListItem struct {
|
type RoomGameListItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
@ -175,8 +190,10 @@ type BaishunLaunchAppResponse struct {
|
|||||||
// SyncCatalogRequest 是目录同步入参。
|
// SyncCatalogRequest 是目录同步入参。
|
||||||
type SyncCatalogRequest struct {
|
type SyncCatalogRequest struct {
|
||||||
SysOrigin string `json:"sysOrigin"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
VendorGameIDs []int `json:"vendorGameIds"`
|
VendorGameIDs []int `json:"vendorGameIds"`
|
||||||
Force bool `json:"force"`
|
Force bool `json:"force"`
|
||||||
|
DefaultShowcase *bool `json:"defaultShowcase"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncCatalogResponse 是目录同步结果统计。
|
// SyncCatalogResponse 是目录同步结果统计。
|
||||||
@ -190,6 +207,7 @@ type SyncCatalogResponse struct {
|
|||||||
type AdminBaishunGameItem struct {
|
type AdminBaishunGameItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SysOrigin string `json:"sysOrigin"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
GameID string `json:"gameId"`
|
GameID string `json:"gameId"`
|
||||||
GameType string `json:"gameType,omitempty"`
|
GameType string `json:"gameType,omitempty"`
|
||||||
VendorGameID int `json:"vendorGameId"`
|
VendorGameID int `json:"vendorGameId"`
|
||||||
@ -232,6 +250,7 @@ type AdminBaishunGamePageResponse struct {
|
|||||||
type SaveAdminBaishunGameRequest struct {
|
type SaveAdminBaishunGameRequest struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SysOrigin string `json:"sysOrigin"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
GameID string `json:"gameId"`
|
GameID string `json:"gameId"`
|
||||||
VendorGameID int `json:"vendorGameId"`
|
VendorGameID int `json:"vendorGameId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -239,7 +258,7 @@ type SaveAdminBaishunGameRequest struct {
|
|||||||
GameCode string `json:"gameCode"`
|
GameCode string `json:"gameCode"`
|
||||||
Cover string `json:"cover"`
|
Cover string `json:"cover"`
|
||||||
Amounts string `json:"amounts"`
|
Amounts string `json:"amounts"`
|
||||||
Showcase bool `json:"showcase"`
|
Showcase *bool `json:"showcase"`
|
||||||
Sort int64 `json:"sort"`
|
Sort int64 `json:"sort"`
|
||||||
Height float64 `json:"height"`
|
Height float64 `json:"height"`
|
||||||
Width float64 `json:"width"`
|
Width float64 `json:"width"`
|
||||||
@ -277,7 +296,12 @@ type SyncAdminCatalogResponse struct {
|
|||||||
type BaishunProviderConfigItem struct {
|
type BaishunProviderConfigItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SysOrigin string `json:"sysOrigin"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
ActiveProfile string `json:"activeProfile"`
|
||||||
|
Active bool `json:"active"`
|
||||||
PlatformBaseURL string `json:"platformBaseUrl"`
|
PlatformBaseURL string `json:"platformBaseUrl"`
|
||||||
|
GameListURL string `json:"gameListUrl"`
|
||||||
|
LuckyGiftURL string `json:"luckyGiftUrl"`
|
||||||
AppID int64 `json:"appId"`
|
AppID int64 `json:"appId"`
|
||||||
AppName string `json:"appName"`
|
AppName string `json:"appName"`
|
||||||
AppChannel string `json:"appChannel"`
|
AppChannel string `json:"appChannel"`
|
||||||
@ -291,7 +315,10 @@ type BaishunProviderConfigItem struct {
|
|||||||
// SaveBaishunProviderConfigRequest 是后台保存百顺配置的入参。
|
// SaveBaishunProviderConfigRequest 是后台保存百顺配置的入参。
|
||||||
type SaveBaishunProviderConfigRequest struct {
|
type SaveBaishunProviderConfigRequest struct {
|
||||||
SysOrigin string `json:"sysOrigin"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
PlatformBaseURL string `json:"platformBaseUrl"`
|
PlatformBaseURL string `json:"platformBaseUrl"`
|
||||||
|
GameListURL string `json:"gameListUrl"`
|
||||||
|
LuckyGiftURL string `json:"luckyGiftUrl"`
|
||||||
AppID int64 `json:"appId"`
|
AppID int64 `json:"appId"`
|
||||||
AppName string `json:"appName"`
|
AppName string `json:"appName"`
|
||||||
AppChannel string `json:"appChannel"`
|
AppChannel string `json:"appChannel"`
|
||||||
@ -301,9 +328,16 @@ type SaveBaishunProviderConfigRequest struct {
|
|||||||
SSTokenTTLSeconds int `json:"ssTokenTtlSeconds"`
|
SSTokenTTLSeconds int `json:"ssTokenTtlSeconds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActivateBaishunProviderProfileRequest 是切换百顺正式/测试配置档案的入参。
|
||||||
|
type ActivateBaishunProviderProfileRequest struct {
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
}
|
||||||
|
|
||||||
// AdminBaishunCatalogItem 是后台目录页展示的单个百顺目录项。
|
// AdminBaishunCatalogItem 是后台目录页展示的单个百顺目录项。
|
||||||
type AdminBaishunCatalogItem struct {
|
type AdminBaishunCatalogItem struct {
|
||||||
VendorGameID int `json:"vendorGameId"`
|
VendorGameID int `json:"vendorGameId"`
|
||||||
|
Profile string `json:"profile"`
|
||||||
GameID string `json:"gameId"`
|
GameID string `json:"gameId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Cover string `json:"cover"`
|
Cover string `json:"cover"`
|
||||||
@ -390,6 +424,13 @@ type BaishunBalanceInfoRequest struct {
|
|||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BaishunReportRequest struct {
|
||||||
|
ReportType string `json:"report_type"`
|
||||||
|
ReportMsg map[string]any `json:"report_msg"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
SSToken string `json:"ss_token"`
|
||||||
|
}
|
||||||
|
|
||||||
// baishunStandardResponse 是百顺标准回包结构。
|
// baishunStandardResponse 是百顺标准回包结构。
|
||||||
type baishunStandardResponse struct {
|
type baishunStandardResponse struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
@ -410,6 +451,7 @@ type baishunBalanceInfoResponse struct {
|
|||||||
type gameListRow struct {
|
type gameListRow struct {
|
||||||
ConfigID int64
|
ConfigID int64
|
||||||
SysOrigin string
|
SysOrigin string
|
||||||
|
Profile string
|
||||||
InternalGameID string
|
InternalGameID string
|
||||||
Name string
|
Name string
|
||||||
Category string
|
Category string
|
||||||
|
|||||||
78
internal/service/binancerecharge/amount.go
Normal file
78
internal/service/binancerecharge/amount.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package binancerecharge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parsePositiveDecimal(value string, field string) (*big.Rat, string, error) {
|
||||||
|
normalized, err := normalizeDecimal(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", NewAppError(http.StatusBadRequest, "invalid_"+field, field+" must be a decimal number")
|
||||||
|
}
|
||||||
|
rat, ok := new(big.Rat).SetString(normalized)
|
||||||
|
if !ok || rat.Sign() <= 0 {
|
||||||
|
return nil, "", NewAppError(http.StatusBadRequest, "invalid_"+field, field+" must be greater than 0")
|
||||||
|
}
|
||||||
|
return rat, normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNonNegativeDecimal(value string, field string) (*big.Rat, string, error) {
|
||||||
|
normalized, err := normalizeDecimal(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", NewAppError(http.StatusBadRequest, "invalid_"+field, field+" must be a decimal number")
|
||||||
|
}
|
||||||
|
rat, ok := new(big.Rat).SetString(normalized)
|
||||||
|
if !ok || rat.Sign() < 0 {
|
||||||
|
return nil, "", NewAppError(http.StatusBadRequest, "invalid_"+field, field+" must be greater than or equal to 0")
|
||||||
|
}
|
||||||
|
return rat, normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDecimal(value string) (string, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return "", fmt.Errorf("blank decimal")
|
||||||
|
}
|
||||||
|
rat, ok := new(big.Rat).SetString(value)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("invalid decimal")
|
||||||
|
}
|
||||||
|
return trimDecimalZeros(rat.FloatString(8)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimDecimalZeros(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if !strings.Contains(value, ".") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
value = strings.TrimRight(value, "0")
|
||||||
|
value = strings.TrimRight(value, ".")
|
||||||
|
if value == "" || value == "-0" {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func decimalEqual(left string, right string) bool {
|
||||||
|
leftRat, ok := new(big.Rat).SetString(strings.TrimSpace(left))
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rightRat, ok := new(big.Rat).SetString(strings.TrimSpace(right))
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return leftRat.Cmp(rightRat) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateGold(amount *big.Rat, rate *big.Rat) (int64, error) {
|
||||||
|
product := new(big.Rat).Mul(amount, rate)
|
||||||
|
gold := new(big.Int).Quo(product.Num(), product.Denom())
|
||||||
|
if !gold.IsInt64() {
|
||||||
|
return 0, NewAppError(http.StatusBadRequest, "gold_amount_too_large", "gold amount is too large")
|
||||||
|
}
|
||||||
|
return gold.Int64(), nil
|
||||||
|
}
|
||||||
282
internal/service/binancerecharge/binance.go
Normal file
282
internal/service/binancerecharge/binance.go
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
package binancerecharge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultBinanceBaseURL = "https://api.binance.com"
|
||||||
|
binanceUSDT = "USDT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPBinanceVerifier struct {
|
||||||
|
baseURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPBinanceVerifier(baseURL string, timeout time.Duration) *HTTPBinanceVerifier {
|
||||||
|
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = defaultBinanceBaseURL
|
||||||
|
}
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
return &HTTPBinanceVerifier{
|
||||||
|
baseURL: baseURL,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HTTPBinanceVerifier) Verify(ctx context.Context, credential BinanceCredential, query BinanceVerifyQuery) (VerifiedTransaction, error) {
|
||||||
|
if v == nil {
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusServiceUnavailable, "binance_verifier_unavailable", "binance verifier is unavailable")
|
||||||
|
}
|
||||||
|
switch query.TransferType {
|
||||||
|
case TransferTypeBinancePay:
|
||||||
|
return v.verifyPayTransaction(ctx, credential, query)
|
||||||
|
case TransferTypeChain:
|
||||||
|
return v.verifyChainDeposit(ctx, credential, query)
|
||||||
|
default:
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadRequest, "invalid_transfer_type", "transferType must be BINANCE_PAY or CHAIN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HTTPBinanceVerifier) verifyPayTransaction(ctx context.Context, credential BinanceCredential, query BinanceVerifyQuery) (VerifiedTransaction, error) {
|
||||||
|
now := time.Now()
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("startTime", fmt.Sprintf("%d", now.AddDate(0, 0, -90).UnixMilli()))
|
||||||
|
values.Set("endTime", fmt.Sprintf("%d", now.UnixMilli()))
|
||||||
|
values.Set("limit", "100")
|
||||||
|
values.Set("recvWindow", "5000")
|
||||||
|
|
||||||
|
raw, err := v.signedGet(ctx, credential, "/sapi/v1/pay/transactions", values)
|
||||||
|
if err != nil {
|
||||||
|
return VerifiedTransaction{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data []json.RawMessage `json:"data"`
|
||||||
|
Success *bool `json:"success"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &resp); err != nil {
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadGateway, "binance_response_invalid", err.Error())
|
||||||
|
}
|
||||||
|
if resp.Success != nil && !*resp.Success {
|
||||||
|
message := strings.TrimSpace(resp.Message)
|
||||||
|
if message == "" {
|
||||||
|
message = "binance pay query failed"
|
||||||
|
}
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadGateway, "binance_query_failed", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundBill := false
|
||||||
|
for _, item := range resp.Data {
|
||||||
|
var record map[string]any
|
||||||
|
if err := json.Unmarshal(item, &record); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !binancePayRecordMatchesBill(record, query.BillNo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundBill = true
|
||||||
|
if !binancePayRecordMatchesAmount(record, query.Amount) {
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadRequest, "binance_amount_mismatch", "binance transaction amount does not match")
|
||||||
|
}
|
||||||
|
return VerifiedTransaction{
|
||||||
|
TransactionID: firstString(record, "transactionId", "orderId", "merchantTradeNo"),
|
||||||
|
OrderType: firstString(record, "orderType"),
|
||||||
|
Amount: query.Amount,
|
||||||
|
RawJSON: string(item),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if foundBill {
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadRequest, "binance_amount_mismatch", "binance transaction amount does not match")
|
||||||
|
}
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadRequest, "binance_transaction_not_found", "binance transaction was not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HTTPBinanceVerifier) verifyChainDeposit(ctx context.Context, credential BinanceCredential, query BinanceVerifyQuery) (VerifiedTransaction, error) {
|
||||||
|
now := time.Now()
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("coin", binanceUSDT)
|
||||||
|
values.Set("status", "1")
|
||||||
|
values.Set("txId", query.BillNo)
|
||||||
|
values.Set("startTime", fmt.Sprintf("%d", now.AddDate(0, 0, -90).UnixMilli()))
|
||||||
|
values.Set("endTime", fmt.Sprintf("%d", now.UnixMilli()))
|
||||||
|
values.Set("limit", "1000")
|
||||||
|
values.Set("recvWindow", "5000")
|
||||||
|
|
||||||
|
raw, err := v.signedGet(ctx, credential, "/sapi/v1/capital/deposit/hisrec", values)
|
||||||
|
if err != nil {
|
||||||
|
return VerifiedTransaction{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, &rows); err != nil {
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadGateway, "binance_response_invalid", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
foundBill := false
|
||||||
|
for _, item := range rows {
|
||||||
|
var record map[string]any
|
||||||
|
if err := json.Unmarshal(item, &record); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !chainDepositRecordMatchesBill(record, query.BillNo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundBill = true
|
||||||
|
if !strings.EqualFold(firstString(record, "coin"), binanceUSDT) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !chainDepositStatusSuccess(record) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !decimalEqual(firstString(record, "amount"), query.Amount) {
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadRequest, "binance_amount_mismatch", "binance deposit amount does not match")
|
||||||
|
}
|
||||||
|
return VerifiedTransaction{
|
||||||
|
TransactionID: firstString(record, "txId", "id"),
|
||||||
|
OrderType: "CHAIN_DEPOSIT",
|
||||||
|
Amount: query.Amount,
|
||||||
|
RawJSON: string(item),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if foundBill {
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadRequest, "binance_amount_mismatch", "binance deposit amount does not match")
|
||||||
|
}
|
||||||
|
return VerifiedTransaction{}, NewAppError(http.StatusBadRequest, "binance_transaction_not_found", "binance deposit was not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HTTPBinanceVerifier) signedGet(ctx context.Context, credential BinanceCredential, path string, values url.Values) ([]byte, error) {
|
||||||
|
apiKey := strings.TrimSpace(credential.APIKey)
|
||||||
|
apiSecret := strings.TrimSpace(credential.APISecret)
|
||||||
|
if apiKey == "" || apiSecret == "" {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "binance_credentials_missing", "binance api key and secret are required")
|
||||||
|
}
|
||||||
|
values.Set("timestamp", fmt.Sprintf("%d", time.Now().UnixMilli()))
|
||||||
|
query := values.Encode()
|
||||||
|
signature := signBinanceQuery(apiSecret, query)
|
||||||
|
|
||||||
|
endpoint := v.baseURL + path + "?" + query + "&signature=" + url.QueryEscape(signature)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewAppError(http.StatusBadGateway, "binance_request_failed", err.Error())
|
||||||
|
}
|
||||||
|
req.Header.Set("X-MBX-APIKEY", apiKey)
|
||||||
|
|
||||||
|
resp, err := v.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewAppError(http.StatusBadGateway, "binance_request_failed", err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewAppError(http.StatusBadGateway, "binance_response_read_failed", err.Error())
|
||||||
|
}
|
||||||
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||||
|
return nil, NewAppError(http.StatusBadGateway, "binance_query_failed", strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func signBinanceQuery(secret string, query string) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(secret))
|
||||||
|
_, _ = mac.Write([]byte(query))
|
||||||
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func binancePayRecordMatchesBill(record map[string]any, billNo string) bool {
|
||||||
|
billNo = strings.TrimSpace(billNo)
|
||||||
|
for _, key := range []string{"transactionId", "orderId", "merchantTradeNo", "prepayId", "id"} {
|
||||||
|
if strings.EqualFold(firstString(record, key), billNo) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func binancePayRecordMatchesAmount(record map[string]any, expected string) bool {
|
||||||
|
if strings.EqualFold(firstString(record, "currency"), binanceUSDT) && decimalEqual(firstString(record, "amount"), expected) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
details, ok := record["fundsDetail"].([]any)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, item := range details {
|
||||||
|
detail, ok := item.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(firstString(detail, "currency"), binanceUSDT) && decimalEqual(firstString(detail, "amount"), expected) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainDepositRecordMatchesBill(record map[string]any, billNo string) bool {
|
||||||
|
billNo = strings.TrimSpace(billNo)
|
||||||
|
for _, key := range []string{"txId", "id"} {
|
||||||
|
if strings.EqualFold(firstString(record, key), billNo) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func chainDepositStatusSuccess(record map[string]any) bool {
|
||||||
|
switch value := record["status"].(type) {
|
||||||
|
case float64:
|
||||||
|
return int(value) == 1
|
||||||
|
case int:
|
||||||
|
return value == 1
|
||||||
|
case string:
|
||||||
|
return strings.TrimSpace(value) == "1"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstString(record map[string]any, keys ...string) string {
|
||||||
|
for _, key := range keys {
|
||||||
|
if value := stringValue(record[key]); strings.TrimSpace(value) != "" {
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringValue(value any) string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
return typed
|
||||||
|
case json.Number:
|
||||||
|
return typed.String()
|
||||||
|
case float64:
|
||||||
|
return trimDecimalZeros(fmt.Sprintf("%.8f", typed))
|
||||||
|
case int:
|
||||||
|
return fmt.Sprintf("%d", typed)
|
||||||
|
case int64:
|
||||||
|
return fmt.Sprintf("%d", typed)
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
220
internal/service/binancerecharge/config.go
Normal file
220
internal/service/binancerecharge/config.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package binancerecharge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigResponse, error) {
|
||||||
|
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||||
|
if sysOrigin == "" {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "sys_origin_required", "sysOrigin is required")
|
||||||
|
}
|
||||||
|
if s.db == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg model.BinanceRechargeConfig
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
First(&cfg).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &ConfigResponse{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Enabled: true,
|
||||||
|
Rates: []RateConfigResponse{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewAppError(http.StatusInternalServerError, "config_query_failed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
rates, err := s.listRates(ctx, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ConfigResponse{
|
||||||
|
SysOrigin: cfg.SysOrigin,
|
||||||
|
APIKey: cfg.APIKey,
|
||||||
|
APISecretConfigured: strings.TrimSpace(cfg.APISecret) != "",
|
||||||
|
Enabled: cfg.Enabled,
|
||||||
|
Rates: rates,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
|
||||||
|
sysOrigin := normalizeSysOrigin(req.SysOrigin)
|
||||||
|
if sysOrigin == "" {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "sys_origin_required", "sysOrigin is required")
|
||||||
|
}
|
||||||
|
if s.db == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey := strings.TrimSpace(req.APIKey)
|
||||||
|
apiSecret := strings.TrimSpace(req.APISecret)
|
||||||
|
rates, err := validateRateRequests(req.Rates)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg model.BinanceRechargeConfig
|
||||||
|
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
now := time.Now()
|
||||||
|
var existing model.BinanceRechargeConfig
|
||||||
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
First(&existing).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if apiSecret == "" && existing.ID > 0 {
|
||||||
|
apiSecret = existing.APISecret
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
return NewAppError(http.StatusBadRequest, "api_key_required", "apiKey is required")
|
||||||
|
}
|
||||||
|
if apiSecret == "" {
|
||||||
|
return NewAppError(http.StatusBadRequest, "api_secret_required", "apiSecret is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = model.BinanceRechargeConfig{
|
||||||
|
ID: existing.ID,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
APIKey: apiKey,
|
||||||
|
APISecret: apiSecret,
|
||||||
|
Enabled: req.Enabled,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}
|
||||||
|
if existing.ID > 0 {
|
||||||
|
cfg.CreateTime = existing.CreateTime
|
||||||
|
if err := tx.Model(&model.BinanceRechargeConfig{}).
|
||||||
|
Where("id = ?", existing.ID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"api_key": apiKey,
|
||||||
|
"api_secret": apiSecret,
|
||||||
|
"enabled": req.Enabled,
|
||||||
|
"update_time": now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := tx.Create(&cfg).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Where("config_id = ?", cfg.ID).Delete(&model.BinanceRechargeRate{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rows := make([]model.BinanceRechargeRate, 0, len(rates))
|
||||||
|
for index, item := range rates {
|
||||||
|
rows = append(rows, model.BinanceRechargeRate{
|
||||||
|
ConfigID: cfg.ID,
|
||||||
|
MinAmount: item.MinAmount,
|
||||||
|
MaxAmount: item.MaxAmount,
|
||||||
|
GoldPerUSD: item.GoldPerUSD,
|
||||||
|
SortOrder: index + 1,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return tx.Create(&rows).Error
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var appErr *AppError
|
||||||
|
if errors.As(err, &appErr) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, NewAppError(http.StatusInternalServerError, "config_save_failed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetConfig(ctx, sysOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) listRates(ctx context.Context, configID int64) ([]RateConfigResponse, error) {
|
||||||
|
var rows []model.BinanceRechargeRate
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("config_id = ?", configID).
|
||||||
|
Order("sort_order asc, min_amount asc, id asc").
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
return nil, NewAppError(http.StatusInternalServerError, "rate_query_failed", err.Error())
|
||||||
|
}
|
||||||
|
resp := make([]RateConfigResponse, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
resp = append(resp, RateConfigResponse{
|
||||||
|
ID: row.ID,
|
||||||
|
MinAmount: trimDecimalZeros(row.MinAmount),
|
||||||
|
MaxAmount: trimDecimalZeros(row.MaxAmount),
|
||||||
|
GoldPerUSD: trimDecimalZeros(row.GoldPerUSD),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRateRequests(input []RateConfigRequest) ([]RateConfigResponse, error) {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "rates_required", "at least one rate range is required")
|
||||||
|
}
|
||||||
|
rates := make([]RateConfigResponse, 0, len(input))
|
||||||
|
for _, item := range input {
|
||||||
|
minRat, minAmount, err := parseNonNegativeDecimal(item.MinAmount, "minAmount")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
maxRat, maxAmount, err := parseNonNegativeDecimal(defaultZero(item.MaxAmount), "maxAmount")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, goldPerUSD, err := parsePositiveDecimal(item.GoldPerUSD, "goldPerUsd")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if maxRat.Sign() > 0 && maxRat.Cmp(minRat) <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_rate_range", "maxAmount must be greater than minAmount")
|
||||||
|
}
|
||||||
|
rates = append(rates, RateConfigResponse{
|
||||||
|
MinAmount: minAmount,
|
||||||
|
MaxAmount: maxAmount,
|
||||||
|
GoldPerUSD: goldPerUSD,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.SliceStable(rates, func(i, j int) bool {
|
||||||
|
left, _ := new(big.Rat).SetString(rates[i].MinAmount)
|
||||||
|
right, _ := new(big.Rat).SetString(rates[j].MinAmount)
|
||||||
|
return left.Cmp(right) < 0
|
||||||
|
})
|
||||||
|
for i := 0; i < len(rates); i++ {
|
||||||
|
currentMax, _ := new(big.Rat).SetString(rates[i].MaxAmount)
|
||||||
|
if currentMax.Sign() == 0 {
|
||||||
|
if i != len(rates)-1 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_rate_range", "open-ended rate range must be the last range")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i+1 < len(rates) {
|
||||||
|
nextMin, _ := new(big.Rat).SetString(rates[i+1].MinAmount)
|
||||||
|
if currentMax.Cmp(nextMin) > 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_rate_range", "rate ranges cannot overlap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultZero(value string) string {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
201
internal/service/binancerecharge/service_test.go
Normal file
201
internal/service/binancerecharge/service_test.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package binancerecharge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSaveConfigKeepsExistingSecretWhenBlank(t *testing.T) {
|
||||||
|
db := newBinanceRechargeTestDB(t)
|
||||||
|
service := NewService(config.Config{}, db, &fakeBinanceJavaGateway{})
|
||||||
|
|
||||||
|
if _, err := service.SaveConfig(context.Background(), SaveConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
APIKey: "key-1",
|
||||||
|
APISecret: "secret-1",
|
||||||
|
Enabled: true,
|
||||||
|
Rates: []RateConfigRequest{
|
||||||
|
{MinAmount: "0", MaxAmount: "100", GoldPerUSD: "80000"},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("initial save config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.SaveConfig(context.Background(), SaveConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
APIKey: "key-2",
|
||||||
|
Enabled: true,
|
||||||
|
Rates: []RateConfigRequest{
|
||||||
|
{MinAmount: "0", MaxAmount: "0", GoldPerUSD: "82000"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("update config: %v", err)
|
||||||
|
}
|
||||||
|
if resp.APIKey != "key-2" {
|
||||||
|
t.Fatalf("APIKey = %q, want key-2", resp.APIKey)
|
||||||
|
}
|
||||||
|
if !resp.APISecretConfigured {
|
||||||
|
t.Fatalf("APISecretConfigured = false, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
var row model.BinanceRechargeConfig
|
||||||
|
if err := db.Where("sys_origin = ?", "LIKEI").First(&row).Error; err != nil {
|
||||||
|
t.Fatalf("query config: %v", err)
|
||||||
|
}
|
||||||
|
if row.APISecret != "secret-1" {
|
||||||
|
t.Fatalf("APISecret = %q, want secret-1", row.APISecret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyAndRechargeIsIdempotent(t *testing.T) {
|
||||||
|
db := newBinanceRechargeTestDB(t)
|
||||||
|
java := &fakeBinanceJavaGateway{
|
||||||
|
profile: integration.UserProfile{
|
||||||
|
ID: integration.Int64Value(200),
|
||||||
|
OriginSys: "LIKEI",
|
||||||
|
},
|
||||||
|
balanceExists: true,
|
||||||
|
}
|
||||||
|
service := NewService(config.Config{}, db, java)
|
||||||
|
service.SetBinanceVerifier(&fakeBinanceVerifier{})
|
||||||
|
|
||||||
|
if _, err := service.SaveConfig(context.Background(), SaveConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
APIKey: "key",
|
||||||
|
APISecret: "secret",
|
||||||
|
Enabled: true,
|
||||||
|
Rates: []RateConfigRequest{
|
||||||
|
{MinAmount: "0", MaxAmount: "100", GoldPerUSD: "80000"},
|
||||||
|
{MinAmount: "100", MaxAmount: "0", GoldPerUSD: "82000"},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("save config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := VerifyRechargeRequest{
|
||||||
|
TargetUserID: 200,
|
||||||
|
OrderNo: "order-1",
|
||||||
|
TransferAmount: "12.5",
|
||||||
|
TransferType: TransferTypeBinancePay,
|
||||||
|
BillNo: "bill-1",
|
||||||
|
}
|
||||||
|
resp, err := service.VerifyAndRecharge(context.Background(), AuthUser{
|
||||||
|
UserID: 100,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verify recharge: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Idempotent {
|
||||||
|
t.Fatalf("first response idempotent = true, want false")
|
||||||
|
}
|
||||||
|
if resp.GoldAmount != 1_000_000 {
|
||||||
|
t.Fatalf("GoldAmount = %d, want 1000000", resp.GoldAmount)
|
||||||
|
}
|
||||||
|
if java.changeCalls != 1 {
|
||||||
|
t.Fatalf("changeCalls = %d, want 1", java.changeCalls)
|
||||||
|
}
|
||||||
|
if java.lastChange.RechargeType != rechargeTypeUSDT {
|
||||||
|
t.Fatalf("RechargeType = %q, want %q", java.lastChange.RechargeType, rechargeTypeUSDT)
|
||||||
|
}
|
||||||
|
if java.lastChange.AcceptAmount != "1000000" {
|
||||||
|
t.Fatalf("AcceptAmount = %q, want 1000000", java.lastChange.AcceptAmount)
|
||||||
|
}
|
||||||
|
if java.lastChange.OrderAmount != "12.5" {
|
||||||
|
t.Fatalf("OrderAmount = %q, want 12.5", java.lastChange.OrderAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = service.VerifyAndRecharge(context.Background(), AuthUser{
|
||||||
|
UserID: 100,
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
}, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verify idempotent recharge: %v", err)
|
||||||
|
}
|
||||||
|
if !resp.Idempotent {
|
||||||
|
t.Fatalf("second response idempotent = false, want true")
|
||||||
|
}
|
||||||
|
if java.changeCalls != 1 {
|
||||||
|
t.Fatalf("changeCalls after idempotent call = %d, want 1", java.changeCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := db.Model(&model.BinanceRechargeVerifyRecord{}).Where("status = ?", verifyStatusSuccess).Count(&count).Error; err != nil {
|
||||||
|
t.Fatalf("count success records: %v", err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatalf("success record count = %d, want 1", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBinanceRechargeTestDB(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.BinanceRechargeConfig{},
|
||||||
|
&model.BinanceRechargeRate{},
|
||||||
|
&model.BinanceRechargeVerifyRecord{},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeBinanceVerifier struct{}
|
||||||
|
|
||||||
|
func (f *fakeBinanceVerifier) Verify(_ context.Context, _ BinanceCredential, query BinanceVerifyQuery) (VerifiedTransaction, error) {
|
||||||
|
return VerifiedTransaction{
|
||||||
|
TransactionID: query.BillNo,
|
||||||
|
OrderType: query.TransferType,
|
||||||
|
Amount: query.Amount,
|
||||||
|
RawJSON: `{"transactionId":"` + query.BillNo + `"}`,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeBinanceJavaGateway struct {
|
||||||
|
profile integration.UserProfile
|
||||||
|
balanceExists bool
|
||||||
|
sellerExists bool
|
||||||
|
changeCalls int
|
||||||
|
lastChange integration.FreightBalanceChangeRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBinanceJavaGateway) GetUserProfile(_ context.Context, userID int64) (integration.UserProfile, error) {
|
||||||
|
if int64(f.profile.ID) == 0 {
|
||||||
|
f.profile.ID = integration.Int64Value(userID)
|
||||||
|
}
|
||||||
|
return f.profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBinanceJavaGateway) ExistsFreightBalance(context.Context, int64) (bool, error) {
|
||||||
|
return f.balanceExists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBinanceJavaGateway) ExistsFreightSeller(context.Context, int64) (bool, error) {
|
||||||
|
return f.sellerExists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBinanceJavaGateway) ChangeFreightBalance(_ context.Context, req integration.FreightBalanceChangeRequest) error {
|
||||||
|
f.changeCalls++
|
||||||
|
f.lastChange = req
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBinanceJavaGateway) AddFreightAgentReview(context.Context, integration.FreightAgentReviewRequest) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBinanceJavaGateway) IncreaseFreightRechargeRecord(context.Context, int64, string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
150
internal/service/binancerecharge/types.go
Normal file
150
internal/service/binancerecharge/types.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package binancerecharge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransferTypeBinancePay = "BINANCE_PAY"
|
||||||
|
TransferTypeChain = "CHAIN"
|
||||||
|
|
||||||
|
verifyStatusPending = "PENDING"
|
||||||
|
verifyStatusSuccess = "SUCCESS"
|
||||||
|
verifyStatusFailed = "FAILED"
|
||||||
|
|
||||||
|
rechargeTypeUSDT = "USDT-进货"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppError = common.AppError
|
||||||
|
type AuthUser = common.AuthUser
|
||||||
|
|
||||||
|
var NewAppError = common.NewAppError
|
||||||
|
|
||||||
|
type binanceRechargeDB interface {
|
||||||
|
WithContext(ctx context.Context) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type javaGateway interface {
|
||||||
|
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
||||||
|
ExistsFreightBalance(ctx context.Context, userID int64) (bool, error)
|
||||||
|
ExistsFreightSeller(ctx context.Context, userID int64) (bool, error)
|
||||||
|
ChangeFreightBalance(ctx context.Context, req integration.FreightBalanceChangeRequest) error
|
||||||
|
AddFreightAgentReview(ctx context.Context, req integration.FreightAgentReviewRequest) error
|
||||||
|
IncreaseFreightRechargeRecord(ctx context.Context, userID int64, amount string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type binanceVerifier interface {
|
||||||
|
Verify(ctx context.Context, credential BinanceCredential, query BinanceVerifyQuery) (VerifiedTransaction, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
cfg config.Config
|
||||||
|
db binanceRechargeDB
|
||||||
|
java javaGateway
|
||||||
|
verifier binanceVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(cfg config.Config, db binanceRechargeDB, java javaGateway) *Service {
|
||||||
|
return &Service{
|
||||||
|
cfg: cfg,
|
||||||
|
db: db,
|
||||||
|
java: java,
|
||||||
|
verifier: NewHTTPBinanceVerifier(cfg.BinanceRecharge.BaseURL, cfg.HTTP.Timeout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetBinanceVerifier(verifier binanceVerifier) {
|
||||||
|
s.verifier = verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateConfigRequest struct {
|
||||||
|
MinAmount string `json:"minAmount"`
|
||||||
|
MaxAmount string `json:"maxAmount"`
|
||||||
|
GoldPerUSD string `json:"goldPerUsd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SaveConfigRequest struct {
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
|
APISecret string `json:"apiSecret"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Rates []RateConfigRequest `json:"rates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigResponse struct {
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
|
APISecretConfigured bool `json:"apiSecretConfigured"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Rates []RateConfigResponse `json:"rates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateConfigResponse struct {
|
||||||
|
ID int64 `json:"id,string,omitempty"`
|
||||||
|
MinAmount string `json:"minAmount"`
|
||||||
|
MaxAmount string `json:"maxAmount"`
|
||||||
|
GoldPerUSD string `json:"goldPerUsd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifyRechargeRequest struct {
|
||||||
|
TargetUserID int64 `json:"targetUserId,string"`
|
||||||
|
OrderNo string `json:"orderNo"`
|
||||||
|
TransferAmount string `json:"transferAmount"`
|
||||||
|
TransferType string `json:"transferType"`
|
||||||
|
BillNo string `json:"billNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifyRechargeResponse struct {
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
DealerUserID int64 `json:"dealerUserId,string"`
|
||||||
|
TargetUserID int64 `json:"targetUserId,string"`
|
||||||
|
OrderNo string `json:"orderNo"`
|
||||||
|
TransferType string `json:"transferType"`
|
||||||
|
BillNo string `json:"billNo"`
|
||||||
|
TransferAmount string `json:"transferAmount"`
|
||||||
|
GoldAmount int64 `json:"goldAmount,string"`
|
||||||
|
RateGoldPerUSD string `json:"rateGoldPerUsd"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Idempotent bool `json:"idempotent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BinanceCredential struct {
|
||||||
|
APIKey string
|
||||||
|
APISecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BinanceVerifyQuery struct {
|
||||||
|
TransferType string
|
||||||
|
BillNo string
|
||||||
|
Amount string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifiedTransaction struct {
|
||||||
|
TransactionID string
|
||||||
|
OrderType string
|
||||||
|
Amount string
|
||||||
|
RawJSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSysOrigin(value string) string {
|
||||||
|
return strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTransferType(value string) (string, error) {
|
||||||
|
normalized := strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
switch normalized {
|
||||||
|
case TransferTypeBinancePay, TransferTypeChain:
|
||||||
|
return normalized, nil
|
||||||
|
default:
|
||||||
|
return "", NewAppError(http.StatusBadRequest, "invalid_transfer_type", "transferType must be BINANCE_PAY or CHAIN")
|
||||||
|
}
|
||||||
|
}
|
||||||
432
internal/service/binancerecharge/verify.go
Normal file
432
internal/service/binancerecharge/verify.go
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
package binancerecharge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pendingRetryAfter = 10 * time.Minute
|
||||||
|
|
||||||
|
var errDuplicateRechargeRecord = errors.New("duplicate binance recharge record")
|
||||||
|
|
||||||
|
type preparedRecharge struct {
|
||||||
|
SysOrigin string
|
||||||
|
DealerUserID int64
|
||||||
|
TargetUserID int64
|
||||||
|
OrderNo string
|
||||||
|
TransferType string
|
||||||
|
BillNo string
|
||||||
|
AmountUSDT string
|
||||||
|
GoldAmount int64
|
||||||
|
RateGoldPerUSD string
|
||||||
|
Verified VerifiedTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) VerifyAndRecharge(ctx context.Context, user AuthUser, req VerifyRechargeRequest) (*VerifyRechargeResponse, error) {
|
||||||
|
if s.db == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
|
||||||
|
}
|
||||||
|
if s.java == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "java_gateway_unavailable", "java gateway is unavailable")
|
||||||
|
}
|
||||||
|
if s.verifier == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "binance_verifier_unavailable", "binance verifier is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
sysOrigin := normalizeSysOrigin(user.SysOrigin)
|
||||||
|
if sysOrigin == "" {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "sys_origin_required", "sysOrigin is required")
|
||||||
|
}
|
||||||
|
if user.UserID <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusUnauthorized, "invalid_user", "user is invalid")
|
||||||
|
}
|
||||||
|
if req.TargetUserID <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "target_user_required", "targetUserId is required")
|
||||||
|
}
|
||||||
|
orderNo := strings.TrimSpace(req.OrderNo)
|
||||||
|
if orderNo == "" {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "order_no_required", "orderNo is required")
|
||||||
|
}
|
||||||
|
billNo := strings.TrimSpace(req.BillNo)
|
||||||
|
if billNo == "" {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "bill_no_required", "billNo is required")
|
||||||
|
}
|
||||||
|
transferType, err := validateTransferType(req.TransferType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
amountRat, amount, err := parsePositiveDecimal(req.TransferAmount, "transferAmount")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, rates, err := s.loadConfigAndRates(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rateRat, rateGoldPerUSD, err := matchRate(amountRat, rates)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
goldAmount, err := calculateGold(amountRat, rateRat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if goldAmount <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "gold_amount_zero", "calculated gold amount must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := s.java.GetUserProfile(ctx, req.TargetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewAppError(http.StatusBadGateway, "target_user_query_failed", err.Error())
|
||||||
|
}
|
||||||
|
if normalizeSysOrigin(profile.OriginSys) != sysOrigin {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "target_user_origin_mismatch", "target user does not belong to current sysOrigin")
|
||||||
|
}
|
||||||
|
|
||||||
|
verified, err := s.verifier.Verify(ctx, BinanceCredential{
|
||||||
|
APIKey: cfg.APIKey,
|
||||||
|
APISecret: cfg.APISecret,
|
||||||
|
}, BinanceVerifyQuery{
|
||||||
|
TransferType: transferType,
|
||||||
|
BillNo: billNo,
|
||||||
|
Amount: amount,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(verified.TransactionID) == "" {
|
||||||
|
verified.TransactionID = billNo
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(verified.Amount) == "" {
|
||||||
|
verified.Amount = amount
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared := preparedRecharge{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
DealerUserID: user.UserID,
|
||||||
|
TargetUserID: req.TargetUserID,
|
||||||
|
OrderNo: orderNo,
|
||||||
|
TransferType: transferType,
|
||||||
|
BillNo: billNo,
|
||||||
|
AmountUSDT: amount,
|
||||||
|
GoldAmount: goldAmount,
|
||||||
|
RateGoldPerUSD: rateGoldPerUSD,
|
||||||
|
Verified: verified,
|
||||||
|
}
|
||||||
|
record, idempotent, err := s.prepareRechargeRecord(ctx, prepared)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if idempotent {
|
||||||
|
resp := responseFromRecord(record, true)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureFreightAccount(ctx, sysOrigin, req.TargetUserID); err != nil {
|
||||||
|
_ = s.markRecordFailed(ctx, record.ID, err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.java.ChangeFreightBalance(ctx, integration.FreightBalanceChangeRequest{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
UserID: req.TargetUserID,
|
||||||
|
AcceptUserID: req.TargetUserID,
|
||||||
|
Type: "INCOME",
|
||||||
|
AcceptAmount: strconv.FormatInt(goldAmount, 10),
|
||||||
|
OrderAmount: amount,
|
||||||
|
USDAmount: "0",
|
||||||
|
Origin: "PURCHASE",
|
||||||
|
Remark: fmt.Sprintf("Binance auto recharge order:%s bill:%s", orderNo, billNo),
|
||||||
|
RechargeType: rechargeTypeUSDT,
|
||||||
|
CreateUser: user.UserID,
|
||||||
|
}); err != nil {
|
||||||
|
_ = s.markRecordPendingIssue(ctx, record.ID, "freight balance change result is unknown: "+err.Error())
|
||||||
|
return nil, NewAppError(http.StatusBadGateway, "freight_balance_change_failed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
remark := ""
|
||||||
|
failReason := ""
|
||||||
|
if err := s.java.IncreaseFreightRechargeRecord(ctx, req.TargetUserID, amount); err != nil {
|
||||||
|
failReason = truncateString("monthly freight recharge record update failed: "+err.Error(), 500)
|
||||||
|
remark = failReason
|
||||||
|
}
|
||||||
|
if err := s.markRecordSuccess(ctx, record.ID, remark, failReason); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Status = verifyStatusSuccess
|
||||||
|
record.Remark = remark
|
||||||
|
record.FailReason = failReason
|
||||||
|
resp := responseFromRecord(record, false)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadConfigAndRates(ctx context.Context, sysOrigin string) (model.BinanceRechargeConfig, []model.BinanceRechargeRate, error) {
|
||||||
|
var cfg model.BinanceRechargeConfig
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
First(&cfg).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return cfg, nil, NewAppError(http.StatusBadRequest, "binance_config_missing", "binance recharge config is missing")
|
||||||
|
}
|
||||||
|
return cfg, nil, NewAppError(http.StatusInternalServerError, "binance_config_query_failed", err.Error())
|
||||||
|
}
|
||||||
|
if !cfg.Enabled {
|
||||||
|
return cfg, nil, NewAppError(http.StatusBadRequest, "binance_config_disabled", "binance recharge verification is disabled")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.APIKey) == "" || strings.TrimSpace(cfg.APISecret) == "" {
|
||||||
|
return cfg, nil, NewAppError(http.StatusBadRequest, "binance_credentials_missing", "binance api key and secret are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rates []model.BinanceRechargeRate
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("config_id = ?", cfg.ID).
|
||||||
|
Order("sort_order asc, min_amount asc, id asc").
|
||||||
|
Find(&rates).Error; err != nil {
|
||||||
|
return cfg, nil, NewAppError(http.StatusInternalServerError, "binance_rate_query_failed", err.Error())
|
||||||
|
}
|
||||||
|
if len(rates) == 0 {
|
||||||
|
return cfg, nil, NewAppError(http.StatusBadRequest, "binance_rates_missing", "binance recharge rates are missing")
|
||||||
|
}
|
||||||
|
return cfg, rates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchRate(amount *big.Rat, rates []model.BinanceRechargeRate) (*big.Rat, string, error) {
|
||||||
|
for _, row := range rates {
|
||||||
|
minRat, _, err := parseNonNegativeDecimal(row.MinAmount, "minAmount")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
maxRat, _, err := parseNonNegativeDecimal(defaultZero(row.MaxAmount), "maxAmount")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
rateRat, rateGoldPerUSD, err := parsePositiveDecimal(row.GoldPerUSD, "goldPerUsd")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if amount.Cmp(minRat) < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if maxRat.Sign() > 0 && amount.Cmp(maxRat) >= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return rateRat, rateGoldPerUSD, nil
|
||||||
|
}
|
||||||
|
return nil, "", NewAppError(http.StatusBadRequest, "binance_rate_not_matched", "transfer amount does not match any configured rate range")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) prepareRechargeRecord(ctx context.Context, prepared preparedRecharge) (model.BinanceRechargeVerifyRecord, bool, error) {
|
||||||
|
var record model.BinanceRechargeVerifyRecord
|
||||||
|
var idempotent bool
|
||||||
|
var err error
|
||||||
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
|
record, idempotent, err = s.prepareRechargeRecordOnce(ctx, prepared)
|
||||||
|
if errors.Is(err, errDuplicateRechargeRecord) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return record, idempotent, err
|
||||||
|
}
|
||||||
|
return record, idempotent, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) prepareRechargeRecordOnce(ctx context.Context, prepared preparedRecharge) (model.BinanceRechargeVerifyRecord, bool, error) {
|
||||||
|
var out model.BinanceRechargeVerifyRecord
|
||||||
|
var idempotent bool
|
||||||
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var existing []model.BinanceRechargeVerifyRecord
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Where("sys_origin = ? AND (order_no = ? OR (transfer_type = ? AND bill_no = ?))",
|
||||||
|
prepared.SysOrigin,
|
||||||
|
prepared.OrderNo,
|
||||||
|
prepared.TransferType,
|
||||||
|
prepared.BillNo,
|
||||||
|
).
|
||||||
|
Limit(2).
|
||||||
|
Find(&existing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(existing) > 1 {
|
||||||
|
return NewAppError(http.StatusConflict, "recharge_order_or_bill_used", "orderNo or billNo has already been used")
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
if len(existing) == 1 {
|
||||||
|
record := existing[0]
|
||||||
|
switch record.Status {
|
||||||
|
case verifyStatusSuccess:
|
||||||
|
out = record
|
||||||
|
idempotent = true
|
||||||
|
return nil
|
||||||
|
case verifyStatusPending:
|
||||||
|
if now.Sub(record.UpdateTime) < pendingRetryAfter {
|
||||||
|
return NewAppError(http.StatusConflict, "recharge_verification_processing", "recharge verification is processing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updates := recordValues(prepared, now, verifyStatusPending)
|
||||||
|
if err := tx.Model(&model.BinanceRechargeVerifyRecord{}).
|
||||||
|
Where("id = ?", record.ID).
|
||||||
|
Updates(updates).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("id = ?", record.ID).First(&out).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
record := model.BinanceRechargeVerifyRecord{
|
||||||
|
SysOrigin: prepared.SysOrigin,
|
||||||
|
DealerUserID: prepared.DealerUserID,
|
||||||
|
TargetUserID: prepared.TargetUserID,
|
||||||
|
OrderNo: prepared.OrderNo,
|
||||||
|
TransferType: prepared.TransferType,
|
||||||
|
BillNo: prepared.BillNo,
|
||||||
|
AmountUSDT: prepared.AmountUSDT,
|
||||||
|
GoldAmount: prepared.GoldAmount,
|
||||||
|
RateGoldPerUSD: prepared.RateGoldPerUSD,
|
||||||
|
BinanceTransactionID: prepared.Verified.TransactionID,
|
||||||
|
BinanceOrderType: prepared.Verified.OrderType,
|
||||||
|
BinanceRawJSON: prepared.Verified.RawJSON,
|
||||||
|
Status: verifyStatusPending,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&record).Error; err != nil {
|
||||||
|
if isDuplicateRecordError(err) {
|
||||||
|
return errDuplicateRechargeRecord
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out = record
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var appErr *AppError
|
||||||
|
if errors.As(err, &appErr) {
|
||||||
|
return out, idempotent, err
|
||||||
|
}
|
||||||
|
return out, idempotent, NewAppError(http.StatusInternalServerError, "recharge_record_prepare_failed", err.Error())
|
||||||
|
}
|
||||||
|
return out, idempotent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordValues(prepared preparedRecharge, now time.Time, status string) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"dealer_user_id": prepared.DealerUserID,
|
||||||
|
"target_user_id": prepared.TargetUserID,
|
||||||
|
"order_no": prepared.OrderNo,
|
||||||
|
"transfer_type": prepared.TransferType,
|
||||||
|
"bill_no": prepared.BillNo,
|
||||||
|
"amount_usdt": prepared.AmountUSDT,
|
||||||
|
"gold_amount": prepared.GoldAmount,
|
||||||
|
"rate_gold_per_usd": prepared.RateGoldPerUSD,
|
||||||
|
"binance_transaction_id": prepared.Verified.TransactionID,
|
||||||
|
"binance_order_type": prepared.Verified.OrderType,
|
||||||
|
"binance_raw_json": prepared.Verified.RawJSON,
|
||||||
|
"status": status,
|
||||||
|
"fail_reason": "",
|
||||||
|
"remark": "",
|
||||||
|
"update_time": now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureFreightAccount(ctx context.Context, sysOrigin string, targetUserID int64) error {
|
||||||
|
exists, err := s.java.ExistsFreightBalance(ctx, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return NewAppError(http.StatusBadGateway, "freight_balance_query_failed", err.Error())
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
existsSeller, err := s.java.ExistsFreightSeller(ctx, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return NewAppError(http.StatusBadGateway, "freight_seller_query_failed", err.Error())
|
||||||
|
}
|
||||||
|
if existsSeller {
|
||||||
|
return NewAppError(http.StatusConflict, "freight_seller_exists_without_balance", "target user is already a freight seller without balance account")
|
||||||
|
}
|
||||||
|
if err := s.java.AddFreightAgentReview(ctx, integration.FreightAgentReviewRequest{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
UserID: targetUserID,
|
||||||
|
}); err != nil {
|
||||||
|
return NewAppError(http.StatusBadGateway, "freight_agent_create_failed", err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) markRecordFailed(ctx context.Context, id int64, reason string) error {
|
||||||
|
return s.updateRecordStatus(ctx, id, verifyStatusFailed, "", truncateString(reason, 500))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) markRecordPendingIssue(ctx context.Context, id int64, reason string) error {
|
||||||
|
return s.updateRecordStatus(ctx, id, verifyStatusPending, "", truncateString(reason, 500))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) markRecordSuccess(ctx context.Context, id int64, remark string, failReason string) error {
|
||||||
|
return s.updateRecordStatus(ctx, id, verifyStatusSuccess, truncateString(remark, 500), truncateString(failReason, 500))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateRecordStatus(ctx context.Context, id int64, status string, remark string, failReason string) error {
|
||||||
|
if id <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := s.db.WithContext(ctx).Model(&model.BinanceRechargeVerifyRecord{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"remark": remark,
|
||||||
|
"fail_reason": failReason,
|
||||||
|
"update_time": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return NewAppError(http.StatusInternalServerError, "recharge_record_update_failed", err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func responseFromRecord(record model.BinanceRechargeVerifyRecord, idempotent bool) VerifyRechargeResponse {
|
||||||
|
return VerifyRechargeResponse{
|
||||||
|
ID: record.ID,
|
||||||
|
SysOrigin: record.SysOrigin,
|
||||||
|
DealerUserID: record.DealerUserID,
|
||||||
|
TargetUserID: record.TargetUserID,
|
||||||
|
OrderNo: record.OrderNo,
|
||||||
|
TransferType: record.TransferType,
|
||||||
|
BillNo: record.BillNo,
|
||||||
|
TransferAmount: trimDecimalZeros(record.AmountUSDT),
|
||||||
|
GoldAmount: record.GoldAmount,
|
||||||
|
RateGoldPerUSD: trimDecimalZeros(record.RateGoldPerUSD),
|
||||||
|
Status: record.Status,
|
||||||
|
Idempotent: idempotent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDuplicateRecordError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
message := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(message, "duplicate") ||
|
||||||
|
strings.Contains(message, "unique constraint") ||
|
||||||
|
strings.Contains(message, "duplicated key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateString(value string, limit int) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if limit <= 0 || len(value) <= limit {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:limit]
|
||||||
|
}
|
||||||
327
internal/service/cprelationbroadcast/event.go
Normal file
327
internal/service/cprelationbroadcast/event.go
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
package cprelationbroadcast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessPayload 解码 Java MQ 消息并发送区域飘屏。
|
||||||
|
func (s *Service) ProcessPayload(ctx context.Context, payload string) (*regionimgroup.RegionBroadcastResponse, error) {
|
||||||
|
req, err := decodeEventPayload(payload, s.cfg.CPRelationBroadcast.MQ.Tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.isEmpty() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return s.ProcessEvent(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessEvent 处理标准化后的 CP 关系事件。
|
||||||
|
func (s *Service) ProcessEvent(ctx context.Context, req EventRequest) (*regionimgroup.RegionBroadcastResponse, error) {
|
||||||
|
if s == nil || s.broadcaster == nil {
|
||||||
|
return nil, common.NewAppError(http.StatusInternalServerError, "cp_relation_broadcaster_missing", "cp relation broadcaster is missing")
|
||||||
|
}
|
||||||
|
if s.db == nil {
|
||||||
|
return nil, common.NewAppError(http.StatusInternalServerError, "cp_relation_db_missing", "cp relation db is missing")
|
||||||
|
}
|
||||||
|
req = normalizeEventRequest(req)
|
||||||
|
|
||||||
|
userID := req.UserID.Int64()
|
||||||
|
cpUserID := req.CpUserID.Int64()
|
||||||
|
if userID <= 0 || cpUserID <= 0 {
|
||||||
|
return nil, common.NewAppError(http.StatusBadRequest, "invalid_cp_relation_users", "userId and cpUserId are required")
|
||||||
|
}
|
||||||
|
relationType, err := normalizeRelationType(req.RelationType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sysOrigin := normalizeSysOrigin(req.SysOrigin, s.cfg.CPRelationBroadcast.DefaultSysOrigin)
|
||||||
|
|
||||||
|
user, err := s.loadEventUser(ctx, userID, req.User)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cpUser, err := s.loadEventUser(ctx, cpUserID, req.CpUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := relationMessage(relationType, user.displayName(), cpUser.displayName())
|
||||||
|
data := buildBroadcastData(req, sysOrigin, relationType, user, cpUser, msg)
|
||||||
|
return s.broadcaster.SendRegionBroadcast(ctx, regionimgroup.RegionBroadcastRequest{
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Type: MessageTypeCPRelationBroadcast,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadEventUser(ctx context.Context, userID int64, provided *EventUser) (EventUser, error) {
|
||||||
|
var user model.UserBaseInfo
|
||||||
|
err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
account := strconv.FormatInt(userID, 10)
|
||||||
|
user = model.UserBaseInfo{
|
||||||
|
ID: userID,
|
||||||
|
Account: account,
|
||||||
|
UserNickname: account,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return EventUser{}, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(user.Account) == "" {
|
||||||
|
user.Account = strconv.FormatInt(userID, 10)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(user.UserNickname) == "" {
|
||||||
|
user.UserNickname = user.Account
|
||||||
|
}
|
||||||
|
return mergeEventUser(userID, user, provided), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBroadcastData(req EventRequest, sysOrigin, relationType string, user EventUser, cpUser EventUser, msg string) map[string]any {
|
||||||
|
userID := user.normalizedID()
|
||||||
|
cpUserID := cpUser.normalizedID()
|
||||||
|
userIDText := strconv.FormatInt(userID, 10)
|
||||||
|
cpUserIDText := strconv.FormatInt(cpUserID, 10)
|
||||||
|
|
||||||
|
data := map[string]any{
|
||||||
|
"sysOrigin": sysOrigin,
|
||||||
|
"type": MessageTypeCPRelationBroadcast,
|
||||||
|
"relationType": relationType,
|
||||||
|
"relationLabel": relationLabel(relationType),
|
||||||
|
"msg": msg,
|
||||||
|
"userId": userIDText,
|
||||||
|
"sendUserId": userIDText,
|
||||||
|
"senderUserId": userIDText,
|
||||||
|
"cpUserId": cpUserIDText,
|
||||||
|
"acceptUserId": cpUserIDText,
|
||||||
|
"toUserId": cpUserIDText,
|
||||||
|
"user": user.toMap(),
|
||||||
|
"cpUser": cpUser.toMap(),
|
||||||
|
|
||||||
|
"account": user.Account,
|
||||||
|
"actualAccount": user.Account,
|
||||||
|
"userNickname": user.displayName(),
|
||||||
|
"nickname": user.displayName(),
|
||||||
|
"userAvatar": firstNonBlank(user.UserAvatar, user.Avatar),
|
||||||
|
"countryCode": user.CountryCode,
|
||||||
|
"countryName": user.CountryName,
|
||||||
|
"cpUserAccount": cpUser.Account,
|
||||||
|
"cpUserNickname": cpUser.displayName(),
|
||||||
|
"cpUserAvatar": firstNonBlank(cpUser.UserAvatar, cpUser.Avatar),
|
||||||
|
"cpUserCountryCode": cpUser.CountryCode,
|
||||||
|
"cpUserCountryName": cpUser.CountryName,
|
||||||
|
"acceptAccount": cpUser.Account,
|
||||||
|
"acceptNickname": cpUser.displayName(),
|
||||||
|
"acceptUserAvatar": firstNonBlank(cpUser.UserAvatar, cpUser.Avatar),
|
||||||
|
"toUserName": cpUser.displayName(),
|
||||||
|
"toUserAvatarUrl": firstNonBlank(cpUser.UserAvatar, cpUser.Avatar),
|
||||||
|
}
|
||||||
|
if eventID := strings.TrimSpace(req.EventID); eventID != "" {
|
||||||
|
data["eventId"] = eventID
|
||||||
|
}
|
||||||
|
if applyID := req.ApplyID.Int64(); applyID > 0 {
|
||||||
|
data["applyId"] = strconv.FormatInt(applyID, 10)
|
||||||
|
}
|
||||||
|
if occurredAt := strings.TrimSpace(req.OccurredAt); occurredAt != "" {
|
||||||
|
data["occurredAt"] = occurredAt
|
||||||
|
}
|
||||||
|
if len(req.Payload) > 0 {
|
||||||
|
data["payload"] = req.Payload
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEventRequest(req EventRequest) EventRequest {
|
||||||
|
if req.UserID.Int64() <= 0 {
|
||||||
|
req.UserID = req.SendUserID
|
||||||
|
}
|
||||||
|
if req.CpUserID.Int64() <= 0 {
|
||||||
|
req.CpUserID = req.AcceptUserID
|
||||||
|
}
|
||||||
|
if req.UserID.Int64() <= 0 && req.User != nil {
|
||||||
|
req.UserID = flexibleInt64(req.User.normalizedID())
|
||||||
|
}
|
||||||
|
if req.CpUserID.Int64() <= 0 && req.CpUser != nil {
|
||||||
|
req.CpUserID = flexibleInt64(req.CpUser.normalizedID())
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req EventRequest) isEmpty() bool {
|
||||||
|
return strings.TrimSpace(req.EventID) == "" &&
|
||||||
|
strings.TrimSpace(req.RelationType) == "" &&
|
||||||
|
req.UserID.Int64() <= 0 &&
|
||||||
|
req.CpUserID.Int64() <= 0 &&
|
||||||
|
req.SendUserID.Int64() <= 0 &&
|
||||||
|
req.AcceptUserID.Int64() <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRelationType(value string) (string, error) {
|
||||||
|
normalized := strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
if normalized == "" {
|
||||||
|
return RelationTypeCP, nil
|
||||||
|
}
|
||||||
|
switch normalized {
|
||||||
|
case RelationTypeCP:
|
||||||
|
return RelationTypeCP, nil
|
||||||
|
case RelationTypeBrother, "BROTHERS":
|
||||||
|
return RelationTypeBrother, nil
|
||||||
|
case RelationTypeSisters, "SISTER":
|
||||||
|
return RelationTypeSisters, nil
|
||||||
|
default:
|
||||||
|
return "", common.NewAppError(http.StatusBadRequest, "invalid_cp_relation_type", fmt.Sprintf("invalid cp relationType: %s", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func relationLabel(relationType string) string {
|
||||||
|
switch relationType {
|
||||||
|
case RelationTypeBrother:
|
||||||
|
return "brothers"
|
||||||
|
case RelationTypeSisters:
|
||||||
|
return "sisters"
|
||||||
|
default:
|
||||||
|
return "cp_relation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func relationMessage(relationType, userName, cpUserName string) string {
|
||||||
|
switch relationType {
|
||||||
|
case RelationTypeBrother:
|
||||||
|
return fmt.Sprintf("%s与%s结为兄弟", userName, cpUserName)
|
||||||
|
case RelationTypeSisters:
|
||||||
|
return fmt.Sprintf("%s与%s结为姐妹", userName, cpUserName)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s与%s get cp relation", userName, cpUserName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSysOrigin(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if text := strings.ToUpper(strings.TrimSpace(value)); text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultSysOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEventPayload(payload string, expectedTag string) (EventRequest, error) {
|
||||||
|
payload = strings.TrimSpace(payload)
|
||||||
|
if payload == "" {
|
||||||
|
return EventRequest{}, nil
|
||||||
|
}
|
||||||
|
req, err := decodeEventObject([]byte(payload))
|
||||||
|
if err != nil {
|
||||||
|
return EventRequest{}, err
|
||||||
|
}
|
||||||
|
if !req.isEmpty() {
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelope messageEventEnvelope
|
||||||
|
if err := json.Unmarshal([]byte(payload), &envelope); err != nil {
|
||||||
|
return EventRequest{}, err
|
||||||
|
}
|
||||||
|
if shouldSkipEnvelope(envelope.Tag, expectedTag) {
|
||||||
|
return EventRequest{}, nil
|
||||||
|
}
|
||||||
|
bodyPayload, ok, err := envelope.bodyPayload()
|
||||||
|
if err != nil || !ok {
|
||||||
|
return EventRequest{}, err
|
||||||
|
}
|
||||||
|
return decodeEventObject([]byte(bodyPayload))
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawEventRequest struct {
|
||||||
|
EventRequest
|
||||||
|
SysOriginSnake string `json:"sys_origin"`
|
||||||
|
EventIDSnake string `json:"event_id"`
|
||||||
|
ApplyIDSnake flexibleInt64 `json:"apply_id"`
|
||||||
|
RelationTypeSnake string `json:"relation_type"`
|
||||||
|
UserIDSnake flexibleInt64 `json:"user_id"`
|
||||||
|
CpUserIDSnake flexibleInt64 `json:"cp_user_id"`
|
||||||
|
SendUserIDSnake flexibleInt64 `json:"send_user_id"`
|
||||||
|
AcceptUserIDSnake flexibleInt64 `json:"accept_user_id"`
|
||||||
|
OccurredAtSnake string `json:"occurred_at"`
|
||||||
|
TargetUserID flexibleInt64 `json:"targetUserId"`
|
||||||
|
TargetUserIDSnake flexibleInt64 `json:"target_user_id"`
|
||||||
|
ToUserIDSnake flexibleInt64 `json:"to_user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEventObject(data []byte) (EventRequest, error) {
|
||||||
|
var raw rawEventRequest
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return EventRequest{}, err
|
||||||
|
}
|
||||||
|
req := raw.EventRequest
|
||||||
|
req.SysOrigin = firstNonBlank(req.SysOrigin, raw.SysOriginSnake)
|
||||||
|
req.EventID = firstNonBlank(req.EventID, raw.EventIDSnake)
|
||||||
|
req.RelationType = firstNonBlank(req.RelationType, raw.RelationTypeSnake)
|
||||||
|
req.OccurredAt = firstNonBlank(req.OccurredAt, raw.OccurredAtSnake)
|
||||||
|
if req.ApplyID.Int64() <= 0 {
|
||||||
|
req.ApplyID = raw.ApplyIDSnake
|
||||||
|
}
|
||||||
|
if req.UserID.Int64() <= 0 {
|
||||||
|
req.UserID = firstFlexible(raw.UserIDSnake, req.SendUserID, raw.SendUserIDSnake)
|
||||||
|
}
|
||||||
|
if req.CpUserID.Int64() <= 0 {
|
||||||
|
req.CpUserID = firstFlexible(raw.CpUserIDSnake, req.AcceptUserID, raw.AcceptUserIDSnake, raw.TargetUserID, raw.TargetUserIDSnake, raw.ToUserIDSnake)
|
||||||
|
}
|
||||||
|
if req.SendUserID.Int64() <= 0 {
|
||||||
|
req.SendUserID = firstFlexible(req.UserID, raw.SendUserIDSnake)
|
||||||
|
}
|
||||||
|
if req.AcceptUserID.Int64() <= 0 {
|
||||||
|
req.AcceptUserID = firstFlexible(req.CpUserID, raw.AcceptUserIDSnake, raw.TargetUserID, raw.TargetUserIDSnake, raw.ToUserIDSnake)
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstFlexible(values ...flexibleInt64) flexibleInt64 {
|
||||||
|
for _, value := range values {
|
||||||
|
if value.Int64() > 0 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageEventEnvelope struct {
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Body json.RawMessage `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e messageEventEnvelope) bodyPayload() (string, bool, error) {
|
||||||
|
raw := strings.TrimSpace(string(e.Body))
|
||||||
|
if raw == "" || raw == "null" {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, "\"") {
|
||||||
|
var body string
|
||||||
|
if err := json.Unmarshal(e.Body, &body); err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
body = strings.TrimSpace(body)
|
||||||
|
return body, body != "", nil
|
||||||
|
}
|
||||||
|
return raw, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldSkipEnvelope(actualTag, expectedTag string) bool {
|
||||||
|
actualTag = strings.TrimSpace(actualTag)
|
||||||
|
expectedTag = strings.TrimSpace(expectedTag)
|
||||||
|
if actualTag == "" || expectedTag == "" || expectedTag == "*" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return actualTag != expectedTag
|
||||||
|
}
|
||||||
142
internal/service/cprelationbroadcast/event_test.go
Normal file
142
internal/service/cprelationbroadcast/event_test.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package cprelationbroadcast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessEventBuildsCPRelationBroadcastPayload(t *testing.T) {
|
||||||
|
service, broadcaster := newCPRelationBroadcastTestService(t)
|
||||||
|
|
||||||
|
_, err := service.ProcessEvent(context.Background(), EventRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
EventID: "CP_RELATION:9001",
|
||||||
|
ApplyID: flexibleInt64(9001),
|
||||||
|
RelationType: RelationTypeCP,
|
||||||
|
UserID: flexibleInt64(1001),
|
||||||
|
CpUserID: flexibleInt64(1002),
|
||||||
|
OccurredAt: "2026-05-22T12:00:00Z",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessEvent() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(broadcaster.requests) != 1 {
|
||||||
|
t.Fatalf("requests = %d, want 1", len(broadcaster.requests))
|
||||||
|
}
|
||||||
|
req := broadcaster.requests[0]
|
||||||
|
if req.Type != MessageTypeCPRelationBroadcast {
|
||||||
|
t.Fatalf("type = %q", req.Type)
|
||||||
|
}
|
||||||
|
data := req.Data
|
||||||
|
if data["msg"] != "Alice与Bob get cp relation" ||
|
||||||
|
data["relationType"] != RelationTypeCP ||
|
||||||
|
data["userId"] != "1001" ||
|
||||||
|
data["cpUserId"] != "1002" {
|
||||||
|
t.Fatalf("payload = %+v", data)
|
||||||
|
}
|
||||||
|
user, ok := data["user"].(map[string]any)
|
||||||
|
if !ok || user["userNickname"] != "Alice" || user["account"] != "alice" {
|
||||||
|
t.Fatalf("user payload = %+v", data["user"])
|
||||||
|
}
|
||||||
|
cpUser, ok := data["cpUser"].(map[string]any)
|
||||||
|
if !ok || cpUser["userNickname"] != "Bob" || cpUser["account"] != "bob" {
|
||||||
|
t.Fatalf("cpUser payload = %+v", data["cpUser"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessEventBuildsBrotherAndSistersMessages(t *testing.T) {
|
||||||
|
service, broadcaster := newCPRelationBroadcastTestService(t)
|
||||||
|
cases := []struct {
|
||||||
|
relationType string
|
||||||
|
wantMsg string
|
||||||
|
}{
|
||||||
|
{RelationTypeBrother, "Alice与Bob结为兄弟"},
|
||||||
|
{RelationTypeSisters, "Alice与Bob结为姐妹"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
broadcaster.requests = nil
|
||||||
|
_, err := service.ProcessEvent(context.Background(), EventRequest{
|
||||||
|
RelationType: tc.relationType,
|
||||||
|
UserID: flexibleInt64(1001),
|
||||||
|
CpUserID: flexibleInt64(1002),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessEvent(%s) error = %v", tc.relationType, err)
|
||||||
|
}
|
||||||
|
if len(broadcaster.requests) != 1 {
|
||||||
|
t.Fatalf("requests = %d, want 1", len(broadcaster.requests))
|
||||||
|
}
|
||||||
|
if got := broadcaster.requests[0].Data["msg"]; got != tc.wantMsg {
|
||||||
|
t.Fatalf("msg = %v, want %s", got, tc.wantMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessPayloadDecodesMessageEventEnvelopeAndAliases(t *testing.T) {
|
||||||
|
service, broadcaster := newCPRelationBroadcastTestService(t)
|
||||||
|
service.cfg.CPRelationBroadcast.MQ.Tag = "cp_relation_broadcast"
|
||||||
|
payload := `{"tag":"cp_relation_broadcast","body":"{\"sys_origin\":\"likei\",\"event_id\":\"CP_RELATION:9002\",\"apply_id\":\"9002\",\"relation_type\":\"BROTHERS\",\"send_user_id\":\"1001\",\"accept_user_id\":\"1002\"}"}`
|
||||||
|
|
||||||
|
_, err := service.ProcessPayload(context.Background(), payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessPayload() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(broadcaster.requests) != 1 {
|
||||||
|
t.Fatalf("requests = %d, want 1", len(broadcaster.requests))
|
||||||
|
}
|
||||||
|
req := broadcaster.requests[0]
|
||||||
|
if req.SysOrigin != "LIKEI" {
|
||||||
|
t.Fatalf("sysOrigin = %q", req.SysOrigin)
|
||||||
|
}
|
||||||
|
if req.Data["eventId"] != "CP_RELATION:9002" ||
|
||||||
|
req.Data["applyId"] != "9002" ||
|
||||||
|
req.Data["relationType"] != RelationTypeBrother ||
|
||||||
|
req.Data["msg"] != "Alice与Bob结为兄弟" {
|
||||||
|
t.Fatalf("payload = %+v", req.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCPRelationBroadcastTestService(t *testing.T) (*Service, *fakeCPRegionBroadcaster) {
|
||||||
|
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.UserBaseInfo{}); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
if err := db.Create(&[]model.UserBaseInfo{
|
||||||
|
{ID: 1001, Account: "alice", UserNickname: "Alice", UserAvatar: "alice.png", OriginSys: "LIKEI", CountryCode: "SA", CountryName: "Saudi Arabia", CreateTime: now},
|
||||||
|
{ID: 1002, Account: "bob", UserNickname: "Bob", UserAvatar: "bob.png", OriginSys: "LIKEI", CountryCode: "SA", CountryName: "Saudi Arabia", CreateTime: now},
|
||||||
|
}).Error; err != nil {
|
||||||
|
t.Fatalf("seed users: %v", err)
|
||||||
|
}
|
||||||
|
broadcaster := &fakeCPRegionBroadcaster{}
|
||||||
|
return NewService(config.Config{
|
||||||
|
CPRelationBroadcast: config.CPRelationBroadcastConfig{DefaultSysOrigin: "LIKEI"},
|
||||||
|
}, db, broadcaster), broadcaster
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeCPRegionBroadcaster struct {
|
||||||
|
requests []regionimgroup.RegionBroadcastRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeCPRegionBroadcaster) SendRegionBroadcast(_ context.Context, req regionimgroup.RegionBroadcastRequest) (*regionimgroup.RegionBroadcastResponse, error) {
|
||||||
|
f.requests = append(f.requests, req)
|
||||||
|
return ®ionimgroup.RegionBroadcastResponse{
|
||||||
|
SysOrigin: req.SysOrigin,
|
||||||
|
RegionCode: "AR",
|
||||||
|
GroupID: "@region-ar",
|
||||||
|
Type: req.Type,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
139
internal/service/cprelationbroadcast/lifecycle.go
Normal file
139
internal/service/cprelationbroadcast/lifecycle.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package cprelationbroadcast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
|
||||||
|
rmq "github.com/apache/rocketmq-clients/golang/v5"
|
||||||
|
"github.com/apache/rocketmq-clients/golang/v5/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartMessageConsumer 启动 CP 关系成功 MQ 消费链路。
|
||||||
|
func (s *Service) StartMessageConsumer(ctx context.Context) error {
|
||||||
|
if !s.cfg.CPRelationBroadcast.MQ.Enabled {
|
||||||
|
log.Printf("cp relation broadcast rocketmq consumer disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.cfg.CPRelationBroadcast.MQ.Endpoint) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.CPRelationBroadcast.MQ.AccessKey) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.CPRelationBroadcast.MQ.AccessSecret) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.CPRelationBroadcast.MQ.Topic) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.CPRelationBroadcast.MQ.ConsumerGroup) == "" {
|
||||||
|
log.Printf("cp relation broadcast rocketmq consumer skipped: endpoint, credentials, topic or consumer group missing")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.startRocketMQConsumerWithRetry(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) startRocketMQConsumerWithRetry(ctx context.Context) {
|
||||||
|
retryDelay := 5 * time.Second
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.startRocketMQConsumer(ctx); err != nil {
|
||||||
|
log.Printf("start cp relation broadcast rocketmq consumer failed: %v; retry in %s", err, retryDelay)
|
||||||
|
timer := time.NewTimer(retryDelay)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
timer.Stop()
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
if retryDelay < time.Minute {
|
||||||
|
retryDelay *= 2
|
||||||
|
if retryDelay > time.Minute {
|
||||||
|
retryDelay = time.Minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) startRocketMQConsumer(ctx context.Context) error {
|
||||||
|
filter := rmq.SUB_ALL
|
||||||
|
if tag := strings.TrimSpace(s.cfg.CPRelationBroadcast.MQ.Tag); tag != "" && tag != "*" {
|
||||||
|
filter = rmq.NewFilterExpression(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
consumer, err := rmq.NewPushConsumer(&rmq.Config{
|
||||||
|
Endpoint: s.cfg.CPRelationBroadcast.MQ.Endpoint,
|
||||||
|
NameSpace: s.cfg.CPRelationBroadcast.MQ.Namespace,
|
||||||
|
ConsumerGroup: s.cfg.CPRelationBroadcast.MQ.ConsumerGroup,
|
||||||
|
Credentials: &credentials.SessionCredentials{
|
||||||
|
AccessKey: s.cfg.CPRelationBroadcast.MQ.AccessKey,
|
||||||
|
AccessSecret: s.cfg.CPRelationBroadcast.MQ.AccessSecret,
|
||||||
|
SecurityToken: s.cfg.CPRelationBroadcast.MQ.SecurityToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rmq.WithPushSubscriptionExpressions(map[string]*rmq.FilterExpression{
|
||||||
|
s.cfg.CPRelationBroadcast.MQ.Topic: filter,
|
||||||
|
}),
|
||||||
|
rmq.WithPushConsumptionThreadCount(4),
|
||||||
|
rmq.WithPushMaxCacheMessageCount(256),
|
||||||
|
rmq.WithPushMessageListener(&rmq.FuncMessageListener{
|
||||||
|
Consume: func(messageView *rmq.MessageView) rmq.ConsumerResult {
|
||||||
|
if messageView == nil {
|
||||||
|
return rmq.SUCCESS
|
||||||
|
}
|
||||||
|
if err := s.processRocketMQMessage(context.Background(), messageView); err != nil {
|
||||||
|
log.Printf("cp relation broadcast mq process failed. messageId=%s err=%v", messageView.GetMessageId(), err)
|
||||||
|
return rmq.FAILURE
|
||||||
|
}
|
||||||
|
return rmq.SUCCESS
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := consumer.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.rocketConsumer = consumer
|
||||||
|
log.Printf("cp relation broadcast rocketmq consumer started. topic=%s group=%s tag=%s",
|
||||||
|
s.cfg.CPRelationBroadcast.MQ.Topic,
|
||||||
|
s.cfg.CPRelationBroadcast.MQ.ConsumerGroup,
|
||||||
|
s.cfg.CPRelationBroadcast.MQ.Tag,
|
||||||
|
)
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
if s.rocketConsumer != nil {
|
||||||
|
_ = s.rocketConsumer.GracefulStop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) processRocketMQMessage(ctx context.Context, messageView *rmq.MessageView) error {
|
||||||
|
payload := strings.TrimSpace(string(messageView.GetBody()))
|
||||||
|
if payload == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := s.ProcessPayload(ctx, payload); err != nil {
|
||||||
|
var syntaxErr *json.SyntaxError
|
||||||
|
var typeErr *json.UnmarshalTypeError
|
||||||
|
if errors.As(err, &syntaxErr) || errors.As(err, &typeErr) {
|
||||||
|
log.Printf("drop malformed cp relation broadcast message. messageId=%s err=%v", messageView.GetMessageId(), err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var appErr *common.AppError
|
||||||
|
if errors.As(err, &appErr) && appErr.Status >= http.StatusBadRequest && appErr.Status < http.StatusInternalServerError {
|
||||||
|
log.Printf("drop invalid cp relation broadcast message. messageId=%s code=%s err=%v", messageView.GetMessageId(), appErr.Code, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
199
internal/service/cprelationbroadcast/types.go
Normal file
199
internal/service/cprelationbroadcast/types.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package cprelationbroadcast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
|
||||||
|
rmq "github.com/apache/rocketmq-clients/golang/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MessageTypeCPRelationBroadcast = "CP_RELATION_BROADCAST"
|
||||||
|
|
||||||
|
RelationTypeCP = "CP"
|
||||||
|
RelationTypeBrother = "BROTHER"
|
||||||
|
RelationTypeSisters = "SISTERS"
|
||||||
|
|
||||||
|
defaultSysOrigin = "LIKEI"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbHandle interface {
|
||||||
|
WithContext(ctx context.Context) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type regionBroadcaster interface {
|
||||||
|
SendRegionBroadcast(ctx context.Context, req regionimgroup.RegionBroadcastRequest) (*regionimgroup.RegionBroadcastResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 消费 Java CP 关系成功 MQ,并复用区域 IM 广播发送飘屏。
|
||||||
|
type Service struct {
|
||||||
|
cfg config.Config
|
||||||
|
db dbHandle
|
||||||
|
broadcaster regionBroadcaster
|
||||||
|
rocketConsumer rmq.PushConsumer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(cfg config.Config, db dbHandle, broadcaster regionBroadcaster) *Service {
|
||||||
|
return &Service{cfg: cfg, db: db, broadcaster: broadcaster}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventRequest 是 Java 在 CP/兄弟/姐妹关系达成后投递给 Go 的 MQ 事件。
|
||||||
|
type EventRequest struct {
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
EventID string `json:"eventId"`
|
||||||
|
ApplyID flexibleInt64 `json:"applyId"`
|
||||||
|
RelationType string `json:"relationType"`
|
||||||
|
UserID flexibleInt64 `json:"userId"`
|
||||||
|
CpUserID flexibleInt64 `json:"cpUserId"`
|
||||||
|
SendUserID flexibleInt64 `json:"sendUserId"`
|
||||||
|
AcceptUserID flexibleInt64 `json:"acceptUserId"`
|
||||||
|
OccurredAt string `json:"occurredAt"`
|
||||||
|
User *EventUser `json:"user"`
|
||||||
|
CpUser *EventUser `json:"cpUser"`
|
||||||
|
Payload map[string]any `json:"payload,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventUser struct {
|
||||||
|
ID flexibleInt64 `json:"id"`
|
||||||
|
UserID flexibleInt64 `json:"userId"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
UserNickname string `json:"userNickname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
UserAvatar string `json:"userAvatar"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
CountryName string `json:"countryName"`
|
||||||
|
OriginSys string `json:"originSys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u EventUser) normalizedID() int64 {
|
||||||
|
if id := u.UserID.Int64(); id > 0 {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return u.ID.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u EventUser) displayName() string {
|
||||||
|
for _, value := range []string{u.UserNickname, u.Nickname, u.Account} {
|
||||||
|
if text := strings.TrimSpace(value); text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if id := u.normalizedID(); id > 0 {
|
||||||
|
return strconv.FormatInt(id, 10)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u EventUser) toMap() map[string]any {
|
||||||
|
id := u.normalizedID()
|
||||||
|
data := map[string]any{}
|
||||||
|
if id > 0 {
|
||||||
|
data["userId"] = strconv.FormatInt(id, 10)
|
||||||
|
data["id"] = strconv.FormatInt(id, 10)
|
||||||
|
}
|
||||||
|
if account := strings.TrimSpace(u.Account); account != "" {
|
||||||
|
data["account"] = account
|
||||||
|
}
|
||||||
|
if nickname := strings.TrimSpace(firstNonBlank(u.UserNickname, u.Nickname)); nickname != "" {
|
||||||
|
data["userNickname"] = nickname
|
||||||
|
data["nickname"] = nickname
|
||||||
|
}
|
||||||
|
if avatar := strings.TrimSpace(firstNonBlank(u.UserAvatar, u.Avatar)); avatar != "" {
|
||||||
|
data["userAvatar"] = avatar
|
||||||
|
data["avatar"] = avatar
|
||||||
|
}
|
||||||
|
if countryCode := strings.TrimSpace(u.CountryCode); countryCode != "" {
|
||||||
|
data["countryCode"] = countryCode
|
||||||
|
}
|
||||||
|
if countryName := strings.TrimSpace(u.CountryName); countryName != "" {
|
||||||
|
data["countryName"] = countryName
|
||||||
|
}
|
||||||
|
if originSys := strings.TrimSpace(u.OriginSys); originSys != "" {
|
||||||
|
data["originSys"] = originSys
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventUserFromModel(user model.UserBaseInfo) EventUser {
|
||||||
|
return EventUser{
|
||||||
|
ID: flexibleInt64(user.ID),
|
||||||
|
UserID: flexibleInt64(user.ID),
|
||||||
|
Account: user.Account,
|
||||||
|
Nickname: user.UserNickname,
|
||||||
|
UserNickname: user.UserNickname,
|
||||||
|
Avatar: user.UserAvatar,
|
||||||
|
UserAvatar: user.UserAvatar,
|
||||||
|
CountryCode: user.CountryCode,
|
||||||
|
CountryName: user.CountryName,
|
||||||
|
OriginSys: user.OriginSys,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeEventUser(id int64, dbUser model.UserBaseInfo, provided *EventUser) EventUser {
|
||||||
|
merged := eventUserFromModel(dbUser)
|
||||||
|
if id > 0 {
|
||||||
|
merged.ID = flexibleInt64(id)
|
||||||
|
merged.UserID = flexibleInt64(id)
|
||||||
|
}
|
||||||
|
if provided == nil {
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
if provided.normalizedID() > 0 {
|
||||||
|
merged.ID = flexibleInt64(provided.normalizedID())
|
||||||
|
merged.UserID = flexibleInt64(provided.normalizedID())
|
||||||
|
}
|
||||||
|
merged.Account = firstNonBlank(provided.Account, merged.Account)
|
||||||
|
merged.UserNickname = firstNonBlank(provided.UserNickname, provided.Nickname, merged.UserNickname)
|
||||||
|
merged.Nickname = merged.UserNickname
|
||||||
|
merged.UserAvatar = firstNonBlank(provided.UserAvatar, provided.Avatar, merged.UserAvatar)
|
||||||
|
merged.Avatar = merged.UserAvatar
|
||||||
|
merged.CountryCode = firstNonBlank(provided.CountryCode, merged.CountryCode)
|
||||||
|
merged.CountryName = firstNonBlank(provided.CountryName, merged.CountryName)
|
||||||
|
merged.OriginSys = firstNonBlank(provided.OriginSys, merged.OriginSys)
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
type flexibleInt64 int64
|
||||||
|
|
||||||
|
func (v *flexibleInt64) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := strings.TrimSpace(string(data))
|
||||||
|
switch raw {
|
||||||
|
case "", "null", `""`:
|
||||||
|
*v = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, `"`) && strings.HasSuffix(raw, `"`) {
|
||||||
|
unquoted, err := strconv.Unquote(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw = strings.TrimSpace(unquoted)
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseInt(raw, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid int64 value: %w", err)
|
||||||
|
}
|
||||||
|
*v = flexibleInt64(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v flexibleInt64) Int64() int64 {
|
||||||
|
return int64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonBlank(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if text := strings.TrimSpace(value); text != "" {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user