首冲奖励
This commit is contained in:
parent
74063d0908
commit
a02e22ac74
@ -16,6 +16,7 @@ import (
|
|||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/binancerecharge"
|
"chatapp3-golang/internal/service/binancerecharge"
|
||||||
"chatapp3-golang/internal/service/errorlog"
|
"chatapp3-golang/internal/service/errorlog"
|
||||||
|
"chatapp3-golang/internal/service/firstrechargereward"
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
"chatapp3-golang/internal/service/hostcenter"
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
@ -60,6 +61,7 @@ func main() {
|
|||||||
rechargeAgencyService := rechargeagency.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
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)
|
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)
|
taskCenterService := taskcenter.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
userBadgeService := userbadge.NewService(app.Repository.DB)
|
userBadgeService := userbadge.NewService(app.Repository.DB)
|
||||||
@ -92,6 +94,9 @@ func main() {
|
|||||||
if err := taskCenterService.StartMessageConsumer(workerCtx); err != nil {
|
if err := taskCenterService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
log.Fatalf("start task center message consumer failed: %v", err)
|
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 := voiceRoomRocketService.Start(workerCtx); err != nil {
|
if err := voiceRoomRocketService.Start(workerCtx); err != nil {
|
||||||
log.Fatalf("start voice room rocket workers failed: %v", err)
|
log.Fatalf("start voice room rocket workers failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -105,32 +110,33 @@ func main() {
|
|||||||
lingxian.NewAppProvider(lingxianService),
|
lingxian.NewAppProvider(lingxianService),
|
||||||
)
|
)
|
||||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||||
AppPopup: appPopupService,
|
AppPopup: appPopupService,
|
||||||
ErrorLog: errorLogService,
|
ErrorLog: errorLogService,
|
||||||
Invite: inviteService,
|
Invite: inviteService,
|
||||||
Baishun: baishunService,
|
Baishun: baishunService,
|
||||||
BinanceRecharge: binanceRechargeService,
|
BinanceRecharge: binanceRechargeService,
|
||||||
Lingxian: lingxianService,
|
FirstRechargeReward: firstRechargeRewardService,
|
||||||
GameOpen: gameOpenService,
|
Lingxian: lingxianService,
|
||||||
GameProviders: gameProviders,
|
GameOpen: gameOpenService,
|
||||||
LuckyGift: luckyGiftService,
|
GameProviders: gameProviders,
|
||||||
ManagerCenter: managerCenterService,
|
LuckyGift: luckyGiftService,
|
||||||
PropsStore: propsStoreService,
|
ManagerCenter: managerCenterService,
|
||||||
HostCenter: hostCenterService,
|
PropsStore: propsStoreService,
|
||||||
RechargeAgency: rechargeAgencyService,
|
HostCenter: hostCenterService,
|
||||||
RechargeReward: rechargeRewardService,
|
RechargeAgency: rechargeAgencyService,
|
||||||
RegionIMGroup: regionIMGroupService,
|
RechargeReward: rechargeRewardService,
|
||||||
RegisterReward: registerRewardService,
|
RegionIMGroup: regionIMGroupService,
|
||||||
RoomTurnoverReward: roomTurnoverRewardService,
|
RegisterReward: registerRewardService,
|
||||||
SignInReward: signInRewardService,
|
RoomTurnoverReward: roomTurnoverRewardService,
|
||||||
SmashEgg: smashEggService,
|
SignInReward: signInRewardService,
|
||||||
TaskCenter: taskCenterService,
|
SmashEgg: smashEggService,
|
||||||
UserBadge: userBadgeService,
|
TaskCenter: taskCenterService,
|
||||||
VIP: vipService,
|
UserBadge: userBadgeService,
|
||||||
Wheel: wheelService,
|
VIP: vipService,
|
||||||
VoiceRoomRedPacket: voiceRoomRedPacketService,
|
Wheel: wheelService,
|
||||||
VoiceRoomRocket: voiceRoomRocketService,
|
VoiceRoomRedPacket: voiceRoomRedPacketService,
|
||||||
WeekStar: weekStarService,
|
VoiceRoomRocket: voiceRoomRocketService,
|
||||||
|
WeekStar: weekStarService,
|
||||||
})
|
})
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: app.Config.HTTP.ListenAddr,
|
Addr: app.Config.HTTP.ListenAddr,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"chatapp3-golang/internal/bootstrap"
|
"chatapp3-golang/internal/bootstrap"
|
||||||
|
"chatapp3-golang/internal/service/firstrechargereward"
|
||||||
"chatapp3-golang/internal/service/regionimgroup"
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
"chatapp3-golang/internal/service/registerreward"
|
"chatapp3-golang/internal/service/registerreward"
|
||||||
"chatapp3-golang/internal/service/taskcenter"
|
"chatapp3-golang/internal/service/taskcenter"
|
||||||
@ -27,6 +28,7 @@ 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)
|
||||||
taskCenterService := taskcenter.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
taskCenterService := taskcenter.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
|
firstRechargeRewardService := firstrechargereward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
regionIMGroupService := regionimgroup.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
||||||
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
||||||
@ -45,6 +47,9 @@ func main() {
|
|||||||
if err := taskCenterService.StartMessageConsumer(workerCtx); err != nil {
|
if err := taskCenterService.StartMessageConsumer(workerCtx); err != nil {
|
||||||
log.Fatalf("start task center message consumer failed: %v", err)
|
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 := voiceRoomRocketService.Start(workerCtx); err != nil {
|
if err := voiceRoomRocketService.Start(workerCtx); err != nil {
|
||||||
log.Fatalf("start voice room rocket workers failed: %v", err)
|
log.Fatalf("start voice room rocket workers failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,23 +10,24 @@ import (
|
|||||||
|
|
||||||
// Config 是按运行层和业务模块分段后的应用配置。
|
// Config 是按运行层和业务模块分段后的应用配置。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
HTTP HTTPConfig
|
HTTP HTTPConfig
|
||||||
Store StoreConfig
|
Store StoreConfig
|
||||||
Java JavaConfig
|
Java JavaConfig
|
||||||
Worker WorkerConfig
|
Worker WorkerConfig
|
||||||
Invite InviteConfig
|
Invite InviteConfig
|
||||||
RechargeReward RechargeRewardConfig
|
RechargeReward RechargeRewardConfig
|
||||||
TaskCenter TaskCenterConfig
|
FirstRechargeReward FirstRechargeRewardConfig
|
||||||
WeekStar WeekStarConfig
|
TaskCenter TaskCenterConfig
|
||||||
RoomTurnoverReward RoomTurnoverRewardConfig
|
WeekStar WeekStarConfig
|
||||||
VoiceRoomRedPacket VoiceRoomRedPacketConfig
|
RoomTurnoverReward RoomTurnoverRewardConfig
|
||||||
VoiceRoomRocket VoiceRoomRocketConfig
|
VoiceRoomRedPacket VoiceRoomRedPacketConfig
|
||||||
TencentIM TencentIMConfig
|
VoiceRoomRocket VoiceRoomRocketConfig
|
||||||
Baishun BaishunConfig
|
TencentIM TencentIMConfig
|
||||||
Lingxian LingxianConfig
|
Baishun BaishunConfig
|
||||||
GameOpen GameOpenConfig
|
Lingxian LingxianConfig
|
||||||
LuckyGift LuckyGiftConfig
|
GameOpen GameOpenConfig
|
||||||
BinanceRecharge BinanceRechargeConfig
|
LuckyGift LuckyGiftConfig
|
||||||
|
BinanceRecharge BinanceRechargeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig 保存 HTTP 服务运行配置。
|
// HTTPConfig 保存 HTTP 服务运行配置。
|
||||||
@ -88,6 +89,25 @@ type RechargeRewardConfig struct {
|
|||||||
CoinSellerGoldPerUSD int64
|
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 保存任务中心模块配置。
|
// TaskCenterConfig 保存任务中心模块配置。
|
||||||
type TaskCenterConfig struct {
|
type TaskCenterConfig struct {
|
||||||
DefaultSysOrigin string
|
DefaultSysOrigin string
|
||||||
@ -265,6 +285,9 @@ func Load() Config {
|
|||||||
taskCenterMQEndpoint := getEnvAny([]string{"CHATAPP_TASK_CENTER_MQ_ENDPOINT", "TASK_CENTER_EVENT_MQ_ENDPOINT", "LIKEI_ROCKETMQ_ENDPOINT"}, "")
|
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"}, "")
|
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"}, "")
|
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"}, "")
|
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"}, "")
|
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"}, "")
|
voiceRoomRocketMQAccessSecret := getEnvAny([]string{"CHATAPP_VOICE_ROOM_ROCKET_MQ_ACCESS_SECRET", "LIKEI_ROCKETMQ_SECRET_KEY"}, "")
|
||||||
@ -329,6 +352,23 @@ func Load() Config {
|
|||||||
100000,
|
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{
|
TaskCenter: TaskCenterConfig{
|
||||||
DefaultSysOrigin: strings.ToUpper(getEnvAny(
|
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"},
|
[]string{"CHATAPP_TASK_CENTER_DEFAULT_SYS_ORIGIN", "TASK_CENTER_DEFAULT_SYS_ORIGIN", "CHATAPP_WEEK_STAR_DEFAULT_SYS_ORIGIN", "WEEK_STAR_DEFAULT_SYS_ORIGIN"},
|
||||||
|
|||||||
65
internal/model/first_recharge_reward_models.go
Normal file
65
internal/model/first_recharge_reward_models.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
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"
|
||||||
|
}
|
||||||
70
internal/router/first_recharge_reward_routes.go
Normal file
70
internal/router/first_recharge_reward_routes.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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))
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeOK(c, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"chatapp3-golang/internal/service/baishun"
|
"chatapp3-golang/internal/service/baishun"
|
||||||
"chatapp3-golang/internal/service/binancerecharge"
|
"chatapp3-golang/internal/service/binancerecharge"
|
||||||
"chatapp3-golang/internal/service/errorlog"
|
"chatapp3-golang/internal/service/errorlog"
|
||||||
|
"chatapp3-golang/internal/service/firstrechargereward"
|
||||||
"chatapp3-golang/internal/service/gameopen"
|
"chatapp3-golang/internal/service/gameopen"
|
||||||
"chatapp3-golang/internal/service/gameprovider"
|
"chatapp3-golang/internal/service/gameprovider"
|
||||||
"chatapp3-golang/internal/service/hostcenter"
|
"chatapp3-golang/internal/service/hostcenter"
|
||||||
@ -38,32 +39,33 @@ import (
|
|||||||
|
|
||||||
// Services 聚合所有业务模块服务,供路由层统一装配。
|
// Services 聚合所有业务模块服务,供路由层统一装配。
|
||||||
type Services struct {
|
type Services struct {
|
||||||
AppPopup *apppopup.Service
|
AppPopup *apppopup.Service
|
||||||
ErrorLog *errorlog.Service
|
ErrorLog *errorlog.Service
|
||||||
Invite *invite.InviteService
|
Invite *invite.InviteService
|
||||||
Baishun *baishun.BaishunService
|
Baishun *baishun.BaishunService
|
||||||
BinanceRecharge *binancerecharge.Service
|
BinanceRecharge *binancerecharge.Service
|
||||||
Lingxian *lingxian.Service
|
FirstRechargeReward *firstrechargereward.Service
|
||||||
GameOpen *gameopen.GameOpenService
|
Lingxian *lingxian.Service
|
||||||
GameProviders *gameprovider.Registry
|
GameOpen *gameopen.GameOpenService
|
||||||
LuckyGift *luckygift.LuckyGiftService
|
GameProviders *gameprovider.Registry
|
||||||
ManagerCenter *managercenter.Service
|
LuckyGift *luckygift.LuckyGiftService
|
||||||
PropsStore *propsstore.Service
|
ManagerCenter *managercenter.Service
|
||||||
HostCenter *hostcenter.Service
|
PropsStore *propsstore.Service
|
||||||
RechargeAgency *rechargeagency.Service
|
HostCenter *hostcenter.Service
|
||||||
RechargeReward *rechargereward.Service
|
RechargeAgency *rechargeagency.Service
|
||||||
RegionIMGroup *regionimgroup.Service
|
RechargeReward *rechargereward.Service
|
||||||
RegisterReward *registerreward.RegisterRewardService
|
RegionIMGroup *regionimgroup.Service
|
||||||
RoomTurnoverReward *roomturnoverreward.Service
|
RegisterReward *registerreward.RegisterRewardService
|
||||||
SignInReward *signinreward.SignInRewardService
|
RoomTurnoverReward *roomturnoverreward.Service
|
||||||
SmashEgg *smashegg.Service
|
SignInReward *signinreward.SignInRewardService
|
||||||
TaskCenter *taskcenter.Service
|
SmashEgg *smashegg.Service
|
||||||
UserBadge *userbadge.Service
|
TaskCenter *taskcenter.Service
|
||||||
VIP *vip.Service
|
UserBadge *userbadge.Service
|
||||||
Wheel *wheel.Service
|
VIP *vip.Service
|
||||||
VoiceRoomRedPacket *voiceroomredpacket.Service
|
Wheel *wheel.Service
|
||||||
VoiceRoomRocket *voiceroomrocket.Service
|
VoiceRoomRedPacket *voiceroomredpacket.Service
|
||||||
WeekStar *weekstar.WeekStarService
|
VoiceRoomRocket *voiceroomrocket.Service
|
||||||
|
WeekStar *weekstar.WeekStarService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter 创建 Gin 路由,并按模块注册所有接口。
|
// NewRouter 创建 Gin 路由,并按模块注册所有接口。
|
||||||
@ -82,6 +84,7 @@ func NewRouter(
|
|||||||
registerInviteRoutes(engine, javaClient, services.Invite)
|
registerInviteRoutes(engine, javaClient, services.Invite)
|
||||||
registerRechargeAgencyRoutes(engine, javaClient, services.RechargeAgency)
|
registerRechargeAgencyRoutes(engine, javaClient, services.RechargeAgency)
|
||||||
registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward)
|
registerRechargeRewardRoutes(engine, javaClient, services.RechargeReward)
|
||||||
|
registerFirstRechargeRewardRoutes(engine, javaClient, services.FirstRechargeReward)
|
||||||
registerBinanceRechargeRoutes(engine, javaClient, services.BinanceRecharge)
|
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)
|
||||||
|
|||||||
234
internal/service/firstrechargereward/config.go
Normal file
234
internal/service/firstrechargereward/config.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/utils"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigResponse, error) {
|
||||||
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
||||||
|
var configRow model.FirstRechargeRewardConfig
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &ConfigResponse{
|
||||||
|
Configured: false,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
Enabled: false,
|
||||||
|
LevelConfigs: []LevelPayload{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
levels, err := s.loadLevels(ctx, configRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rewardItems := s.loadRewardItemsMap(ctx, levels)
|
||||||
|
return &ConfigResponse{
|
||||||
|
Configured: true,
|
||||||
|
ID: configRow.ID,
|
||||||
|
SysOrigin: configRow.SysOrigin,
|
||||||
|
Enabled: configRow.Enabled,
|
||||||
|
UpdateTime: formatDateTime(configRow.UpdateTime),
|
||||||
|
LevelConfigs: buildLevelPayloads(levels, rewardItems, 0, 0, true),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
|
||||||
|
requestID := req.ID.Int64()
|
||||||
|
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
|
||||||
|
levels, err := s.normalizeLevelInputs(req.LevelConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.Enabled && !hasEnabledLevel(levels) {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "enabled config must contain at least one enabled level")
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedConfigID int64
|
||||||
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
now := time.Now()
|
||||||
|
var configRow model.FirstRechargeRewardConfig
|
||||||
|
if requestID > 0 {
|
||||||
|
err := tx.Where("id = ?", requestID).First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return NewAppError(http.StatusNotFound, "first_recharge_reward_config_not_found", "config not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.SysOrigin) == "" {
|
||||||
|
sysOrigin = configRow.SysOrigin
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := tx.Where("sys_origin = ?", sysOrigin).First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
configID, idErr := utils.NextID()
|
||||||
|
if idErr != nil {
|
||||||
|
return idErr
|
||||||
|
}
|
||||||
|
configRow = model.FirstRechargeRewardConfig{
|
||||||
|
ID: configID,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
CreateTime: now,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configRow.SysOrigin = sysOrigin
|
||||||
|
configRow.Enabled = req.Enabled
|
||||||
|
configRow.UpdateTime = now
|
||||||
|
if configRow.CreateTime.IsZero() {
|
||||||
|
configRow.CreateTime = now
|
||||||
|
}
|
||||||
|
if err := tx.Save(&configRow).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.FirstRechargeRewardLevel{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for index := range levels {
|
||||||
|
levels[index].ConfigID = configRow.ID
|
||||||
|
levels[index].CreateTime = now
|
||||||
|
levels[index].UpdateTime = now
|
||||||
|
}
|
||||||
|
if len(levels) > 0 {
|
||||||
|
if err := tx.Create(&levels).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
savedConfigID = configRow.ID
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetConfig(ctx, savedSysOrigin(ctx, s.db, savedConfigID, sysOrigin))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) normalizeLevelInputs(inputs []LevelInput) ([]model.FirstRechargeRewardLevel, error) {
|
||||||
|
if len(inputs) == 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "levelConfigs is required")
|
||||||
|
}
|
||||||
|
seenLevels := make(map[int]struct{}, len(inputs))
|
||||||
|
seenAmounts := make(map[int64]struct{}, len(inputs))
|
||||||
|
rows := make([]model.FirstRechargeRewardLevel, 0, len(inputs))
|
||||||
|
for _, item := range inputs {
|
||||||
|
if item.Level <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_level", "level must be greater than 0")
|
||||||
|
}
|
||||||
|
amountCents := item.RechargeAmount.Cents()
|
||||||
|
if amountCents == 0 && item.RechargeAmountCents > 0 {
|
||||||
|
amountCents = item.RechargeAmountCents
|
||||||
|
}
|
||||||
|
if amountCents <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_recharge_amount", "rechargeAmount must be greater than 0")
|
||||||
|
}
|
||||||
|
rewardGroupID := item.RewardGroupID.Int64()
|
||||||
|
if rewardGroupID <= 0 {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_group", "rewardGroupId is required")
|
||||||
|
}
|
||||||
|
if _, exists := seenLevels[item.Level]; exists {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "duplicate_level", "levelConfigs must not contain duplicate levels")
|
||||||
|
}
|
||||||
|
if _, exists := seenAmounts[amountCents]; exists {
|
||||||
|
return nil, NewAppError(http.StatusBadRequest, "duplicate_recharge_amount", "levelConfigs must not contain duplicate rechargeAmount")
|
||||||
|
}
|
||||||
|
seenLevels[item.Level] = struct{}{}
|
||||||
|
seenAmounts[amountCents] = struct{}{}
|
||||||
|
id, err := utils.NextID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows = append(rows, model.FirstRechargeRewardLevel{
|
||||||
|
ID: id,
|
||||||
|
Level: item.Level,
|
||||||
|
RechargeAmountCents: amountCents,
|
||||||
|
RewardGroupID: rewardGroupID,
|
||||||
|
RewardGroupName: strings.TrimSpace(item.RewardGroupName),
|
||||||
|
Enabled: item.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
|
if rows[i].RechargeAmountCents == rows[j].RechargeAmountCents {
|
||||||
|
return rows[i].Level < rows[j].Level
|
||||||
|
}
|
||||||
|
return rows[i].RechargeAmountCents < rows[j].RechargeAmountCents
|
||||||
|
})
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadConfigBundle(ctx context.Context, sysOrigin string) (*configBundle, error) {
|
||||||
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
||||||
|
var configRow model.FirstRechargeRewardConfig
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("sys_origin = ?", sysOrigin).
|
||||||
|
First(&configRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return &configBundle{}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
levels, err := s.loadLevels(ctx, configRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &configBundle{
|
||||||
|
Config: refConfigSnapshot(configSnapshotFromModel(configRow)),
|
||||||
|
Levels: levels,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadLevels(ctx context.Context, configID int64) ([]levelSnapshot, error) {
|
||||||
|
if configID <= 0 {
|
||||||
|
return []levelSnapshot{}, nil
|
||||||
|
}
|
||||||
|
var rows []model.FirstRechargeRewardLevel
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("config_id = ?", configID).
|
||||||
|
Order("recharge_amount_cents asc").
|
||||||
|
Order("level asc").
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
levels := make([]levelSnapshot, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
levels = append(levels, levelSnapshotFromModel(row))
|
||||||
|
}
|
||||||
|
sortLevels(levels)
|
||||||
|
return levels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEnabledLevel(levels []model.FirstRechargeRewardLevel) bool {
|
||||||
|
for _, level := range levels {
|
||||||
|
if level.Enabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func savedSysOrigin(ctx context.Context, db dbHandle, id int64, fallback string) string {
|
||||||
|
var row model.FirstRechargeRewardConfig
|
||||||
|
if id > 0 && db != nil {
|
||||||
|
if err := db.WithContext(ctx).Where("id = ?", id).First(&row).Error; err == nil {
|
||||||
|
return row.SysOrigin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
407
internal/service/firstrechargereward/event.go
Normal file
407
internal/service/firstrechargereward/event.go
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/utils"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) ProcessRechargePayload(ctx context.Context, payload string) (*ProcessRechargeResponse, error) {
|
||||||
|
payload = strings.TrimSpace(payload)
|
||||||
|
if payload == "" {
|
||||||
|
return &ProcessRechargeResponse{Processed: false, Reason: "empty_payload"}, nil
|
||||||
|
}
|
||||||
|
event, skip, err := decodeRechargeEventPayload(payload, s.cfg.FirstRechargeReward.MQ.Tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if skip {
|
||||||
|
return &ProcessRechargeResponse{Processed: false, Reason: "tag_skipped"}, nil
|
||||||
|
}
|
||||||
|
return s.ProcessRechargeEvent(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ProcessRechargeEvent(ctx context.Context, event RechargeEvent) (*ProcessRechargeResponse, error) {
|
||||||
|
normalized, err := s.normalizeRechargeEvent(event)
|
||||||
|
if err != nil {
|
||||||
|
var appErr *AppError
|
||||||
|
if errors.As(err, &appErr) && appErr.Status >= http.StatusBadRequest && appErr.Status < http.StatusInternalServerError {
|
||||||
|
return &ProcessRechargeResponse{Processed: false, Reason: appErr.Code}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isAcceptedPaymentMethod(normalized.PaymentMethod, normalized.PayPlatform) {
|
||||||
|
return &ProcessRechargeResponse{Processed: false, Reason: "unsupported_payment_method"}, nil
|
||||||
|
}
|
||||||
|
if s.java == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "java_gateway_unavailable", "java gateway is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle, err := s.loadConfigBundle(ctx, normalized.SysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bundle.Config == nil {
|
||||||
|
return &ProcessRechargeResponse{Processed: false, Reason: "config_missing"}, nil
|
||||||
|
}
|
||||||
|
if !bundle.Config.Enabled {
|
||||||
|
return &ProcessRechargeResponse{Processed: false, Reason: "config_disabled"}, nil
|
||||||
|
}
|
||||||
|
matched, ok := pickMatchedLevel(bundle.Levels, normalized.AmountCents)
|
||||||
|
if !ok {
|
||||||
|
return &ProcessRechargeResponse{Processed: false, Reason: "amount_not_reached"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
record, terminal, err := s.getOrCreateGrantRecord(ctx, normalized, bundle.Config, matched)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if terminal {
|
||||||
|
return &ProcessRechargeResponse{
|
||||||
|
Processed: true,
|
||||||
|
RecordID: record.ID,
|
||||||
|
Status: record.Status,
|
||||||
|
Level: record.Level,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.dispatchRewardGroup(ctx, record); err != nil {
|
||||||
|
_ = s.updateGrantRecordFailed(ctx, record.ID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.updateGrantRecordSuccess(ctx, record.ID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
record.Status = statusSuccess
|
||||||
|
now := time.Now()
|
||||||
|
record.SentAt = &now
|
||||||
|
if err := s.pushRewardNotice(ctx, record); err != nil {
|
||||||
|
log.Printf("push first recharge reward notice failed. recordId=%d userId=%d eventId=%s err=%v",
|
||||||
|
record.ID, record.UserID, record.EventID, err)
|
||||||
|
}
|
||||||
|
_ = s.java.RemoveUserProfileCacheAll(ctx, record.UserID)
|
||||||
|
return &ProcessRechargeResponse{
|
||||||
|
Processed: true,
|
||||||
|
RecordID: record.ID,
|
||||||
|
Status: statusSuccess,
|
||||||
|
Level: record.Level,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRechargeEvent, error) {
|
||||||
|
sysOrigin := s.normalizeSysOrigin(event.SysOrigin)
|
||||||
|
userID := event.UserID.Int64()
|
||||||
|
if userID <= 0 {
|
||||||
|
return normalizedRechargeEvent{}, NewAppError(http.StatusBadRequest, "invalid_user_id", "userId is required")
|
||||||
|
}
|
||||||
|
amountCents := event.AmountCents.Int64()
|
||||||
|
if amountCents <= 0 {
|
||||||
|
amountCents = firstPositiveAmountCents(event.Amount.String(), event.AmountUSD.String(), event.USDAmount.String())
|
||||||
|
}
|
||||||
|
if amountCents <= 0 {
|
||||||
|
return normalizedRechargeEvent{}, NewAppError(http.StatusBadRequest, "invalid_amount", "amountCents or amount is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
payPlatform := strings.ToUpper(strings.TrimSpace(event.PayPlatform))
|
||||||
|
paymentMethod := strings.ToUpper(strings.TrimSpace(event.PaymentMethod))
|
||||||
|
sourceOrderID := firstNonEmpty(event.SourceOrderID.String(), event.OrderID.String())
|
||||||
|
eventID := strings.TrimSpace(event.EventID)
|
||||||
|
if eventID == "" && sourceOrderID != "" {
|
||||||
|
eventID = "RECHARGE_SUCCESS:" + firstNonEmpty(paymentMethod, payPlatform, "UNKNOWN") + ":" + sourceOrderID
|
||||||
|
}
|
||||||
|
if eventID == "" {
|
||||||
|
return normalizedRechargeEvent{}, NewAppError(http.StatusBadRequest, "invalid_event_id", "eventId is required")
|
||||||
|
}
|
||||||
|
occurredAt := parseEventTime(event.OccurredAt.String())
|
||||||
|
if occurredAt.IsZero() {
|
||||||
|
occurredAt = time.Now()
|
||||||
|
}
|
||||||
|
return normalizedRechargeEvent{
|
||||||
|
EventID: eventID,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
UserID: userID,
|
||||||
|
AmountCents: amountCents,
|
||||||
|
Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"),
|
||||||
|
PayPlatform: payPlatform,
|
||||||
|
PaymentMethod: paymentMethod,
|
||||||
|
SourceOrderID: sourceOrderID,
|
||||||
|
OccurredAt: occurredAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getOrCreateGrantRecord(
|
||||||
|
ctx context.Context,
|
||||||
|
event normalizedRechargeEvent,
|
||||||
|
config *configSnapshot,
|
||||||
|
level levelSnapshot,
|
||||||
|
) (*model.FirstRechargeRewardGrantRecord, bool, error) {
|
||||||
|
var existing model.FirstRechargeRewardGrantRecord
|
||||||
|
err := s.db.WithContext(ctx).Where("event_id = ?", event.EventID).First(&existing).Error
|
||||||
|
if err == nil {
|
||||||
|
return &existing, existing.Status == statusSuccess, nil
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.db.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND user_id = ?", event.SysOrigin, event.UserID).
|
||||||
|
First(&existing).Error
|
||||||
|
if err == nil {
|
||||||
|
return &existing, existing.Status == statusSuccess, nil
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := utils.NextID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
record := model.FirstRechargeRewardGrantRecord{
|
||||||
|
ID: id,
|
||||||
|
EventID: event.EventID,
|
||||||
|
ConfigID: config.ID,
|
||||||
|
LevelID: level.ID,
|
||||||
|
SysOrigin: config.SysOrigin,
|
||||||
|
UserID: event.UserID,
|
||||||
|
RechargeAmountCents: event.AmountCents,
|
||||||
|
RechargeThresholdCents: level.RechargeAmountCents,
|
||||||
|
Level: level.Level,
|
||||||
|
PayPlatform: event.PayPlatform,
|
||||||
|
PaymentMethod: event.PaymentMethod,
|
||||||
|
SourceOrderID: event.SourceOrderID,
|
||||||
|
RewardGroupID: level.RewardGroupID,
|
||||||
|
RewardGroupName: level.RewardGroupName,
|
||||||
|
RewardGroupTrackID: id,
|
||||||
|
Status: statusPending,
|
||||||
|
RewardGroupStatus: statusPending,
|
||||||
|
EventTime: event.OccurredAt,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}
|
||||||
|
if profile, err := s.java.GetUserProfile(ctx, event.UserID); err == nil {
|
||||||
|
record.Account = strings.TrimSpace(profile.Account)
|
||||||
|
record.UserAvatar = strings.TrimSpace(profile.UserAvatar)
|
||||||
|
record.UserNickname = strings.TrimSpace(profile.UserNickname)
|
||||||
|
record.CountryCode = strings.TrimSpace(profile.CountryCode)
|
||||||
|
record.CountryName = strings.TrimSpace(profile.CountryName)
|
||||||
|
}
|
||||||
|
if err := s.db.WithContext(ctx).Create(&record).Error; err != nil {
|
||||||
|
if isDuplicateError(err) {
|
||||||
|
if reload, ok, reloadErr := s.loadExistingGrantRecord(ctx, event); reloadErr != nil {
|
||||||
|
return nil, false, reloadErr
|
||||||
|
} else if ok {
|
||||||
|
return &reload, reload.Status == statusSuccess, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return &record, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadExistingGrantRecord(ctx context.Context, event normalizedRechargeEvent) (model.FirstRechargeRewardGrantRecord, bool, error) {
|
||||||
|
var existing model.FirstRechargeRewardGrantRecord
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("event_id = ? OR (sys_origin = ? AND user_id = ?)", event.EventID, event.SysOrigin, event.UserID).
|
||||||
|
First(&existing).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return model.FirstRechargeRewardGrantRecord{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return model.FirstRechargeRewardGrantRecord{}, false, err
|
||||||
|
}
|
||||||
|
return existing, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) dispatchRewardGroup(ctx context.Context, record *model.FirstRechargeRewardGrantRecord) error {
|
||||||
|
if record.RewardGroupID <= 0 {
|
||||||
|
return NewAppError(http.StatusBadRequest, "reward_group_missing", "reward group is missing")
|
||||||
|
}
|
||||||
|
trackID := record.RewardGroupTrackID
|
||||||
|
if trackID <= 0 {
|
||||||
|
trackID = record.ID
|
||||||
|
}
|
||||||
|
return s.java.SendActivityReward(ctx, integration.SendActivityRewardRequest{
|
||||||
|
TrackID: trackID,
|
||||||
|
Origin: firstRechargeRewardOrigin,
|
||||||
|
SysOrigin: record.SysOrigin,
|
||||||
|
SourceGroupID: record.RewardGroupID,
|
||||||
|
AcceptUserID: record.UserID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateGrantRecordSuccess(ctx context.Context, recordID int64) error {
|
||||||
|
now := time.Now()
|
||||||
|
return s.db.WithContext(ctx).
|
||||||
|
Model(&model.FirstRechargeRewardGrantRecord{}).
|
||||||
|
Where("id = ?", recordID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": statusSuccess,
|
||||||
|
"reward_group_status": statusSuccess,
|
||||||
|
"last_error": "",
|
||||||
|
"sent_at": now,
|
||||||
|
"update_time": now,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateGrantRecordFailed(ctx context.Context, recordID int64, cause error) error {
|
||||||
|
return s.db.WithContext(ctx).
|
||||||
|
Model(&model.FirstRechargeRewardGrantRecord{}).
|
||||||
|
Where("id = ?", recordID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": statusFailed,
|
||||||
|
"reward_group_status": statusFailed,
|
||||||
|
"retry_count": gorm.Expr("retry_count + 1"),
|
||||||
|
"last_error": truncateMessage(cause.Error(), 1024),
|
||||||
|
"update_time": time.Now(),
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) pushRewardNotice(ctx context.Context, record *model.FirstRechargeRewardGrantRecord) error {
|
||||||
|
expand := map[string]any{
|
||||||
|
"scene": "FIRST_RECHARGE_REWARD",
|
||||||
|
"showRewardPopup": true,
|
||||||
|
"userId": record.UserID,
|
||||||
|
"sysOrigin": record.SysOrigin,
|
||||||
|
"eventId": record.EventID,
|
||||||
|
"recordId": fmt.Sprintf("%d", record.ID),
|
||||||
|
"level": record.Level,
|
||||||
|
"rechargeAmount": formatAmountCents(record.RechargeAmountCents),
|
||||||
|
"rechargeAmountCents": record.RechargeAmountCents,
|
||||||
|
"rewardGroupId": fmt.Sprintf("%d", record.RewardGroupID),
|
||||||
|
"rewardGroupName": record.RewardGroupName,
|
||||||
|
"payPlatform": record.PayPlatform,
|
||||||
|
"paymentMethod": record.PaymentMethod,
|
||||||
|
"noticeType": firstRechargeRewardNoticeType,
|
||||||
|
}
|
||||||
|
return s.java.SendOfficialNoticeCustomize(ctx, integration.OfficialNoticeCustomizeRequest{
|
||||||
|
ToAccounts: []int64{record.UserID},
|
||||||
|
NoticeType: firstRechargeRewardNoticeType,
|
||||||
|
Title: firstRechargeRewardNoticeTitle,
|
||||||
|
Content: firstRechargeRewardNoticeContent,
|
||||||
|
Expand: expand,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRechargeEventPayload(payload string, expectedTag string) (RechargeEvent, bool, error) {
|
||||||
|
var event RechargeEvent
|
||||||
|
if err := json.Unmarshal([]byte(payload), &event); err != nil {
|
||||||
|
return event, false, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(event.EventID) != "" || event.UserID.Int64() > 0 {
|
||||||
|
return event, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelope rechargeMessageEnvelope
|
||||||
|
if err := json.Unmarshal([]byte(payload), &envelope); err != nil {
|
||||||
|
return RechargeEvent{}, false, err
|
||||||
|
}
|
||||||
|
if shouldSkipEnvelope(envelope.Tag, expectedTag) {
|
||||||
|
return RechargeEvent{}, true, nil
|
||||||
|
}
|
||||||
|
bodyPayload, ok, err := envelope.bodyPayload()
|
||||||
|
if err != nil || !ok {
|
||||||
|
return RechargeEvent{}, false, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(bodyPayload), &event); err != nil {
|
||||||
|
return RechargeEvent{}, false, err
|
||||||
|
}
|
||||||
|
return event, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type rechargeMessageEnvelope struct {
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Body json.RawMessage `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e rechargeMessageEnvelope) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAcceptedPaymentMethod(paymentMethod string, _ string) bool {
|
||||||
|
paymentMethod = strings.ToUpper(strings.TrimSpace(paymentMethod))
|
||||||
|
return paymentMethod == paymentMethodGoogle || paymentMethod == paymentMethodThirdParty
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstPositiveAmountCents(values ...string) int64 {
|
||||||
|
for _, value := range values {
|
||||||
|
cents, err := parseAmountCents(value)
|
||||||
|
if err == nil && cents > 0 {
|
||||||
|
return cents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEventTime(raw string) time.Time {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if t, err := time.Parse(time.RFC3339Nano, raw); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
if t, err := time.Parse("2006-01-02 15:04:05", raw); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmpty(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDuplicateError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
text := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(text, "duplicate") || strings.Contains(text, "unique constraint")
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateMessage(message string, limit int) string {
|
||||||
|
message = strings.TrimSpace(message)
|
||||||
|
if limit <= 0 || len(message) <= limit {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return message[:limit]
|
||||||
|
}
|
||||||
107
internal/service/firstrechargereward/home.go
Normal file
107
internal/service/firstrechargereward/home.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
activityStatusNotConfigured = "NOT_CONFIGURED"
|
||||||
|
activityStatusDisabled = "DISABLED"
|
||||||
|
activityStatusOngoing = "ONGOING"
|
||||||
|
activityStatusPending = "PENDING"
|
||||||
|
activityStatusRewarded = "REWARDED"
|
||||||
|
activityStatusFailed = "FAILED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHome returns app-facing first recharge reward config and the user's grant state.
|
||||||
|
func (s *Service) GetHome(ctx context.Context, user AuthUser) (*HomeResponse, error) {
|
||||||
|
sysOrigin, err := requireUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.java == nil {
|
||||||
|
return nil, NewAppError(http.StatusServiceUnavailable, "java_gateway_unavailable", "java gateway is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle, err := s.loadConfigBundle(ctx, sysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bundle.Config == nil {
|
||||||
|
return &HomeResponse{
|
||||||
|
Configured: false,
|
||||||
|
Enabled: false,
|
||||||
|
ActivityStatus: activityStatusNotConfigured,
|
||||||
|
UserID: user.UserID,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
LevelConfigs: []LevelPayload{},
|
||||||
|
HasRewardRecord: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardItems := s.loadRewardItemsMap(ctx, bundle.Levels)
|
||||||
|
resp := &HomeResponse{
|
||||||
|
Configured: true,
|
||||||
|
Enabled: bundle.Config.Enabled,
|
||||||
|
ActivityStatus: activityStatusOngoing,
|
||||||
|
UserID: user.UserID,
|
||||||
|
SysOrigin: bundle.Config.SysOrigin,
|
||||||
|
LevelConfigs: buildLevelPayloads(bundle.Levels, rewardItems, 0, 0, false),
|
||||||
|
HasRewardRecord: false,
|
||||||
|
}
|
||||||
|
if !bundle.Config.Enabled {
|
||||||
|
resp.ActivityStatus = activityStatusDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
record, ok, err := s.loadUserGrantRecord(ctx, bundle.Config.SysOrigin, user.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.HasRewardRecord = true
|
||||||
|
resp.RewardStatus = record.Status
|
||||||
|
resp.Rewarded = record.Status == statusSuccess
|
||||||
|
resp.MatchedLevel = record.Level
|
||||||
|
resp.RechargeAmount = formatAmountCents(record.RechargeAmountCents)
|
||||||
|
resp.RechargeAmountCents = record.RechargeAmountCents
|
||||||
|
resp.RewardGroupID = record.RewardGroupID
|
||||||
|
resp.RewardGroupName = record.RewardGroupName
|
||||||
|
resp.RewardItems = rewardItems[record.RewardGroupID]
|
||||||
|
resp.LevelConfigs = buildLevelPayloads(bundle.Levels, rewardItems, record.RechargeAmountCents, record.Level, false)
|
||||||
|
|
||||||
|
switch record.Status {
|
||||||
|
case statusSuccess:
|
||||||
|
resp.ActivityStatus = activityStatusRewarded
|
||||||
|
case statusPending:
|
||||||
|
resp.ActivityStatus = activityStatusPending
|
||||||
|
case statusFailed:
|
||||||
|
resp.ActivityStatus = activityStatusFailed
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadUserGrantRecord(ctx context.Context, sysOrigin string, userID int64) (model.FirstRechargeRewardGrantRecord, bool, error) {
|
||||||
|
if userID <= 0 {
|
||||||
|
return model.FirstRechargeRewardGrantRecord{}, false, nil
|
||||||
|
}
|
||||||
|
var record model.FirstRechargeRewardGrantRecord
|
||||||
|
err := s.db.WithContext(ctx).
|
||||||
|
Where("sys_origin = ? AND user_id = ?", s.normalizeSysOrigin(sysOrigin), userID).
|
||||||
|
First(&record).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return model.FirstRechargeRewardGrantRecord{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return model.FirstRechargeRewardGrantRecord{}, false, err
|
||||||
|
}
|
||||||
|
return record, true, nil
|
||||||
|
}
|
||||||
139
internal/service/firstrechargereward/lifecycle.go
Normal file
139
internal/service/firstrechargereward/lifecycle.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
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 starts the first recharge reward RocketMQ consumer.
|
||||||
|
func (s *Service) StartMessageConsumer(ctx context.Context) error {
|
||||||
|
if !s.cfg.FirstRechargeReward.MQ.Enabled {
|
||||||
|
log.Printf("first recharge reward rocketmq consumer disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.cfg.FirstRechargeReward.MQ.Endpoint) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.FirstRechargeReward.MQ.AccessKey) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.FirstRechargeReward.MQ.AccessSecret) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.FirstRechargeReward.MQ.Topic) == "" ||
|
||||||
|
strings.TrimSpace(s.cfg.FirstRechargeReward.MQ.ConsumerGroup) == "" {
|
||||||
|
log.Printf("first recharge reward 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 first recharge reward 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.FirstRechargeReward.MQ.Tag); tag != "" && tag != "*" {
|
||||||
|
filter = rmq.NewFilterExpression(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
consumer, err := rmq.NewPushConsumer(&rmq.Config{
|
||||||
|
Endpoint: s.cfg.FirstRechargeReward.MQ.Endpoint,
|
||||||
|
NameSpace: s.cfg.FirstRechargeReward.MQ.Namespace,
|
||||||
|
ConsumerGroup: s.cfg.FirstRechargeReward.MQ.ConsumerGroup,
|
||||||
|
Credentials: &credentials.SessionCredentials{
|
||||||
|
AccessKey: s.cfg.FirstRechargeReward.MQ.AccessKey,
|
||||||
|
AccessSecret: s.cfg.FirstRechargeReward.MQ.AccessSecret,
|
||||||
|
SecurityToken: s.cfg.FirstRechargeReward.MQ.SecurityToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rmq.WithPushSubscriptionExpressions(map[string]*rmq.FilterExpression{
|
||||||
|
s.cfg.FirstRechargeReward.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("first recharge reward 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("first recharge reward rocketmq consumer started. topic=%s group=%s tag=%s",
|
||||||
|
s.cfg.FirstRechargeReward.MQ.Topic,
|
||||||
|
s.cfg.FirstRechargeReward.MQ.ConsumerGroup,
|
||||||
|
s.cfg.FirstRechargeReward.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.ProcessRechargePayload(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 first recharge reward 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 first recharge reward message. messageId=%s code=%s err=%v", messageView.GetMessageId(), appErr.Code, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
146
internal/service/firstrechargereward/mapper.go
Normal file
146
internal/service/firstrechargereward/mapper.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configSnapshotFromModel(row model.FirstRechargeRewardConfig) configSnapshot {
|
||||||
|
return configSnapshot{
|
||||||
|
ID: row.ID,
|
||||||
|
SysOrigin: strings.ToUpper(strings.TrimSpace(row.SysOrigin)),
|
||||||
|
Enabled: row.Enabled,
|
||||||
|
UpdateTime: row.UpdateTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func levelSnapshotFromModel(row model.FirstRechargeRewardLevel) levelSnapshot {
|
||||||
|
return levelSnapshot{
|
||||||
|
ID: row.ID,
|
||||||
|
Level: row.Level,
|
||||||
|
RechargeAmountCents: row.RechargeAmountCents,
|
||||||
|
RewardGroupID: row.RewardGroupID,
|
||||||
|
RewardGroupName: strings.TrimSpace(row.RewardGroupName),
|
||||||
|
Enabled: row.Enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refConfigSnapshot(value configSnapshot) *configSnapshot {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLevelPayloads(
|
||||||
|
levels []levelSnapshot,
|
||||||
|
rewardItems map[int64][]RewardItem,
|
||||||
|
currentRechargeCents int64,
|
||||||
|
matchedLevel int,
|
||||||
|
admin bool,
|
||||||
|
) []LevelPayload {
|
||||||
|
payloads := make([]LevelPayload, 0, len(levels))
|
||||||
|
for _, level := range levels {
|
||||||
|
if !admin && !level.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payloads = append(payloads, LevelPayload{
|
||||||
|
ID: level.ID,
|
||||||
|
Level: level.Level,
|
||||||
|
RechargeAmount: formatAmountCents(level.RechargeAmountCents),
|
||||||
|
RechargeAmountCents: level.RechargeAmountCents,
|
||||||
|
RewardGroupID: level.RewardGroupID,
|
||||||
|
RewardGroupName: level.RewardGroupName,
|
||||||
|
RewardItems: rewardItems[level.RewardGroupID],
|
||||||
|
Enabled: level.Enabled,
|
||||||
|
Reached: currentRechargeCents >= level.RechargeAmountCents,
|
||||||
|
Current: matchedLevel > 0 && matchedLevel == level.Level,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return payloads
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickMatchedLevel(levels []levelSnapshot, rechargeAmountCents int64) (levelSnapshot, bool) {
|
||||||
|
var matched levelSnapshot
|
||||||
|
ok := false
|
||||||
|
for _, level := range levels {
|
||||||
|
if !level.Enabled || level.RechargeAmountCents <= 0 || level.RewardGroupID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rechargeAmountCents >= level.RechargeAmountCents && (!ok || level.RechargeAmountCents > matched.RechargeAmountCents) {
|
||||||
|
matched = level
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortLevels(levels []levelSnapshot) {
|
||||||
|
sort.Slice(levels, func(i, j int) bool {
|
||||||
|
if levels[i].RechargeAmountCents == levels[j].RechargeAmountCents {
|
||||||
|
return levels[i].Level < levels[j].Level
|
||||||
|
}
|
||||||
|
return levels[i].RechargeAmountCents < levels[j].RechargeAmountCents
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadRewardItemsMap(ctx context.Context, levels []levelSnapshot) map[int64][]RewardItem {
|
||||||
|
if s.java == nil {
|
||||||
|
return map[int64][]RewardItem{}
|
||||||
|
}
|
||||||
|
groupIDs := make([]int64, 0)
|
||||||
|
seen := make(map[int64]struct{})
|
||||||
|
for _, level := range levels {
|
||||||
|
if level.RewardGroupID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[level.RewardGroupID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[level.RewardGroupID] = struct{}{}
|
||||||
|
groupIDs = append(groupIDs, level.RewardGroupID)
|
||||||
|
}
|
||||||
|
sort.Slice(groupIDs, func(i, j int) bool { return groupIDs[i] < groupIDs[j] })
|
||||||
|
|
||||||
|
result := make(map[int64][]RewardItem, len(groupIDs))
|
||||||
|
for _, groupID := range groupIDs {
|
||||||
|
detail, err := s.java.GetRewardGroupDetail(ctx, groupID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[groupID] = rewardItemsFromGroup(detail)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func rewardItemsFromGroup(detail integration.RewardGroupDetail) []RewardItem {
|
||||||
|
items := make([]RewardItem, 0, len(detail.RewardConfigList))
|
||||||
|
for _, item := range detail.RewardConfigList {
|
||||||
|
items = append(items, RewardItem{
|
||||||
|
ID: int64(item.ID),
|
||||||
|
Type: strings.TrimSpace(item.Type),
|
||||||
|
Name: strings.TrimSpace(item.Name),
|
||||||
|
Content: strings.TrimSpace(item.Content),
|
||||||
|
Quantity: int64(item.Quantity),
|
||||||
|
Cover: strings.TrimSpace(item.Cover),
|
||||||
|
Remark: strings.TrimSpace(item.Remark),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDateTime(t time.Time) string {
|
||||||
|
if t.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPtrDateTime(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return formatDateTime(*t)
|
||||||
|
}
|
||||||
31
internal/service/firstrechargereward/money.go
Normal file
31
internal/service/firstrechargereward/money.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseAmountCents(raw string) (int64, error) {
|
||||||
|
value := strings.TrimSpace(raw)
|
||||||
|
if value == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
value = strings.TrimPrefix(value, "$")
|
||||||
|
parsed, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if parsed < 0 {
|
||||||
|
return 0, fmt.Errorf("amount must not be negative")
|
||||||
|
}
|
||||||
|
return int64(math.Round(parsed * 100)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAmountCents(cents int64) string {
|
||||||
|
if cents <= 0 {
|
||||||
|
return "0.00"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f", float64(cents)/100)
|
||||||
|
}
|
||||||
108
internal/service/firstrechargereward/records.go
Normal file
108
internal/service/firstrechargereward/records.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PageGrantRecords returns admin-facing first recharge reward grant records.
|
||||||
|
func (s *Service) PageGrantRecords(
|
||||||
|
ctx context.Context,
|
||||||
|
sysOrigin string,
|
||||||
|
status string,
|
||||||
|
userID int64,
|
||||||
|
cursor int,
|
||||||
|
limit int,
|
||||||
|
) (*GrantRecordPageResponse, error) {
|
||||||
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
||||||
|
status = strings.ToUpper(strings.TrimSpace(status))
|
||||||
|
cursor, limit = NormalizePage(cursor, limit)
|
||||||
|
|
||||||
|
query := s.db.WithContext(ctx).Model(&model.FirstRechargeRewardGrantRecord{}).
|
||||||
|
Where("sys_origin = ?", sysOrigin)
|
||||||
|
if status != "" {
|
||||||
|
query = query.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
if userID > 0 {
|
||||||
|
query = query.Where("user_id = ?", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []model.FirstRechargeRewardGrantRecord
|
||||||
|
if err := query.
|
||||||
|
Order("create_time desc").
|
||||||
|
Offset((cursor - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardItems := s.loadRewardItemsForRecords(ctx, rows)
|
||||||
|
views := make([]GrantRecordView, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
views = append(views, grantRecordViewFromModel(row, rewardItems[row.RewardGroupID]))
|
||||||
|
}
|
||||||
|
return &GrantRecordPageResponse{
|
||||||
|
Records: views,
|
||||||
|
Total: total,
|
||||||
|
Current: cursor,
|
||||||
|
Size: limit,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) loadRewardItemsForRecords(ctx context.Context, rows []model.FirstRechargeRewardGrantRecord) map[int64][]RewardItem {
|
||||||
|
levels := make([]levelSnapshot, 0, len(rows))
|
||||||
|
seen := make(map[int64]struct{})
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.RewardGroupID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[row.RewardGroupID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[row.RewardGroupID] = struct{}{}
|
||||||
|
levels = append(levels, levelSnapshot{
|
||||||
|
RewardGroupID: row.RewardGroupID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return s.loadRewardItemsMap(ctx, levels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func grantRecordViewFromModel(row model.FirstRechargeRewardGrantRecord, rewardItems []RewardItem) GrantRecordView {
|
||||||
|
return GrantRecordView{
|
||||||
|
ID: row.ID,
|
||||||
|
EventID: row.EventID,
|
||||||
|
UserID: row.UserID,
|
||||||
|
Account: row.Account,
|
||||||
|
UserAvatar: row.UserAvatar,
|
||||||
|
UserNickname: row.UserNickname,
|
||||||
|
CountryCode: row.CountryCode,
|
||||||
|
CountryName: row.CountryName,
|
||||||
|
SysOrigin: row.SysOrigin,
|
||||||
|
RechargeAmount: formatAmountCents(row.RechargeAmountCents),
|
||||||
|
RechargeAmountCents: row.RechargeAmountCents,
|
||||||
|
RechargeThreshold: formatAmountCents(row.RechargeThresholdCents),
|
||||||
|
RechargeThresholdCents: row.RechargeThresholdCents,
|
||||||
|
Level: row.Level,
|
||||||
|
PayPlatform: row.PayPlatform,
|
||||||
|
PaymentMethod: row.PaymentMethod,
|
||||||
|
SourceOrderID: row.SourceOrderID,
|
||||||
|
RewardGroupID: row.RewardGroupID,
|
||||||
|
RewardGroupName: row.RewardGroupName,
|
||||||
|
RewardItems: rewardItems,
|
||||||
|
Status: row.Status,
|
||||||
|
RewardGroupStatus: row.RewardGroupStatus,
|
||||||
|
RetryCount: row.RetryCount,
|
||||||
|
LastError: row.LastError,
|
||||||
|
EventTime: formatDateTime(row.EventTime),
|
||||||
|
SentAt: formatPtrDateTime(row.SentAt),
|
||||||
|
CreateTime: formatDateTime(row.CreateTime),
|
||||||
|
UpdateTime: formatDateTime(row.UpdateTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
286
internal/service/firstrechargereward/service_test.go
Normal file
286
internal/service/firstrechargereward/service_test.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSaveConfigReturnsRewardItems(t *testing.T) {
|
||||||
|
service, gateway, _ := newTestService(t)
|
||||||
|
gateway.groups[1001] = testRewardGroup(1001, "首冲礼包")
|
||||||
|
|
||||||
|
req := mustDecodeSaveConfigRequest(t, `{
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"enabled": true,
|
||||||
|
"levelConfigs": [
|
||||||
|
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
resp, err := service.SaveConfig(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if !resp.Configured || !resp.Enabled {
|
||||||
|
t.Fatalf("SaveConfig() configured/enabled = %v/%v", resp.Configured, resp.Enabled)
|
||||||
|
}
|
||||||
|
if len(resp.LevelConfigs) != 1 {
|
||||||
|
t.Fatalf("level count = %d", len(resp.LevelConfigs))
|
||||||
|
}
|
||||||
|
level := resp.LevelConfigs[0]
|
||||||
|
if level.RechargeAmountCents != 999 || level.RewardGroupID != 1001 {
|
||||||
|
t.Fatalf("level payload = %+v", level)
|
||||||
|
}
|
||||||
|
if len(level.RewardItems) != 1 || level.RewardItems[0].Name != "首冲礼包奖励" {
|
||||||
|
t.Fatalf("reward items = %+v", level.RewardItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
||||||
|
service, gateway, db := newTestService(t)
|
||||||
|
gateway.groups[1001] = testRewardGroup(1001, "首冲礼包")
|
||||||
|
gateway.profiles[123] = integration.UserProfile{
|
||||||
|
ID: integration.Int64Value(123),
|
||||||
|
Account: "100123",
|
||||||
|
UserNickname: "tester",
|
||||||
|
CountryCode: "SA",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"enabled": true,
|
||||||
|
"levelConfigs": [
|
||||||
|
{"level": 1, "rechargeAmount": "5.00", "rewardGroupId": "1001", "rewardGroupName": "首冲礼包", "enabled": true}
|
||||||
|
]
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsupported, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
||||||
|
"eventId": "RECHARGE_SUCCESS:APPLE:1",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"userId": "123",
|
||||||
|
"amountCents": "999",
|
||||||
|
"payPlatform": "APPLE",
|
||||||
|
"paymentMethod": "APPLE",
|
||||||
|
"sourceOrderId": "1"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unsupported ProcessRechargeEvent() error = %v", err)
|
||||||
|
}
|
||||||
|
if unsupported.Processed || unsupported.Reason != "unsupported_payment_method" {
|
||||||
|
t.Fatalf("unsupported response = %+v", unsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
lowAmount, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
||||||
|
"eventId": "RECHARGE_SUCCESS:GOOGLE:LOW",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"userId": "123",
|
||||||
|
"amountCents": "499",
|
||||||
|
"payPlatform": "GOOGLE",
|
||||||
|
"paymentMethod": "GOOGLE",
|
||||||
|
"sourceOrderId": "LOW"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("low amount ProcessRechargeEvent() error = %v", err)
|
||||||
|
}
|
||||||
|
if lowAmount.Processed || lowAmount.Reason != "amount_not_reached" {
|
||||||
|
t.Fatalf("low amount response = %+v", lowAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
granted, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
||||||
|
"eventId": "RECHARGE_SUCCESS:GOOGLE:OK",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"userId": "123",
|
||||||
|
"amountCents": "999",
|
||||||
|
"payPlatform": "GOOGLE",
|
||||||
|
"paymentMethod": "GOOGLE",
|
||||||
|
"sourceOrderId": "OK",
|
||||||
|
"occurredAt": "2026-05-21T12:00:00Z"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("grant ProcessRechargeEvent() error = %v", err)
|
||||||
|
}
|
||||||
|
if !granted.Processed || granted.Status != statusSuccess || granted.Level != 1 {
|
||||||
|
t.Fatalf("grant response = %+v", granted)
|
||||||
|
}
|
||||||
|
if len(gateway.rewardRequests) != 1 {
|
||||||
|
t.Fatalf("reward request count = %d", len(gateway.rewardRequests))
|
||||||
|
}
|
||||||
|
if got := gateway.rewardRequests[0].Origin; got != firstRechargeRewardOrigin {
|
||||||
|
t.Fatalf("reward origin = %s", got)
|
||||||
|
}
|
||||||
|
if len(gateway.notices) != 1 || gateway.notices[0].NoticeType != firstRechargeRewardNoticeType {
|
||||||
|
t.Fatalf("notices = %+v", gateway.notices)
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicate, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
||||||
|
"eventId": "RECHARGE_SUCCESS:GOOGLE:OK",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"userId": "123",
|
||||||
|
"amountCents": "999",
|
||||||
|
"payPlatform": "GOOGLE",
|
||||||
|
"paymentMethod": "GOOGLE",
|
||||||
|
"sourceOrderId": "OK"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("duplicate ProcessRechargeEvent() error = %v", err)
|
||||||
|
}
|
||||||
|
if !duplicate.Processed || duplicate.Status != statusSuccess {
|
||||||
|
t.Fatalf("duplicate response = %+v", duplicate)
|
||||||
|
}
|
||||||
|
if len(gateway.rewardRequests) != 1 {
|
||||||
|
t.Fatalf("duplicate reward request count = %d", len(gateway.rewardRequests))
|
||||||
|
}
|
||||||
|
|
||||||
|
thirdPartyUnknownPlatform, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
||||||
|
"eventId": "RECHARGE_SUCCESS:THIRD_PARTY:CUSTOM",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"userId": "456",
|
||||||
|
"amountCents": "999",
|
||||||
|
"payPlatform": "CUSTOM_PAY",
|
||||||
|
"paymentMethod": "THIRD_PARTY",
|
||||||
|
"sourceOrderId": "CUSTOM"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("third party ProcessRechargeEvent() error = %v", err)
|
||||||
|
}
|
||||||
|
if !thirdPartyUnknownPlatform.Processed || thirdPartyUnknownPlatform.Status != statusSuccess {
|
||||||
|
t.Fatalf("third party response = %+v", thirdPartyUnknownPlatform)
|
||||||
|
}
|
||||||
|
if len(gateway.rewardRequests) != 2 {
|
||||||
|
t.Fatalf("third party reward request count = %d", len(gateway.rewardRequests))
|
||||||
|
}
|
||||||
|
|
||||||
|
googleDifferentPlatform, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
||||||
|
"eventId": "RECHARGE_SUCCESS:GOOGLE:CUSTOM",
|
||||||
|
"sysOrigin": "LIKEI",
|
||||||
|
"userId": "789",
|
||||||
|
"amountCents": "999",
|
||||||
|
"payPlatform": "CUSTOM_GOOGLE_GATEWAY",
|
||||||
|
"paymentMethod": "GOOGLE",
|
||||||
|
"sourceOrderId": "CUSTOM_GOOGLE"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("google ProcessRechargeEvent() error = %v", err)
|
||||||
|
}
|
||||||
|
if !googleDifferentPlatform.Processed || googleDifferentPlatform.Status != statusSuccess {
|
||||||
|
t.Fatalf("google response = %+v", googleDifferentPlatform)
|
||||||
|
}
|
||||||
|
if len(gateway.rewardRequests) != 3 {
|
||||||
|
t.Fatalf("google reward request count = %d", len(gateway.rewardRequests))
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil {
|
||||||
|
t.Fatalf("count grant records: %v", err)
|
||||||
|
}
|
||||||
|
if count != 3 {
|
||||||
|
t.Fatalf("grant record count = %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestService(t *testing.T) (*Service, *fakeGateway, *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.FirstRechargeRewardConfig{},
|
||||||
|
&model.FirstRechargeRewardLevel{},
|
||||||
|
&model.FirstRechargeRewardGrantRecord{},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("migrate sqlite: %v", err)
|
||||||
|
}
|
||||||
|
gateway := &fakeGateway{
|
||||||
|
groups: map[int64]integration.RewardGroupDetail{},
|
||||||
|
profiles: map[int64]integration.UserProfile{},
|
||||||
|
}
|
||||||
|
service := NewService(config.Config{
|
||||||
|
FirstRechargeReward: config.FirstRechargeRewardConfig{
|
||||||
|
DefaultSysOrigin: "LIKEI",
|
||||||
|
},
|
||||||
|
}, db, gateway)
|
||||||
|
return service, gateway, db
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustDecodeSaveConfigRequest(t *testing.T, raw string) SaveConfigRequest {
|
||||||
|
t.Helper()
|
||||||
|
var req SaveConfigRequest
|
||||||
|
if err := json.Unmarshal([]byte(raw), &req); err != nil {
|
||||||
|
t.Fatalf("decode SaveConfigRequest: %v", err)
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustDecodeRechargeEvent(t *testing.T, raw string) RechargeEvent {
|
||||||
|
t.Helper()
|
||||||
|
var event RechargeEvent
|
||||||
|
if err := json.Unmarshal([]byte(raw), &event); err != nil {
|
||||||
|
t.Fatalf("decode RechargeEvent: %v", err)
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRewardGroup(id int64, name string) integration.RewardGroupDetail {
|
||||||
|
return integration.RewardGroupDetail{
|
||||||
|
ID: integration.Int64Value(id),
|
||||||
|
Name: name,
|
||||||
|
RewardConfigList: []integration.RewardGroupItem{
|
||||||
|
{
|
||||||
|
ID: integration.Int64Value(11),
|
||||||
|
Type: "PROPS",
|
||||||
|
Name: name + "奖励",
|
||||||
|
Content: "10001",
|
||||||
|
Quantity: integration.Int64Value(1),
|
||||||
|
Cover: "https://example.com/reward.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeGateway struct {
|
||||||
|
groups map[int64]integration.RewardGroupDetail
|
||||||
|
profiles map[int64]integration.UserProfile
|
||||||
|
rewardRequests []integration.SendActivityRewardRequest
|
||||||
|
notices []integration.OfficialNoticeCustomizeRequest
|
||||||
|
cacheRemoved []int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *fakeGateway) GetRewardGroupDetail(_ context.Context, groupID int64) (integration.RewardGroupDetail, error) {
|
||||||
|
if group, ok := g.groups[groupID]; ok {
|
||||||
|
return group, nil
|
||||||
|
}
|
||||||
|
return testRewardGroup(groupID, "默认奖励组"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *fakeGateway) GetUserProfile(_ context.Context, userID int64) (integration.UserProfile, error) {
|
||||||
|
if profile, ok := g.profiles[userID]; ok {
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
return integration.UserProfile{ID: integration.Int64Value(userID)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *fakeGateway) SendActivityReward(_ context.Context, req integration.SendActivityRewardRequest) error {
|
||||||
|
g.rewardRequests = append(g.rewardRequests, req)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *fakeGateway) SendOfficialNoticeCustomize(_ context.Context, req integration.OfficialNoticeCustomizeRequest) error {
|
||||||
|
g.notices = append(g.notices, req)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *fakeGateway) RemoveUserProfileCacheAll(_ context.Context, userID int64) error {
|
||||||
|
g.cacheRemoved = append(g.cacheRemoved, userID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
356
internal/service/firstrechargereward/types.go
Normal file
356
internal/service/firstrechargereward/types.go
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
package firstrechargereward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/common"
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
|
||||||
|
rmq "github.com/apache/rocketmq-clients/golang/v5"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
statusPending = "PENDING"
|
||||||
|
statusSuccess = "SUCCESS"
|
||||||
|
statusFailed = "FAILED"
|
||||||
|
|
||||||
|
paymentMethodGoogle = "GOOGLE"
|
||||||
|
paymentMethodThirdParty = "THIRD_PARTY"
|
||||||
|
|
||||||
|
firstRechargeRewardOrigin = "FIRST_CHARGE_REWARD"
|
||||||
|
firstRechargeRewardNoticeType = "FIRST_RECHARGE_REWARD_GRANTED"
|
||||||
|
firstRechargeRewardNoticeTitle = "First recharge reward"
|
||||||
|
firstRechargeRewardNoticeContent = "Reward granted"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppError = common.AppError
|
||||||
|
type AuthUser = common.AuthUser
|
||||||
|
|
||||||
|
var NewAppError = common.NewAppError
|
||||||
|
|
||||||
|
type dbHandle interface {
|
||||||
|
WithContext(ctx context.Context) *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type gateway interface {
|
||||||
|
GetRewardGroupDetail(ctx context.Context, groupID int64) (integration.RewardGroupDetail, error)
|
||||||
|
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
||||||
|
SendActivityReward(ctx context.Context, req integration.SendActivityRewardRequest) error
|
||||||
|
SendOfficialNoticeCustomize(ctx context.Context, req integration.OfficialNoticeCustomizeRequest) error
|
||||||
|
RemoveUserProfileCacheAll(ctx context.Context, userID int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service owns first recharge reward config, MQ consumption, and reward dispatch.
|
||||||
|
type Service struct {
|
||||||
|
cfg config.Config
|
||||||
|
db dbHandle
|
||||||
|
java gateway
|
||||||
|
rocketConsumer rmq.PushConsumer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(cfg config.Config, db dbHandle, java gateway) *Service {
|
||||||
|
return &Service{cfg: cfg, db: db, java: java}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LevelPayload struct {
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
RechargeAmount string `json:"rechargeAmount"`
|
||||||
|
RechargeAmountCents int64 `json:"rechargeAmountCents"`
|
||||||
|
RewardGroupID int64 `json:"rewardGroupId,string"`
|
||||||
|
RewardGroupName string `json:"rewardGroupName,omitempty"`
|
||||||
|
RewardItems []RewardItem `json:"rewardItems"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Reached bool `json:"reached"`
|
||||||
|
Current bool `json:"current"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LevelInput struct {
|
||||||
|
ID flexibleInt64 `json:"id"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
RechargeAmount flexibleAmount `json:"rechargeAmount"`
|
||||||
|
RechargeAmountCents int64 `json:"rechargeAmountCents"`
|
||||||
|
RewardGroupID flexibleInt64 `json:"rewardGroupId"`
|
||||||
|
RewardGroupName string `json:"rewardGroupName"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigResponse struct {
|
||||||
|
Configured bool `json:"configured"`
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
UpdateTime string `json:"updateTime,omitempty"`
|
||||||
|
LevelConfigs []LevelPayload `json:"levelConfigs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SaveConfigRequest struct {
|
||||||
|
ID flexibleInt64 `json:"id"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
LevelConfigs []LevelInput `json:"levelConfigs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HomeResponse struct {
|
||||||
|
Configured bool `json:"configured"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
ActivityStatus string `json:"activityStatus"`
|
||||||
|
UserID int64 `json:"userId,string"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
HasRewardRecord bool `json:"hasRewardRecord"`
|
||||||
|
RewardStatus string `json:"rewardStatus,omitempty"`
|
||||||
|
Rewarded bool `json:"rewarded"`
|
||||||
|
MatchedLevel int `json:"matchedLevel,omitempty"`
|
||||||
|
RechargeAmount string `json:"rechargeAmount,omitempty"`
|
||||||
|
RechargeAmountCents int64 `json:"rechargeAmountCents,omitempty"`
|
||||||
|
RewardGroupID int64 `json:"rewardGroupId,string,omitempty"`
|
||||||
|
RewardGroupName string `json:"rewardGroupName,omitempty"`
|
||||||
|
RewardItems []RewardItem `json:"rewardItems,omitempty"`
|
||||||
|
LevelConfigs []LevelPayload `json:"levelConfigs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantRecordPageResponse struct {
|
||||||
|
Records []GrantRecordView `json:"records"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Current int `json:"current"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantRecordView struct {
|
||||||
|
ID int64 `json:"id,string"`
|
||||||
|
EventID string `json:"eventId"`
|
||||||
|
UserID int64 `json:"userId,string"`
|
||||||
|
Account string `json:"account,omitempty"`
|
||||||
|
UserAvatar string `json:"userAvatar,omitempty"`
|
||||||
|
UserNickname string `json:"userNickname,omitempty"`
|
||||||
|
CountryCode string `json:"countryCode,omitempty"`
|
||||||
|
CountryName string `json:"countryName,omitempty"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
RechargeAmount string `json:"rechargeAmount"`
|
||||||
|
RechargeAmountCents int64 `json:"rechargeAmountCents"`
|
||||||
|
RechargeThreshold string `json:"rechargeThreshold"`
|
||||||
|
RechargeThresholdCents int64 `json:"rechargeThresholdCents"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
PayPlatform string `json:"payPlatform"`
|
||||||
|
PaymentMethod string `json:"paymentMethod"`
|
||||||
|
SourceOrderID string `json:"sourceOrderId,omitempty"`
|
||||||
|
RewardGroupID int64 `json:"rewardGroupId,string"`
|
||||||
|
RewardGroupName string `json:"rewardGroupName,omitempty"`
|
||||||
|
RewardItems []RewardItem `json:"rewardItems"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RewardGroupStatus string `json:"rewardGroupStatus"`
|
||||||
|
RetryCount int `json:"retryCount"`
|
||||||
|
LastError string `json:"lastError,omitempty"`
|
||||||
|
EventTime string `json:"eventTime,omitempty"`
|
||||||
|
SentAt string `json:"sentAt,omitempty"`
|
||||||
|
CreateTime string `json:"createTime,omitempty"`
|
||||||
|
UpdateTime string `json:"updateTime,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RewardItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Quantity int64 `json:"quantity"`
|
||||||
|
Cover string `json:"cover"`
|
||||||
|
Remark string `json:"remark,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RechargeEvent struct {
|
||||||
|
EventID string `json:"eventId"`
|
||||||
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
UserID flexibleInt64 `json:"userId"`
|
||||||
|
AmountCents flexibleInt64 `json:"amountCents"`
|
||||||
|
Amount flexibleString `json:"amount"`
|
||||||
|
AmountUSD flexibleString `json:"amountUsd"`
|
||||||
|
USDAmount flexibleString `json:"usdAmount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
PayPlatform string `json:"payPlatform"`
|
||||||
|
PaymentMethod string `json:"paymentMethod"`
|
||||||
|
SourceOrderID flexibleString `json:"sourceOrderId"`
|
||||||
|
OrderID flexibleString `json:"orderId"`
|
||||||
|
OccurredAt flexibleString `json:"occurredAt"`
|
||||||
|
Payload json.RawMessage `json:"payload,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessRechargeResponse struct {
|
||||||
|
Processed bool `json:"processed"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
RecordID int64 `json:"recordId,string,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Level int `json:"level,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type configBundle struct {
|
||||||
|
Config *configSnapshot
|
||||||
|
Levels []levelSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
type configSnapshot struct {
|
||||||
|
ID int64
|
||||||
|
SysOrigin string
|
||||||
|
Enabled bool
|
||||||
|
UpdateTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type levelSnapshot struct {
|
||||||
|
ID int64
|
||||||
|
Level int
|
||||||
|
RechargeAmountCents int64
|
||||||
|
RewardGroupID int64
|
||||||
|
RewardGroupName string
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type normalizedRechargeEvent struct {
|
||||||
|
EventID string
|
||||||
|
SysOrigin string
|
||||||
|
UserID int64
|
||||||
|
AmountCents int64
|
||||||
|
Currency string
|
||||||
|
PayPlatform string
|
||||||
|
PaymentMethod string
|
||||||
|
SourceOrderID string
|
||||||
|
OccurredAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if raw == "" {
|
||||||
|
*v = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 (v flexibleInt64) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(strconv.FormatInt(int64(v), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
type flexibleString string
|
||||||
|
|
||||||
|
func (v *flexibleString) UnmarshalJSON(data []byte) error {
|
||||||
|
raw := strings.TrimSpace(string(data))
|
||||||
|
if raw == "" || raw == "null" {
|
||||||
|
*v = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, `"`) && strings.HasSuffix(raw, `"`) {
|
||||||
|
unquoted, err := strconv.Unquote(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*v = flexibleString(strings.TrimSpace(unquoted))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*v = flexibleString(raw)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v flexibleString) String() string {
|
||||||
|
return strings.TrimSpace(string(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
type flexibleAmount int64
|
||||||
|
|
||||||
|
func (v *flexibleAmount) 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)
|
||||||
|
}
|
||||||
|
cents, err := parseAmountCents(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*v = flexibleAmount(cents)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v flexibleAmount) Cents() int64 {
|
||||||
|
return int64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseInt64(raw string) int64 {
|
||||||
|
value, _ := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizePage(cursor int, limit int) (int, int) {
|
||||||
|
if cursor <= 0 {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
return cursor, limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) normalizeSysOrigin(sysOrigin string) string {
|
||||||
|
sysOrigin = strings.ToUpper(strings.TrimSpace(sysOrigin))
|
||||||
|
if sysOrigin != "" {
|
||||||
|
return sysOrigin
|
||||||
|
}
|
||||||
|
if fallback := strings.ToUpper(strings.TrimSpace(s.cfg.FirstRechargeReward.DefaultSysOrigin)); fallback != "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
if fallback := strings.ToUpper(strings.TrimSpace(s.cfg.RechargeReward.DefaultSysOrigin)); fallback != "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
if fallback := strings.ToUpper(strings.TrimSpace(s.cfg.WeekStar.DefaultSysOrigin)); fallback != "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return "LIKEI"
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireUser(user AuthUser) (string, error) {
|
||||||
|
if user.UserID <= 0 {
|
||||||
|
return "", NewAppError(http.StatusUnauthorized, "invalid_user", "user is required")
|
||||||
|
}
|
||||||
|
sysOrigin := strings.ToUpper(strings.TrimSpace(user.SysOrigin))
|
||||||
|
if sysOrigin == "" {
|
||||||
|
return "", NewAppError(http.StatusUnauthorized, "invalid_sys_origin", "authenticated sysOrigin is required")
|
||||||
|
}
|
||||||
|
return sysOrigin, nil
|
||||||
|
}
|
||||||
62
migrations/049_first_recharge_reward.sql
Normal file
62
migrations/049_first_recharge_reward.sql
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `first_recharge_reward_config` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键ID',
|
||||||
|
`sys_origin` varchar(32) NOT NULL COMMENT '来源系统',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用',
|
||||||
|
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_first_recharge_reward_sys_origin` (`sys_origin`),
|
||||||
|
KEY `idx_first_recharge_reward_enabled` (`enabled`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='首冲奖励配置';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `first_recharge_reward_level` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键ID',
|
||||||
|
`config_id` bigint NOT NULL COMMENT '首冲奖励主配置ID',
|
||||||
|
`level` int NOT NULL COMMENT '档位',
|
||||||
|
`recharge_amount_cents` bigint NOT NULL DEFAULT '0' COMMENT '充值金额门槛,单位为分',
|
||||||
|
`reward_group_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励资源组ID',
|
||||||
|
`reward_group_name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖励资源组名称快照',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
|
||||||
|
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_first_recharge_reward_level` (`config_id`, `level`),
|
||||||
|
UNIQUE KEY `uk_first_recharge_reward_amount` (`config_id`, `recharge_amount_cents`),
|
||||||
|
KEY `idx_first_recharge_reward_level_config` (`config_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='首冲奖励充值档位';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `first_recharge_reward_grant_record` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键ID',
|
||||||
|
`event_id` varchar(128) NOT NULL COMMENT '充值事件幂等ID',
|
||||||
|
`config_id` bigint NOT NULL COMMENT '首冲奖励主配置ID',
|
||||||
|
`level_id` bigint NOT NULL COMMENT '首冲奖励档位ID',
|
||||||
|
`sys_origin` varchar(32) NOT NULL COMMENT '来源系统',
|
||||||
|
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||||||
|
`account` varchar(64) NOT NULL DEFAULT '' COMMENT '用户账号快照',
|
||||||
|
`user_avatar` varchar(1024) NOT NULL DEFAULT '' COMMENT '用户头像快照',
|
||||||
|
`user_nickname` varchar(255) NOT NULL DEFAULT '' COMMENT '用户昵称快照',
|
||||||
|
`country_code` varchar(32) NOT NULL DEFAULT '' COMMENT '国家编码快照',
|
||||||
|
`country_name` varchar(128) NOT NULL DEFAULT '' COMMENT '国家名称快照',
|
||||||
|
`recharge_amount_cents` bigint NOT NULL DEFAULT '0' COMMENT '本次充值金额,单位为分',
|
||||||
|
`recharge_threshold_cents` bigint NOT NULL DEFAULT '0' COMMENT '命中档位门槛,单位为分',
|
||||||
|
`level` int NOT NULL COMMENT '命中档位',
|
||||||
|
`pay_platform` varchar(64) NOT NULL DEFAULT '' COMMENT '支付平台:GOOGLE/MIFA_PAY/PAYER_MAX等',
|
||||||
|
`payment_method` varchar(32) NOT NULL DEFAULT '' COMMENT '充值方式:GOOGLE/THIRD_PARTY',
|
||||||
|
`source_order_id` varchar(128) NOT NULL DEFAULT '' COMMENT '来源订单ID',
|
||||||
|
`reward_group_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励资源组ID',
|
||||||
|
`reward_group_name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖励资源组名称快照',
|
||||||
|
`reward_group_track_id` bigint NOT NULL DEFAULT '0' COMMENT '奖励资源组发放幂等追踪ID',
|
||||||
|
`status` varchar(32) NOT NULL DEFAULT 'PENDING' COMMENT '整体发放状态:PENDING/SUCCESS/FAILED',
|
||||||
|
`reward_group_status` varchar(32) NOT NULL DEFAULT 'PENDING' COMMENT '奖励组发放状态:PENDING/SUCCESS/FAILED',
|
||||||
|
`retry_count` int NOT NULL DEFAULT '0' COMMENT '重试次数',
|
||||||
|
`last_error` varchar(1024) NOT NULL DEFAULT '' COMMENT '最近一次失败原因',
|
||||||
|
`event_time` datetime(3) DEFAULT NULL COMMENT '充值事件时间',
|
||||||
|
`sent_at` datetime(3) DEFAULT NULL COMMENT '发放成功时间',
|
||||||
|
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_first_recharge_reward_event` (`event_id`),
|
||||||
|
UNIQUE KEY `uk_first_recharge_reward_user` (`sys_origin`, `user_id`),
|
||||||
|
KEY `idx_first_recharge_reward_status` (`sys_origin`, `user_id`, `status`),
|
||||||
|
KEY `idx_first_recharge_reward_order` (`source_order_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='首冲奖励发放记录';
|
||||||
Loading…
x
Reference in New Issue
Block a user