Compare commits
No commits in common. "f1b781e3be23892074a8d165b75b49b010f10e57" and "4cf4eba5ff430e189da32e25935e45a26d5cf764" have entirely different histories.
f1b781e3be
...
4cf4eba5ff
@ -235,15 +235,14 @@ type OfficialNoticeCustomizeRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RewardGroupItem struct {
|
type RewardGroupItem struct {
|
||||||
ID Int64Value `json:"id"`
|
ID Int64Value `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
DetailType string `json:"detailType"`
|
Name string `json:"name"`
|
||||||
Name string `json:"name"`
|
Content string `json:"content"`
|
||||||
Content string `json:"content"`
|
Quantity Int64Value `json:"quantity"`
|
||||||
Quantity Int64Value `json:"quantity"`
|
Cover string `json:"cover"`
|
||||||
Cover string `json:"cover"`
|
SourceURL string `json:"sourceUrl"`
|
||||||
SourceURL string `json:"sourceUrl"`
|
Remark string `json:"remark"`
|
||||||
Remark string `json:"remark"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RewardGroupDetail struct {
|
type RewardGroupDetail struct {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"chatapp3-golang/internal/config"
|
"chatapp3-golang/internal/config"
|
||||||
"chatapp3-golang/internal/repo"
|
|
||||||
"chatapp3-golang/internal/service/highwin"
|
"chatapp3-golang/internal/service/highwin"
|
||||||
"chatapp3-golang/internal/service/regionimgroup"
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
|
||||||
@ -15,7 +14,6 @@ import (
|
|||||||
func registerRegionIMGroupRoutes(
|
func registerRegionIMGroupRoutes(
|
||||||
engine *gin.Engine,
|
engine *gin.Engine,
|
||||||
cfg config.Config,
|
cfg config.Config,
|
||||||
repository *repo.Repository,
|
|
||||||
javaClient authGateway,
|
javaClient authGateway,
|
||||||
service *regionimgroup.Service,
|
service *regionimgroup.Service,
|
||||||
) {
|
) {
|
||||||
@ -25,7 +23,7 @@ func registerRegionIMGroupRoutes(
|
|||||||
|
|
||||||
registerRegionIMGroupAppRoutes(engine, javaClient, service)
|
registerRegionIMGroupAppRoutes(engine, javaClient, service)
|
||||||
registerRegionIMGroupInternalRoutes(engine, cfg, service)
|
registerRegionIMGroupInternalRoutes(engine, cfg, service)
|
||||||
registerRegionIMGroupAdminRoutes(engine, repository, javaClient, service)
|
registerRegionIMGroupAdminRoutes(engine, javaClient, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRegionIMGroupAppRoutes(engine *gin.Engine, javaClient authGateway, service *regionimgroup.Service) {
|
func registerRegionIMGroupAppRoutes(engine *gin.Engine, javaClient authGateway, service *regionimgroup.Service) {
|
||||||
@ -65,14 +63,10 @@ func registerRegionIMGroupInternalRoutes(engine *gin.Engine, cfg config.Config,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRegionIMGroupAdminRoutes(engine *gin.Engine, repository *repo.Repository, javaClient authGateway, service *regionimgroup.Service) {
|
func registerRegionIMGroupAdminRoutes(engine *gin.Engine, javaClient authGateway, service *regionimgroup.Service) {
|
||||||
registerRegionIMGroupAdminGroup(engine.Group("/region/config"), javaClient, service)
|
registerRegionIMGroupAdminGroup(engine.Group("/region/config"), javaClient, service)
|
||||||
registerRegionIMGroupAdminGroup(engine.Group("/resident-activity/voice-room-red-packet"), javaClient, service)
|
registerRegionIMGroupAdminGroup(engine.Group("/resident-activity/voice-room-red-packet"), javaClient, service)
|
||||||
var worker *highwin.BroadcastSimulationWorker
|
registerRegionBroadcastSimulationAdminGroup(engine.Group("/operate/region-broadcast"), javaClient, service)
|
||||||
if repository != nil {
|
|
||||||
worker = highwin.NewBroadcastSimulationWorker(repository.DB, repository.Redis, service)
|
|
||||||
}
|
|
||||||
registerRegionBroadcastSimulationAdminGroup(engine.Group("/operate/region-broadcast"), javaClient, service, worker)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRegionIMGroupAdminGroup(group *gin.RouterGroup, javaClient authGateway, service *regionimgroup.Service) {
|
func registerRegionIMGroupAdminGroup(group *gin.RouterGroup, javaClient authGateway, service *regionimgroup.Service) {
|
||||||
@ -113,7 +107,7 @@ func registerRegionIMGroupAdminGroup(group *gin.RouterGroup, javaClient authGate
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRegionBroadcastSimulationAdminGroup(group *gin.RouterGroup, javaClient authGateway, service *regionimgroup.Service, worker *highwin.BroadcastSimulationWorker) {
|
func registerRegionBroadcastSimulationAdminGroup(group *gin.RouterGroup, javaClient authGateway, service *regionimgroup.Service) {
|
||||||
group.Use(consoleAuthMiddleware(javaClient))
|
group.Use(consoleAuthMiddleware(javaClient))
|
||||||
group.POST("/simulate", func(c *gin.Context) {
|
group.POST("/simulate", func(c *gin.Context) {
|
||||||
var req highwin.BroadcastSimulationRequest
|
var req highwin.BroadcastSimulationRequest
|
||||||
@ -128,23 +122,4 @@ func registerRegionBroadcastSimulationAdminGroup(group *gin.RouterGroup, javaCli
|
|||||||
}
|
}
|
||||||
writeOK(c, resp)
|
writeOK(c, resp)
|
||||||
})
|
})
|
||||||
group.GET("/worker/status", func(c *gin.Context) {
|
|
||||||
writeOK(c, worker.Status())
|
|
||||||
})
|
|
||||||
group.POST("/worker/start", func(c *gin.Context) {
|
|
||||||
var req highwin.ContinuousSimulationRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
writeError(c, regionimgroup.NewAppError(http.StatusBadRequest, "bad_request", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := worker.Start(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
writeError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeOK(c, resp)
|
|
||||||
})
|
|
||||||
group.POST("/worker/stop", func(c *gin.Context) {
|
|
||||||
writeOK(c, worker.Stop())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,7 +93,7 @@ func NewRouter(
|
|||||||
registerVipRoutes(engine, javaClient, services.VIP)
|
registerVipRoutes(engine, javaClient, services.VIP)
|
||||||
registerWheelRoutes(engine, cfg, javaClient, services.Wheel)
|
registerWheelRoutes(engine, cfg, javaClient, services.Wheel)
|
||||||
registerSmashEggRoutes(engine, javaClient, services.SmashEgg)
|
registerSmashEggRoutes(engine, javaClient, services.SmashEgg)
|
||||||
registerRegionIMGroupRoutes(engine, cfg, repository, javaClient, services.RegionIMGroup)
|
registerRegionIMGroupRoutes(engine, cfg, javaClient, services.RegionIMGroup)
|
||||||
registerVoiceRoomRedPacketRoutes(engine, javaClient, services.VoiceRoomRedPacket)
|
registerVoiceRoomRedPacketRoutes(engine, javaClient, services.VoiceRoomRedPacket)
|
||||||
registerVoiceRoomRocketRoutes(engine, cfg, javaClient, services.VoiceRoomRocket)
|
registerVoiceRoomRocketRoutes(engine, cfg, javaClient, services.VoiceRoomRocket)
|
||||||
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
registerWeekStarRoutes(engine, javaClient, services.WeekStar)
|
||||||
|
|||||||
@ -41,9 +41,6 @@ func (s *Service) ProcessRechargeEvent(ctx context.Context, event RechargeEvent)
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !isFirstRechargeRewardAppVersionSupported(normalized.AppVersion) {
|
|
||||||
return &ProcessRechargeResponse{Processed: false, Reason: "app_version_not_supported"}, nil
|
|
||||||
}
|
|
||||||
if !isAcceptedPaymentMethod(normalized.PaymentMethod, normalized.PayPlatform) {
|
if !isAcceptedPaymentMethod(normalized.PaymentMethod, normalized.PayPlatform) {
|
||||||
return &ProcessRechargeResponse{Processed: false, Reason: "unsupported_payment_method"}, nil
|
return &ProcessRechargeResponse{Processed: false, Reason: "unsupported_payment_method"}, nil
|
||||||
}
|
}
|
||||||
@ -118,7 +115,6 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg
|
|||||||
|
|
||||||
payPlatform := strings.ToUpper(strings.TrimSpace(event.PayPlatform))
|
payPlatform := strings.ToUpper(strings.TrimSpace(event.PayPlatform))
|
||||||
paymentMethod := strings.ToUpper(strings.TrimSpace(event.PaymentMethod))
|
paymentMethod := strings.ToUpper(strings.TrimSpace(event.PaymentMethod))
|
||||||
appVersion := normalizeRechargeAppVersion(event)
|
|
||||||
sourceOrderID := firstNonEmpty(event.SourceOrderID.String(), event.OrderID.String())
|
sourceOrderID := firstNonEmpty(event.SourceOrderID.String(), event.OrderID.String())
|
||||||
eventID := strings.TrimSpace(event.EventID)
|
eventID := strings.TrimSpace(event.EventID)
|
||||||
if eventID == "" && sourceOrderID != "" {
|
if eventID == "" && sourceOrderID != "" {
|
||||||
@ -139,7 +135,6 @@ func (s *Service) normalizeRechargeEvent(event RechargeEvent) (normalizedRecharg
|
|||||||
Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"),
|
Currency: firstNonEmpty(strings.ToUpper(strings.TrimSpace(event.Currency)), "USD"),
|
||||||
PayPlatform: payPlatform,
|
PayPlatform: payPlatform,
|
||||||
PaymentMethod: paymentMethod,
|
PaymentMethod: paymentMethod,
|
||||||
AppVersion: appVersion,
|
|
||||||
SourceOrderID: sourceOrderID,
|
SourceOrderID: sourceOrderID,
|
||||||
OccurredAt: occurredAt,
|
OccurredAt: occurredAt,
|
||||||
}, nil
|
}, nil
|
||||||
@ -362,34 +357,6 @@ func isAcceptedPaymentMethod(paymentMethod string, _ string) bool {
|
|||||||
return paymentMethod == paymentMethodGoogle || paymentMethod == paymentMethodThirdParty
|
return paymentMethod == paymentMethodGoogle || paymentMethod == paymentMethodThirdParty
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeRechargeAppVersion(event RechargeEvent) string {
|
|
||||||
return firstNonEmpty(
|
|
||||||
event.AppVersion.String(),
|
|
||||||
event.ClientVersion.String(),
|
|
||||||
event.Version.String(),
|
|
||||||
event.ReqVersion.String(),
|
|
||||||
rechargePayloadString(event.Payload, "appVersion", "clientVersion", "version", "reqVersion"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rechargePayloadString(payload json.RawMessage, keys ...string) string {
|
|
||||||
if len(payload) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var values map[string]any
|
|
||||||
if err := json.Unmarshal(payload, &values); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for _, key := range keys {
|
|
||||||
if value, ok := values[key]; ok {
|
|
||||||
if text := strings.TrimSpace(fmt.Sprint(value)); text != "" && text != "<nil>" {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstPositiveAmountCents(values ...string) int64 {
|
func firstPositiveAmountCents(values ...string) int64 {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
cents, err := parseAmountCents(value)
|
cents, err := parseAmountCents(value)
|
||||||
|
|||||||
@ -120,7 +120,7 @@ func rewardItemsFromGroup(detail integration.RewardGroupDetail) []RewardItem {
|
|||||||
for _, item := range detail.RewardConfigList {
|
for _, item := range detail.RewardConfigList {
|
||||||
items = append(items, RewardItem{
|
items = append(items, RewardItem{
|
||||||
ID: int64(item.ID),
|
ID: int64(item.ID),
|
||||||
Type: rewardItemDisplayType(item),
|
Type: strings.TrimSpace(item.Type),
|
||||||
Name: strings.TrimSpace(item.Name),
|
Name: strings.TrimSpace(item.Name),
|
||||||
Content: strings.TrimSpace(item.Content),
|
Content: strings.TrimSpace(item.Content),
|
||||||
Quantity: int64(item.Quantity),
|
Quantity: int64(item.Quantity),
|
||||||
@ -131,29 +131,6 @@ func rewardItemsFromGroup(detail integration.RewardGroupDetail) []RewardItem {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
func rewardItemDisplayType(item integration.RewardGroupItem) string {
|
|
||||||
itemType := strings.ToUpper(strings.TrimSpace(item.Type))
|
|
||||||
detailType := strings.ToUpper(strings.TrimSpace(item.DetailType))
|
|
||||||
if itemType == "PROPS" {
|
|
||||||
if detailType != "" && detailType != "PROPS" {
|
|
||||||
return detailType
|
|
||||||
}
|
|
||||||
if isVipRewardItem(item) {
|
|
||||||
return "NOBLE_VIP"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return itemType
|
|
||||||
}
|
|
||||||
|
|
||||||
func isVipRewardItem(item integration.RewardGroupItem) bool {
|
|
||||||
name := strings.ToUpper(strings.TrimSpace(item.Name))
|
|
||||||
remark := strings.ToUpper(strings.TrimSpace(item.Remark))
|
|
||||||
return strings.Contains(name, "VIP") ||
|
|
||||||
strings.Contains(name, "NOBLE") ||
|
|
||||||
strings.Contains(remark, "VIP") ||
|
|
||||||
strings.Contains(remark, "NOBLE")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDateTime(t time.Time) string {
|
func formatDateTime(t time.Time) string {
|
||||||
if t.IsZero() {
|
if t.IsZero() {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -50,15 +50,14 @@ func TestSaveConfigReturnsVipCoverFromSourceURL(t *testing.T) {
|
|||||||
Name: "VIP礼包",
|
Name: "VIP礼包",
|
||||||
RewardConfigList: []integration.RewardGroupItem{
|
RewardConfigList: []integration.RewardGroupItem{
|
||||||
{
|
{
|
||||||
ID: integration.Int64Value(11),
|
ID: integration.Int64Value(11),
|
||||||
Type: "PROPS",
|
Type: "PROPS",
|
||||||
DetailType: "NOBLE_VIP",
|
Name: "VIP 1",
|
||||||
Name: "VIP 1",
|
Content: "2049402425177018400",
|
||||||
Content: "2049402425177018400",
|
Quantity: integration.Int64Value(30),
|
||||||
Quantity: integration.Int64Value(30),
|
Cover: "",
|
||||||
Cover: "",
|
SourceURL: "https://example.com/vip-short-badge.png",
|
||||||
SourceURL: "https://example.com/vip-short-badge.png",
|
Remark: "30 天",
|
||||||
Remark: "30 天",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -79,59 +78,6 @@ func TestSaveConfigReturnsVipCoverFromSourceURL(t *testing.T) {
|
|||||||
if got := resp.LevelConfigs[0].RewardItems[0].Cover; got != "https://example.com/vip-short-badge.png" {
|
if got := resp.LevelConfigs[0].RewardItems[0].Cover; got != "https://example.com/vip-short-badge.png" {
|
||||||
t.Fatalf("vip cover = %q", got)
|
t.Fatalf("vip cover = %q", got)
|
||||||
}
|
}
|
||||||
if got := resp.LevelConfigs[0].RewardItems[0].Type; got != "NOBLE_VIP" {
|
|
||||||
t.Fatalf("vip type = %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRewardItemsReturnConcretePropsType(t *testing.T) {
|
|
||||||
service, gateway, _ := newTestService(t)
|
|
||||||
gateway.groups[3001] = integration.RewardGroupDetail{
|
|
||||||
ID: integration.Int64Value(3001),
|
|
||||||
Name: "道具礼包",
|
|
||||||
RewardConfigList: []integration.RewardGroupItem{
|
|
||||||
{
|
|
||||||
ID: integration.Int64Value(21),
|
|
||||||
Type: "PROPS",
|
|
||||||
DetailType: "RIDE",
|
|
||||||
Name: "Luxury sports car",
|
|
||||||
Content: "2044620024357908482",
|
|
||||||
Quantity: integration.Int64Value(7),
|
|
||||||
Cover: "https://example.com/ride.png",
|
|
||||||
Remark: "Luxury sports car",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: integration.Int64Value(22),
|
|
||||||
Type: "PROPS",
|
|
||||||
Name: "VIP 3",
|
|
||||||
Content: "2050200000000000003",
|
|
||||||
Quantity: integration.Int64Value(3),
|
|
||||||
Cover: "https://example.com/vip3.png",
|
|
||||||
Remark: "VIP 3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := service.SaveConfig(context.Background(), mustDecodeSaveConfigRequest(t, `{
|
|
||||||
"sysOrigin": "LIKEI",
|
|
||||||
"enabled": true,
|
|
||||||
"levelConfigs": [
|
|
||||||
{"level": 1, "rechargeAmount": "9.99", "rewardGroupId": "3001", "rewardGroupName": "道具礼包", "enabled": true}
|
|
||||||
]
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("SaveConfig() error = %v", err)
|
|
||||||
}
|
|
||||||
items := resp.LevelConfigs[0].RewardItems
|
|
||||||
if len(items) != 2 {
|
|
||||||
t.Fatalf("reward items = %+v", items)
|
|
||||||
}
|
|
||||||
if items[0].Type != "RIDE" {
|
|
||||||
t.Fatalf("ride type = %q", items[0].Type)
|
|
||||||
}
|
|
||||||
if items[1].Type != "NOBLE_VIP" {
|
|
||||||
t.Fatalf("vip fallback type = %q", items[1].Type)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
||||||
@ -162,7 +108,6 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
|||||||
"amountCents": "999",
|
"amountCents": "999",
|
||||||
"payPlatform": "APPLE",
|
"payPlatform": "APPLE",
|
||||||
"paymentMethod": "APPLE",
|
"paymentMethod": "APPLE",
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"sourceOrderId": "1"
|
"sourceOrderId": "1"
|
||||||
}`))
|
}`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -179,7 +124,6 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
|||||||
"amountCents": "499",
|
"amountCents": "499",
|
||||||
"payPlatform": "GOOGLE",
|
"payPlatform": "GOOGLE",
|
||||||
"paymentMethod": "GOOGLE",
|
"paymentMethod": "GOOGLE",
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"sourceOrderId": "LOW"
|
"sourceOrderId": "LOW"
|
||||||
}`))
|
}`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -196,7 +140,6 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
|||||||
"amountCents": "999",
|
"amountCents": "999",
|
||||||
"payPlatform": "GOOGLE",
|
"payPlatform": "GOOGLE",
|
||||||
"paymentMethod": "GOOGLE",
|
"paymentMethod": "GOOGLE",
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"sourceOrderId": "OK",
|
"sourceOrderId": "OK",
|
||||||
"occurredAt": "2026-05-21T12:00:00Z"
|
"occurredAt": "2026-05-21T12:00:00Z"
|
||||||
}`))
|
}`))
|
||||||
@ -223,7 +166,6 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
|||||||
"amountCents": "999",
|
"amountCents": "999",
|
||||||
"payPlatform": "GOOGLE",
|
"payPlatform": "GOOGLE",
|
||||||
"paymentMethod": "GOOGLE",
|
"paymentMethod": "GOOGLE",
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"sourceOrderId": "OK"
|
"sourceOrderId": "OK"
|
||||||
}`))
|
}`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -243,7 +185,6 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
|||||||
"amountCents": "999",
|
"amountCents": "999",
|
||||||
"payPlatform": "CUSTOM_PAY",
|
"payPlatform": "CUSTOM_PAY",
|
||||||
"paymentMethod": "THIRD_PARTY",
|
"paymentMethod": "THIRD_PARTY",
|
||||||
"appVersion": "1.5.1",
|
|
||||||
"sourceOrderId": "CUSTOM"
|
"sourceOrderId": "CUSTOM"
|
||||||
}`))
|
}`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -263,7 +204,6 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
|||||||
"amountCents": "999",
|
"amountCents": "999",
|
||||||
"payPlatform": "CUSTOM_GOOGLE_GATEWAY",
|
"payPlatform": "CUSTOM_GOOGLE_GATEWAY",
|
||||||
"paymentMethod": "GOOGLE",
|
"paymentMethod": "GOOGLE",
|
||||||
"appVersion": "1.5.1",
|
|
||||||
"sourceOrderId": "CUSTOM_GOOGLE"
|
"sourceOrderId": "CUSTOM_GOOGLE"
|
||||||
}`))
|
}`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -285,108 +225,6 @@ func TestProcessRechargeEventFiltersAndGrantsOnce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessRechargeEventStartsCountingAtSupportedAppVersion(t *testing.T) {
|
|
||||||
service, gateway, db := newTestService(t)
|
|
||||||
gateway.groups[1001] = testRewardGroup(1001, "首冲礼包")
|
|
||||||
|
|
||||||
_, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldVersion, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
|
||||||
"eventId": "RECHARGE_SUCCESS:GOOGLE:OLD_VERSION",
|
|
||||||
"sysOrigin": "LIKEI",
|
|
||||||
"userId": "123",
|
|
||||||
"amountCents": "999",
|
|
||||||
"payPlatform": "GOOGLE",
|
|
||||||
"paymentMethod": "GOOGLE",
|
|
||||||
"appVersion": "1.4.9",
|
|
||||||
"sourceOrderId": "OLD_VERSION"
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("old version ProcessRechargeEvent() error = %v", err)
|
|
||||||
}
|
|
||||||
if oldVersion.Processed || oldVersion.Reason != "app_version_not_supported" {
|
|
||||||
t.Fatalf("old version response = %+v", oldVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
missingVersion, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
|
||||||
"eventId": "RECHARGE_SUCCESS:GOOGLE:MISSING_VERSION",
|
|
||||||
"sysOrigin": "LIKEI",
|
|
||||||
"userId": "123",
|
|
||||||
"amountCents": "999",
|
|
||||||
"payPlatform": "GOOGLE",
|
|
||||||
"paymentMethod": "GOOGLE",
|
|
||||||
"sourceOrderId": "MISSING_VERSION"
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("missing version ProcessRechargeEvent() error = %v", err)
|
|
||||||
}
|
|
||||||
if missingVersion.Processed || missingVersion.Reason != "app_version_not_supported" {
|
|
||||||
t.Fatalf("missing version response = %+v", missingVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil {
|
|
||||||
t.Fatalf("count grant records after unsupported versions: %v", err)
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatalf("grant record count after unsupported versions = %d", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
supported, err := service.ProcessRechargeEvent(context.Background(), mustDecodeRechargeEvent(t, `{
|
|
||||||
"eventId": "RECHARGE_SUCCESS:GOOGLE:SUPPORTED_VERSION",
|
|
||||||
"sysOrigin": "LIKEI",
|
|
||||||
"userId": "123",
|
|
||||||
"amountCents": "999",
|
|
||||||
"payPlatform": "GOOGLE",
|
|
||||||
"paymentMethod": "GOOGLE",
|
|
||||||
"appVersion": "1.5.0",
|
|
||||||
"sourceOrderId": "SUPPORTED_VERSION"
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("supported version ProcessRechargeEvent() error = %v", err)
|
|
||||||
}
|
|
||||||
if !supported.Processed || supported.Status != statusSuccess {
|
|
||||||
t.Fatalf("supported version response = %+v", supported)
|
|
||||||
}
|
|
||||||
if err := db.Model(&model.FirstRechargeRewardGrantRecord{}).Count(&count).Error; err != nil {
|
|
||||||
t.Fatalf("count grant records after supported version: %v", err)
|
|
||||||
}
|
|
||||||
if count != 1 {
|
|
||||||
t.Fatalf("grant record count after supported version = %d", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFirstRechargeRewardAppVersionSupported(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
appVersion string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{name: "missing", appVersion: "", want: false},
|
|
||||||
{name: "below", appVersion: "1.4.9", want: false},
|
|
||||||
{name: "equal", appVersion: "1.5.0", want: true},
|
|
||||||
{name: "short equal", appVersion: "1.5", want: true},
|
|
||||||
{name: "above", appVersion: "1.5.1", want: true},
|
|
||||||
{name: "suffix", appVersion: "1.5.0+100", want: true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := isFirstRechargeRewardAppVersionSupported(tt.appVersion); got != tt.want {
|
|
||||||
t.Fatalf("isFirstRechargeRewardAppVersionSupported(%q) = %v, want %v", tt.appVersion, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestService(t *testing.T) (*Service, *fakeGateway, *gorm.DB) {
|
func newTestService(t *testing.T) (*Service, *fakeGateway, *gorm.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
@ -436,13 +274,12 @@ func testRewardGroup(id int64, name string) integration.RewardGroupDetail {
|
|||||||
Name: name,
|
Name: name,
|
||||||
RewardConfigList: []integration.RewardGroupItem{
|
RewardConfigList: []integration.RewardGroupItem{
|
||||||
{
|
{
|
||||||
ID: integration.Int64Value(11),
|
ID: integration.Int64Value(11),
|
||||||
Type: "PROPS",
|
Type: "PROPS",
|
||||||
DetailType: "RIDE",
|
Name: name + "奖励",
|
||||||
Name: name + "奖励",
|
Content: "10001",
|
||||||
Content: "10001",
|
Quantity: integration.Int64Value(1),
|
||||||
Quantity: integration.Int64Value(1),
|
Cover: "https://example.com/reward.png",
|
||||||
Cover: "https://example.com/reward.png",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,10 +176,6 @@ type RechargeEvent struct {
|
|||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
PayPlatform string `json:"payPlatform"`
|
PayPlatform string `json:"payPlatform"`
|
||||||
PaymentMethod string `json:"paymentMethod"`
|
PaymentMethod string `json:"paymentMethod"`
|
||||||
AppVersion flexibleString `json:"appVersion"`
|
|
||||||
ClientVersion flexibleString `json:"clientVersion"`
|
|
||||||
Version flexibleString `json:"version"`
|
|
||||||
ReqVersion flexibleString `json:"reqVersion"`
|
|
||||||
SourceOrderID flexibleString `json:"sourceOrderId"`
|
SourceOrderID flexibleString `json:"sourceOrderId"`
|
||||||
OrderID flexibleString `json:"orderId"`
|
OrderID flexibleString `json:"orderId"`
|
||||||
OccurredAt flexibleString `json:"occurredAt"`
|
OccurredAt flexibleString `json:"occurredAt"`
|
||||||
@ -223,7 +219,6 @@ type normalizedRechargeEvent struct {
|
|||||||
Currency string
|
Currency string
|
||||||
PayPlatform string
|
PayPlatform string
|
||||||
PaymentMethod string
|
PaymentMethod string
|
||||||
AppVersion string
|
|
||||||
SourceOrderID string
|
SourceOrderID string
|
||||||
OccurredAt time.Time
|
OccurredAt time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
package firstrechargereward
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const firstRechargeRewardMinAppVersion = "1.5.0"
|
|
||||||
|
|
||||||
func isFirstRechargeRewardAppVersionSupported(appVersion string) bool {
|
|
||||||
appVersion = strings.TrimSpace(appVersion)
|
|
||||||
if appVersion == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return compareAppVersion(appVersion, firstRechargeRewardMinAppVersion) >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareAppVersion(left, right string) int {
|
|
||||||
leftParts := parseAppVersion(left)
|
|
||||||
rightParts := parseAppVersion(right)
|
|
||||||
maxLen := len(leftParts)
|
|
||||||
if len(rightParts) > maxLen {
|
|
||||||
maxLen = len(rightParts)
|
|
||||||
}
|
|
||||||
for i := 0; i < maxLen; i++ {
|
|
||||||
var lv, rv int
|
|
||||||
if i < len(leftParts) {
|
|
||||||
lv = leftParts[i]
|
|
||||||
}
|
|
||||||
if i < len(rightParts) {
|
|
||||||
rv = rightParts[i]
|
|
||||||
}
|
|
||||||
if lv > rv {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if lv < rv {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAppVersion(version string) []int {
|
|
||||||
version = strings.TrimSpace(version)
|
|
||||||
if version == "" {
|
|
||||||
return []int{0}
|
|
||||||
}
|
|
||||||
parts := strings.Split(version, ".")
|
|
||||||
result := make([]int, 0, len(parts))
|
|
||||||
for _, part := range parts {
|
|
||||||
part = strings.TrimSpace(part)
|
|
||||||
if part == "" {
|
|
||||||
result = append(result, 0)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
digits := strings.Builder{}
|
|
||||||
for _, r := range part {
|
|
||||||
if r < '0' || r > '9' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
digits.WriteRune(r)
|
|
||||||
}
|
|
||||||
if digits.Len() == 0 {
|
|
||||||
result = append(result, 0)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
value, err := strconv.Atoi(digits.String())
|
|
||||||
if err != nil {
|
|
||||||
result = append(result, 0)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, value)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@ -3,24 +3,18 @@ package highwin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"chatapp3-golang/internal/common"
|
"chatapp3-golang/internal/common"
|
||||||
"chatapp3-golang/internal/service/regionimgroup"
|
"chatapp3-golang/internal/service/regionimgroup"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SimulationKindLuckyGift = "LUCKY_GIFT"
|
SimulationKindLuckyGift = "LUCKY_GIFT"
|
||||||
SimulationKindGameWin = "GAME_WIN"
|
SimulationKindGameWin = "GAME_WIN"
|
||||||
adminTimeLayout = "2006-01-02 15:04:05"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BroadcastSimulationRequest struct {
|
type BroadcastSimulationRequest struct {
|
||||||
@ -51,216 +45,6 @@ type BroadcastSimulationResponse struct {
|
|||||||
Data map[string]any `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BroadcastSimulationWorker struct {
|
|
||||||
db *gorm.DB
|
|
||||||
redis *redis.Client
|
|
||||||
broadcaster RegionBroadcaster
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
cancel context.CancelFunc
|
|
||||||
config ContinuousSimulationRequest
|
|
||||||
status ContinuousSimulationStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContinuousSimulationRequest struct {
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
RegionCodes []string `json:"regionCodes"`
|
|
||||||
EnableLuckyGift bool `json:"enableLuckyGift"`
|
|
||||||
EnableGameWin bool `json:"enableGameWin"`
|
|
||||||
MinDelaySeconds int `json:"minDelaySeconds"`
|
|
||||||
MaxDelaySeconds int `json:"maxDelaySeconds"`
|
|
||||||
|
|
||||||
BetAmounts []int64 `json:"betAmounts"`
|
|
||||||
Multiples []float64 `json:"multiples"`
|
|
||||||
GameWinAmounts []int64 `json:"gameWinAmounts"`
|
|
||||||
LuckyGiftWinAmounts []int64 `json:"luckyGiftWinAmounts"`
|
|
||||||
GiftCandyOptions []int64 `json:"giftCandyOptions"`
|
|
||||||
GiftQuantityOptions []int64 `json:"giftQuantityOptions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContinuousSimulationStatus struct {
|
|
||||||
Running bool `json:"running"`
|
|
||||||
SysOrigin string `json:"sysOrigin"`
|
|
||||||
RegionCodes []string `json:"regionCodes"`
|
|
||||||
StartedAt string `json:"startedAt,omitempty"`
|
|
||||||
LastRunAt string `json:"lastRunAt,omitempty"`
|
|
||||||
LastError string `json:"lastError,omitempty"`
|
|
||||||
NextDelaySec int `json:"nextDelaySec"`
|
|
||||||
LuckyGiftCount int64 `json:"luckyGiftCount"`
|
|
||||||
GameWinCount int64 `json:"gameWinCount"`
|
|
||||||
TotalCount int64 `json:"totalCount"`
|
|
||||||
Config any `json:"config,omitempty"`
|
|
||||||
UpdateTime time.Time `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type continuousCandidateUser struct {
|
|
||||||
ID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type continuousCandidateGame struct {
|
|
||||||
GameID string
|
|
||||||
Cover string
|
|
||||||
}
|
|
||||||
|
|
||||||
type continuousCandidateRoom struct {
|
|
||||||
RoomID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBroadcastSimulationWorker(db *gorm.DB, redisClient *redis.Client, broadcaster RegionBroadcaster) *BroadcastSimulationWorker {
|
|
||||||
return &BroadcastSimulationWorker{
|
|
||||||
db: db,
|
|
||||||
redis: redisClient,
|
|
||||||
broadcaster: broadcaster,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) Start(ctx context.Context, req ContinuousSimulationRequest) (*ContinuousSimulationStatus, error) {
|
|
||||||
req = normalizeContinuousRequest(req)
|
|
||||||
if err := validateContinuousRequest(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if w == nil || w.db == nil || w.redis == nil || w.broadcaster == nil {
|
|
||||||
return nil, common.NewAppError(http.StatusInternalServerError, "simulation_worker_not_configured", "simulation worker is not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
w.mu.Lock()
|
|
||||||
if w.cancel != nil {
|
|
||||||
w.cancel()
|
|
||||||
}
|
|
||||||
workerCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
w.cancel = cancel
|
|
||||||
w.config = req
|
|
||||||
w.status = ContinuousSimulationStatus{
|
|
||||||
Running: true,
|
|
||||||
SysOrigin: req.SysOrigin,
|
|
||||||
RegionCodes: append([]string(nil), req.RegionCodes...),
|
|
||||||
StartedAt: time.Now().Format(adminTimeLayout),
|
|
||||||
Config: req,
|
|
||||||
}
|
|
||||||
status := w.status
|
|
||||||
w.mu.Unlock()
|
|
||||||
|
|
||||||
go w.run(workerCtx, req)
|
|
||||||
return &status, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) Stop() *ContinuousSimulationStatus {
|
|
||||||
if w == nil {
|
|
||||||
return &ContinuousSimulationStatus{}
|
|
||||||
}
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
if w.cancel != nil {
|
|
||||||
w.cancel()
|
|
||||||
w.cancel = nil
|
|
||||||
}
|
|
||||||
w.status.Running = false
|
|
||||||
w.status.NextDelaySec = 0
|
|
||||||
w.status.UpdateTime = time.Now()
|
|
||||||
status := w.status
|
|
||||||
status.Config = w.config
|
|
||||||
return &status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) Status() *ContinuousSimulationStatus {
|
|
||||||
if w == nil {
|
|
||||||
return &ContinuousSimulationStatus{}
|
|
||||||
}
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
status := w.status
|
|
||||||
status.Config = w.config
|
|
||||||
return &status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) run(ctx context.Context, req ContinuousSimulationRequest) {
|
|
||||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
for {
|
|
||||||
delay := randomDelaySeconds(rng, req.MinDelaySeconds, req.MaxDelaySeconds)
|
|
||||||
w.updateStatus(func(status *ContinuousSimulationStatus) {
|
|
||||||
status.NextDelaySec = delay
|
|
||||||
})
|
|
||||||
|
|
||||||
timer := time.NewTimer(time.Duration(delay) * time.Second)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
timer.Stop()
|
|
||||||
w.updateStatus(func(status *ContinuousSimulationStatus) {
|
|
||||||
status.Running = false
|
|
||||||
status.NextDelaySec = 0
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case <-timer.C:
|
|
||||||
}
|
|
||||||
|
|
||||||
w.runOnce(ctx, req, rng)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) runOnce(ctx context.Context, req ContinuousSimulationRequest, rng *rand.Rand) {
|
|
||||||
regionCode := req.RegionCodes[rng.Intn(len(req.RegionCodes))]
|
|
||||||
userID, err := w.randomOfflineUser(ctx, req.SysOrigin, rng)
|
|
||||||
if err != nil {
|
|
||||||
w.setLastError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roomID, err := w.randomActiveRoom(ctx, req.SysOrigin, rng)
|
|
||||||
if err != nil {
|
|
||||||
w.setLastError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.EnableLuckyGift {
|
|
||||||
winAmount := pickInt64(rng, req.LuckyGiftWinAmounts)
|
|
||||||
if winAmount <= 0 {
|
|
||||||
winAmount = pickInt64(rng, req.GameWinAmounts)
|
|
||||||
}
|
|
||||||
err := w.sendContinuous(ctx, BroadcastSimulationRequest{
|
|
||||||
SysOrigin: req.SysOrigin,
|
|
||||||
RegionCode: regionCode,
|
|
||||||
Kind: SimulationKindLuckyGift,
|
|
||||||
SenderUserID: strconv.FormatInt(userID, 10),
|
|
||||||
RoomID: strconv.FormatInt(roomID, 10),
|
|
||||||
Multiple: formatFloat(pickFloat64(rng, req.Multiples)),
|
|
||||||
WinAmount: strconv.FormatInt(winAmount, 10),
|
|
||||||
GiftCandy: strconv.FormatInt(pickInt64(rng, req.GiftCandyOptions), 10),
|
|
||||||
GiftQuantity: strconv.FormatInt(pickInt64(rng, req.GiftQuantityOptions), 10),
|
|
||||||
ProviderOrder: fmt.Sprintf("worker-lucky-%d", time.Now().UnixNano()),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
w.setLastError(err)
|
|
||||||
} else {
|
|
||||||
w.incrementCount(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.EnableGameWin {
|
|
||||||
game, err := w.randomGame(ctx, req.SysOrigin)
|
|
||||||
if err != nil {
|
|
||||||
w.setLastError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = w.sendContinuous(ctx, BroadcastSimulationRequest{
|
|
||||||
SysOrigin: req.SysOrigin,
|
|
||||||
RegionCode: regionCode,
|
|
||||||
Kind: SimulationKindGameWin,
|
|
||||||
SenderUserID: strconv.FormatInt(userID, 10),
|
|
||||||
RoomID: strconv.FormatInt(roomID, 10),
|
|
||||||
Multiple: formatFloat(pickFloat64(rng, req.Multiples)),
|
|
||||||
WinAmount: strconv.FormatInt(pickInt64(rng, req.GameWinAmounts), 10),
|
|
||||||
GameID: game.GameID,
|
|
||||||
GameURL: game.Cover,
|
|
||||||
GameRoundID: fmt.Sprintf("worker-game-%d", time.Now().UnixNano()),
|
|
||||||
BetAmount: strconv.FormatInt(pickInt64(rng, req.BetAmounts), 10),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
w.setLastError(err)
|
|
||||||
} else {
|
|
||||||
w.incrementCount(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SendSimulationBroadcast(
|
func SendSimulationBroadcast(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
broadcaster RegionBroadcaster,
|
broadcaster RegionBroadcaster,
|
||||||
@ -327,264 +111,6 @@ func SendSimulationBroadcast(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) sendContinuous(ctx context.Context, req BroadcastSimulationRequest) error {
|
|
||||||
_, err := SendSimulationBroadcast(ctx, w.broadcaster, req)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) randomOfflineUser(ctx context.Context, sysOrigin string, rng *rand.Rand) (int64, error) {
|
|
||||||
onlineIDs := w.onlineUserIDs(ctx, sysOrigin)
|
|
||||||
query := w.db.WithContext(ctx).
|
|
||||||
Table("user_base_info AS user").
|
|
||||||
Select("user.id AS id").
|
|
||||||
Where("user.origin_sys = ?", sysOrigin).
|
|
||||||
Where("user.id > 0")
|
|
||||||
if len(onlineIDs) > 0 && len(onlineIDs) <= 1000 {
|
|
||||||
query = query.Where("user.id NOT IN ?", onlineIDs)
|
|
||||||
}
|
|
||||||
var users []continuousCandidateUser
|
|
||||||
if err := query.Order("RAND()").Limit(50).Scan(&users).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
filtered := make([]continuousCandidateUser, 0, len(users))
|
|
||||||
onlineSet := make(map[int64]struct{}, len(onlineIDs))
|
|
||||||
for _, id := range onlineIDs {
|
|
||||||
onlineSet[id] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, user := range users {
|
|
||||||
if user.ID <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, online := onlineSet[user.ID]; online {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, user)
|
|
||||||
}
|
|
||||||
if len(filtered) == 0 {
|
|
||||||
return 0, common.NewAppError(http.StatusNotFound, "simulation_offline_user_not_found", "offline user not found")
|
|
||||||
}
|
|
||||||
return filtered[rng.Intn(len(filtered))].ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) randomActiveRoom(ctx context.Context, sysOrigin string, rng *rand.Rand) (int64, error) {
|
|
||||||
rooms, err := w.activeRooms(ctx, sysOrigin, 10)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if len(rooms) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
return rooms[rng.Intn(len(rooms))].RoomID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) randomGame(ctx context.Context, sysOrigin string) (continuousCandidateGame, error) {
|
|
||||||
var game continuousCandidateGame
|
|
||||||
err := w.db.WithContext(ctx).
|
|
||||||
Table("sys_game_list_config").
|
|
||||||
Select("game_id AS game_id, cover AS cover").
|
|
||||||
Where("sys_origin = ? AND cover <> ''", sysOrigin).
|
|
||||||
Order("RAND()").
|
|
||||||
Limit(1).
|
|
||||||
Scan(&game).Error
|
|
||||||
if err != nil {
|
|
||||||
return continuousCandidateGame{}, err
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(game.GameID) == "" || strings.TrimSpace(game.Cover) == "" {
|
|
||||||
return continuousCandidateGame{}, common.NewAppError(http.StatusNotFound, "simulation_game_not_found", "game cover not found")
|
|
||||||
}
|
|
||||||
return game, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) activeRooms(ctx context.Context, sysOrigin string, minUsers int64) ([]continuousCandidateRoom, error) {
|
|
||||||
var cursor uint64
|
|
||||||
var rooms []continuousCandidateRoom
|
|
||||||
prefix := fmt.Sprintf("voice_room:online:%s:", strings.ToUpper(strings.TrimSpace(sysOrigin)))
|
|
||||||
for {
|
|
||||||
keys, next, err := w.redis.Scan(ctx, cursor, prefix+"*", 100).Result()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, key := range keys {
|
|
||||||
count, err := w.redis.SCard(ctx, key).Result()
|
|
||||||
if err != nil || count <= minUsers {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
roomID, err := strconv.ParseInt(strings.TrimPrefix(key, prefix), 10, 64)
|
|
||||||
if err != nil || roomID <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rooms = append(rooms, continuousCandidateRoom{RoomID: roomID})
|
|
||||||
}
|
|
||||||
cursor = next
|
|
||||||
if cursor == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rooms, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) onlineUserIDs(ctx context.Context, sysOrigin string) []int64 {
|
|
||||||
var cursor uint64
|
|
||||||
prefix := fmt.Sprintf("voice_room:online:%s:", strings.ToUpper(strings.TrimSpace(sysOrigin)))
|
|
||||||
seen := map[int64]struct{}{}
|
|
||||||
for {
|
|
||||||
keys, next, err := w.redis.Scan(ctx, cursor, prefix+"*", 100).Result()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, key := range keys {
|
|
||||||
members, err := w.redis.SMembers(ctx, key).Result()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, raw := range members {
|
|
||||||
id, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
|
||||||
if err == nil && id > 0 {
|
|
||||||
seen[id] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cursor = next
|
|
||||||
if cursor == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ids := make([]int64, 0, len(seen))
|
|
||||||
for id := range seen {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) updateStatus(fn func(*ContinuousSimulationStatus)) {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
fn(&w.status)
|
|
||||||
w.status.UpdateTime = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) setLastError(err error) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.updateStatus(func(status *ContinuousSimulationStatus) {
|
|
||||||
status.LastError = err.Error()
|
|
||||||
status.LastRunAt = time.Now().Format(adminTimeLayout)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BroadcastSimulationWorker) incrementCount(luckyGift bool) {
|
|
||||||
w.updateStatus(func(status *ContinuousSimulationStatus) {
|
|
||||||
status.LastError = ""
|
|
||||||
status.LastRunAt = time.Now().Format(adminTimeLayout)
|
|
||||||
status.TotalCount++
|
|
||||||
if luckyGift {
|
|
||||||
status.LuckyGiftCount++
|
|
||||||
} else {
|
|
||||||
status.GameWinCount++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeContinuousRequest(req ContinuousSimulationRequest) ContinuousSimulationRequest {
|
|
||||||
req.SysOrigin = strings.ToUpper(strings.TrimSpace(req.SysOrigin))
|
|
||||||
req.RegionCodes = normalizeStringList(req.RegionCodes)
|
|
||||||
if req.MinDelaySeconds < 0 {
|
|
||||||
req.MinDelaySeconds = 0
|
|
||||||
}
|
|
||||||
if req.MaxDelaySeconds < req.MinDelaySeconds {
|
|
||||||
req.MaxDelaySeconds = req.MinDelaySeconds
|
|
||||||
}
|
|
||||||
req.BetAmounts = positiveInt64List(req.BetAmounts, []int64{100, 200, 500, 1000})
|
|
||||||
req.Multiples = positiveFloat64List(req.Multiples, []float64{10, 20, 50})
|
|
||||||
req.GameWinAmounts = positiveInt64List(req.GameWinAmounts, []int64{1000, 2000, 5000})
|
|
||||||
req.LuckyGiftWinAmounts = positiveInt64List(req.LuckyGiftWinAmounts, []int64{1000, 2000, 5000})
|
|
||||||
req.GiftCandyOptions = positiveInt64List(req.GiftCandyOptions, []int64{100, 200, 500})
|
|
||||||
req.GiftQuantityOptions = positiveInt64List(req.GiftQuantityOptions, []int64{1, 5, 10})
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateContinuousRequest(req ContinuousSimulationRequest) error {
|
|
||||||
if req.SysOrigin == "" {
|
|
||||||
return common.NewAppError(http.StatusBadRequest, "missing_sys_origin", "sysOrigin is required")
|
|
||||||
}
|
|
||||||
if len(req.RegionCodes) == 0 {
|
|
||||||
return common.NewAppError(http.StatusBadRequest, "missing_region_codes", "regionCodes is required")
|
|
||||||
}
|
|
||||||
if !req.EnableLuckyGift && !req.EnableGameWin {
|
|
||||||
return common.NewAppError(http.StatusBadRequest, "missing_broadcast_type", "enable at least one broadcast type")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeStringList(values []string) []string {
|
|
||||||
seen := map[string]struct{}{}
|
|
||||||
result := make([]string, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
value = strings.ToUpper(strings.TrimSpace(value))
|
|
||||||
if value == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[value]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[value] = struct{}{}
|
|
||||||
result = append(result, value)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func positiveInt64List(values []int64, fallback []int64) []int64 {
|
|
||||||
result := make([]int64, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
if value > 0 {
|
|
||||||
result = append(result, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(result) == 0 {
|
|
||||||
return append([]int64(nil), fallback...)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func positiveFloat64List(values []float64, fallback []float64) []float64 {
|
|
||||||
result := make([]float64, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
if value >= ThresholdMultiple {
|
|
||||||
result = append(result, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(result) == 0 {
|
|
||||||
return append([]float64(nil), fallback...)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomDelaySeconds(rng *rand.Rand, min, max int) int {
|
|
||||||
if max <= min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
return min + rng.Intn(max-min+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickInt64(rng *rand.Rand, values []int64) int64 {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return values[rng.Intn(len(values))]
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickFloat64(rng *rand.Rand, values []float64) float64 {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return values[rng.Intn(len(values))]
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatFloat(value float64) string {
|
|
||||||
return strconv.FormatFloat(value, 'f', -1, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func simulationMessageType(kind string) (string, error) {
|
func simulationMessageType(kind string) (string, error) {
|
||||||
switch strings.ToUpper(strings.TrimSpace(kind)) {
|
switch strings.ToUpper(strings.TrimSpace(kind)) {
|
||||||
case SimulationKindLuckyGift:
|
case SimulationKindLuckyGift:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user