任务
This commit is contained in:
parent
f995d80610
commit
811178252f
@ -13,13 +13,20 @@ const (
|
|||||||
AudienceNewbie = "newbie"
|
AudienceNewbie = "newbie"
|
||||||
|
|
||||||
ActionNone = "none"
|
ActionNone = "none"
|
||||||
|
ActionRoom = "room"
|
||||||
ActionRoomRandom = "room_random"
|
ActionRoomRandom = "room_random"
|
||||||
|
ActionRoomWindow = "room_window"
|
||||||
|
ActionRoomRandomWindow = "room_random_window"
|
||||||
|
ActionRoomGame = "room_game"
|
||||||
ActionRoomRandomGame = "room_random_game"
|
ActionRoomRandomGame = "room_random_game"
|
||||||
|
ActionExploreGame = "explore_game"
|
||||||
ActionWallet = "wallet"
|
ActionWallet = "wallet"
|
||||||
ActionGiftPanel = "gift_panel"
|
ActionGiftPanel = "gift_panel"
|
||||||
ActionGiftPanelSpecific = "gift_panel_specific"
|
ActionGiftPanelSpecific = "gift_panel_specific"
|
||||||
|
|
||||||
MetricGameSpendCoin = "game_spend_coin"
|
MetricGameSpendCoin = "game_spend_coin"
|
||||||
|
MetricGameDicePlayCount = "game_dice_play_count"
|
||||||
|
MetricGameRockPlayCount = "game_rock_play_count"
|
||||||
MetricGiftSpendCoin = "gift_spend_coin"
|
MetricGiftSpendCoin = "gift_spend_coin"
|
||||||
MetricGiftSendCount = "gift_send_count"
|
MetricGiftSendCount = "gift_send_count"
|
||||||
MetricLuckyGiftSpendCoin = "lucky_gift_spend_coin"
|
MetricLuckyGiftSpendCoin = "lucky_gift_spend_coin"
|
||||||
|
|||||||
@ -535,10 +535,20 @@ func normalizeActionType(value string) string {
|
|||||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
case "", taskdomain.ActionNone:
|
case "", taskdomain.ActionNone:
|
||||||
return taskdomain.ActionNone
|
return taskdomain.ActionNone
|
||||||
|
case taskdomain.ActionRoom:
|
||||||
|
return taskdomain.ActionRoom
|
||||||
case taskdomain.ActionRoomRandom:
|
case taskdomain.ActionRoomRandom:
|
||||||
return taskdomain.ActionRoomRandom
|
return taskdomain.ActionRoomRandom
|
||||||
|
case taskdomain.ActionRoomWindow:
|
||||||
|
return taskdomain.ActionRoomWindow
|
||||||
|
case taskdomain.ActionRoomRandomWindow:
|
||||||
|
return taskdomain.ActionRoomRandomWindow
|
||||||
|
case taskdomain.ActionRoomGame:
|
||||||
|
return taskdomain.ActionRoomGame
|
||||||
case taskdomain.ActionRoomRandomGame:
|
case taskdomain.ActionRoomRandomGame:
|
||||||
return taskdomain.ActionRoomRandomGame
|
return taskdomain.ActionRoomRandomGame
|
||||||
|
case taskdomain.ActionExploreGame:
|
||||||
|
return taskdomain.ActionExploreGame
|
||||||
case taskdomain.ActionWallet:
|
case taskdomain.ActionWallet:
|
||||||
return taskdomain.ActionWallet
|
return taskdomain.ActionWallet
|
||||||
case taskdomain.ActionGiftPanel:
|
case taskdomain.ActionGiftPanel:
|
||||||
@ -552,7 +562,17 @@ func normalizeActionType(value string) string {
|
|||||||
|
|
||||||
func validActionType(value string) bool {
|
func validActionType(value string) bool {
|
||||||
switch value {
|
switch value {
|
||||||
case taskdomain.ActionNone, taskdomain.ActionRoomRandom, taskdomain.ActionRoomRandomGame, taskdomain.ActionWallet, taskdomain.ActionGiftPanel, taskdomain.ActionGiftPanelSpecific:
|
case taskdomain.ActionNone,
|
||||||
|
taskdomain.ActionRoom,
|
||||||
|
taskdomain.ActionRoomRandom,
|
||||||
|
taskdomain.ActionRoomWindow,
|
||||||
|
taskdomain.ActionRoomRandomWindow,
|
||||||
|
taskdomain.ActionRoomGame,
|
||||||
|
taskdomain.ActionRoomRandomGame,
|
||||||
|
taskdomain.ActionExploreGame,
|
||||||
|
taskdomain.ActionWallet,
|
||||||
|
taskdomain.ActionGiftPanel,
|
||||||
|
taskdomain.ActionGiftPanelSpecific:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -26,6 +26,13 @@ import (
|
|||||||
|
|
||||||
const defaultScene = gamedomain.SceneVoiceRoom
|
const defaultScene = gamedomain.SceneVoiceRoom
|
||||||
|
|
||||||
|
const (
|
||||||
|
selfGameIDDice = "dice"
|
||||||
|
selfGameIDRock = "rock"
|
||||||
|
taskMetricGameDicePlayed = "game_dice_play_count"
|
||||||
|
taskMetricGameRockPlayed = "game_rock_play_count"
|
||||||
|
)
|
||||||
|
|
||||||
// Repository 隔离 game-service 持久化事实,订单和 session 不能只放内存。
|
// Repository 隔离 game-service 持久化事实,订单和 session 不能只放内存。
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Ping(ctx context.Context) error
|
Ping(ctx context.Context) error
|
||||||
@ -697,7 +704,38 @@ func (s *Service) ProcessLevelEventOutboxBatch(ctx context.Context, runID string
|
|||||||
_ = s.repository.MarkLevelEventFailed(eventCtx, event.EventID, "task returned "+taskResp.GetStatus(), nextRetry, s.now().UnixMilli())
|
_ = s.repository.MarkLevelEventFailed(eventCtx, event.EventID, "task returned "+taskResp.GetStatus(), nextRetry, s.now().UnixMilli())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 只有成长和任务两条链路都达到终态才标记 delivered,防止“游戏等级已加、任务未加”的半完成 outbox。
|
if playMetric := selfGamePlayCountMetric(event.GameID); playMetric != "" {
|
||||||
|
// dice/rock 的“玩 1 局”是独立每日任务口径,不能复用扣币金额;仍使用同一条扣费 outbox 派生,保证每个真人扣款参与者最多累计 1 次。
|
||||||
|
playResp, relayErr := s.activity.ConsumeTaskEvent(eventCtx, &activityv1.ConsumeTaskEventRequest{
|
||||||
|
Meta: &activityv1.RequestMeta{
|
||||||
|
RequestId: event.EventID + ":task:play_count",
|
||||||
|
Caller: "game-service",
|
||||||
|
SentAtMs: s.now().UnixMilli(),
|
||||||
|
AppCode: event.AppCode,
|
||||||
|
},
|
||||||
|
EventId: event.EventID + ":task:play_count",
|
||||||
|
EventType: "SelfGamePlayed",
|
||||||
|
SourceService: "game-service",
|
||||||
|
UserId: event.UserID,
|
||||||
|
MetricType: playMetric,
|
||||||
|
Value: 1,
|
||||||
|
OccurredAtMs: event.OccurredAtMS,
|
||||||
|
DimensionsJson: event.PayloadJSON,
|
||||||
|
})
|
||||||
|
if relayErr != nil {
|
||||||
|
failure++
|
||||||
|
nextRetry := s.now().Add(time.Minute).UnixMilli()
|
||||||
|
_ = s.repository.MarkLevelEventFailed(eventCtx, event.EventID, xerr.MessageOf(relayErr), nextRetry, s.now().UnixMilli())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if playResp.GetStatus() != "consumed" && playResp.GetStatus() != "skipped" && playResp.GetStatus() != "duplicate" {
|
||||||
|
failure++
|
||||||
|
nextRetry := s.now().Add(time.Minute).UnixMilli()
|
||||||
|
_ = s.repository.MarkLevelEventFailed(eventCtx, event.EventID, "play task returned "+playResp.GetStatus(), nextRetry, s.now().UnixMilli())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 只有成长、扣币任务和自研游戏局数任务都达到终态才标记 delivered,防止“游戏等级已加、每日任务未加”的半完成 outbox。
|
||||||
if err := s.repository.MarkLevelEventDelivered(eventCtx, event.EventID, s.now().UnixMilli()); err != nil {
|
if err := s.repository.MarkLevelEventDelivered(eventCtx, event.EventID, s.now().UnixMilli()); err != nil {
|
||||||
failure++
|
failure++
|
||||||
continue
|
continue
|
||||||
@ -707,6 +745,17 @@ func (s *Service) ProcessLevelEventOutboxBatch(ctx context.Context, runID string
|
|||||||
return int32(len(events)), processed, success, failure, len(events) == batchSize, nil
|
return int32(len(events)), processed, success, failure, len(events) == batchSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selfGamePlayCountMetric(gameID string) string {
|
||||||
|
switch strings.TrimSpace(gameID) {
|
||||||
|
case selfGameIDDice:
|
||||||
|
return taskMetricGameDicePlayed
|
||||||
|
case selfGameIDRock:
|
||||||
|
return taskMetricGameRockPlayed
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeScene(scene string) string {
|
func normalizeScene(scene string) string {
|
||||||
scene = strings.ToLower(strings.TrimSpace(scene))
|
scene = strings.ToLower(strings.TrimSpace(scene))
|
||||||
if scene == "" {
|
if scene == "" {
|
||||||
|
|||||||
@ -1653,6 +1653,42 @@ func TestProcessLevelEventOutboxBatchRelaysGameSpend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProcessLevelEventOutboxBatchRelaysSelfGamePlayCount(t *testing.T) {
|
||||||
|
repo := &fakeRepository{levelEvents: []gamedomain.LevelEventOutbox{{
|
||||||
|
AppCode: "lalu",
|
||||||
|
EventID: "game_level:dice_order_1",
|
||||||
|
OrderID: "dice_order_1",
|
||||||
|
UserID: 42,
|
||||||
|
GameID: "dice",
|
||||||
|
ProviderOrderID: "dice_match_1:42:debit",
|
||||||
|
ProviderRoundID: "dice_match_1",
|
||||||
|
CoinAmount: 100,
|
||||||
|
WalletTransactionID: "wtx-dice-1",
|
||||||
|
PayloadJSON: `{"game_id":"dice","match_id":"dice_match_1"}`,
|
||||||
|
OccurredAtMS: 1700000000000,
|
||||||
|
}}}
|
||||||
|
activity := &fakeActivity{status: "consumed"}
|
||||||
|
svc := New(Config{}, repo, &fakeWallet{}, &fakeUser{}, activity)
|
||||||
|
svc.now = func() time.Time { return time.UnixMilli(1700000001000) }
|
||||||
|
|
||||||
|
_, _, success, failure, _, err := svc.ProcessLevelEventOutboxBatch(context.Background(), "run_1", "worker_1", 100, 30*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessLevelEventOutboxBatch failed: %v", err)
|
||||||
|
}
|
||||||
|
if success != 1 || failure != 0 {
|
||||||
|
t.Fatalf("batch result mismatch: success=%d failure=%d", success, failure)
|
||||||
|
}
|
||||||
|
if len(activity.taskRequests) != 2 {
|
||||||
|
t.Fatalf("task request count mismatch: %d", len(activity.taskRequests))
|
||||||
|
}
|
||||||
|
if activity.taskRequests[0].GetMetricType() != "game_spend_coin" || activity.taskRequests[0].GetValue() != 100 {
|
||||||
|
t.Fatalf("game spend task mismatch: %+v", activity.taskRequests[0])
|
||||||
|
}
|
||||||
|
if activity.taskRequests[1].GetMetricType() != "game_dice_play_count" || activity.taskRequests[1].GetValue() != 1 || activity.taskRequests[1].GetEventId() != "game_level:dice_order_1:task:play_count" {
|
||||||
|
t.Fatalf("play count task mismatch: %+v", activity.taskRequests[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type fakeRepository struct {
|
type fakeRepository struct {
|
||||||
launchable gamedomain.LaunchableGame
|
launchable gamedomain.LaunchableGame
|
||||||
platform gamedomain.Platform
|
platform gamedomain.Platform
|
||||||
@ -1846,10 +1882,11 @@ func (f *fakeWallet) GetBalances(context.Context, *walletv1.GetBalancesRequest)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fakeActivity struct {
|
type fakeActivity struct {
|
||||||
status string
|
status string
|
||||||
taskStatus string
|
taskStatus string
|
||||||
last *activityv1.ConsumeLevelEventRequest
|
last *activityv1.ConsumeLevelEventRequest
|
||||||
lastTask *activityv1.ConsumeTaskEventRequest
|
lastTask *activityv1.ConsumeTaskEventRequest
|
||||||
|
taskRequests []*activityv1.ConsumeTaskEventRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeActivity) ConsumeLevelEvent(_ context.Context, req *activityv1.ConsumeLevelEventRequest, _ ...grpc.CallOption) (*activityv1.ConsumeLevelEventResponse, error) {
|
func (f *fakeActivity) ConsumeLevelEvent(_ context.Context, req *activityv1.ConsumeLevelEventRequest, _ ...grpc.CallOption) (*activityv1.ConsumeLevelEventResponse, error) {
|
||||||
@ -1863,6 +1900,7 @@ func (f *fakeActivity) ConsumeLevelEvent(_ context.Context, req *activityv1.Cons
|
|||||||
|
|
||||||
func (f *fakeActivity) ConsumeTaskEvent(_ context.Context, req *activityv1.ConsumeTaskEventRequest, _ ...grpc.CallOption) (*activityv1.ConsumeTaskEventResponse, error) {
|
func (f *fakeActivity) ConsumeTaskEvent(_ context.Context, req *activityv1.ConsumeTaskEventRequest, _ ...grpc.CallOption) (*activityv1.ConsumeTaskEventResponse, error) {
|
||||||
f.lastTask = req
|
f.lastTask = req
|
||||||
|
f.taskRequests = append(f.taskRequests, req)
|
||||||
status := f.taskStatus
|
status := f.taskStatus
|
||||||
if status == "" {
|
if status == "" {
|
||||||
status = "consumed"
|
status = "consumed"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user