diff --git a/server/admin/internal/modules/gamemanagement/robot_appearance.go b/server/admin/internal/modules/gamemanagement/robot_appearance.go index 4892c21f..301bf8ed 100644 --- a/server/admin/internal/modules/gamemanagement/robot_appearance.go +++ b/server/admin/internal/modules/gamemanagement/robot_appearance.go @@ -90,20 +90,19 @@ func (h *Handler) loadRobotAppearanceCatalog(ctx context.Context, requestID stri func (h *Handler) listRobotResources(ctx context.Context, requestID string, appCode string, resourceType string) ([]*walletv1.Resource, error) { resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{ - RequestId: requestID, - AppCode: appCode, - ResourceType: resourceType, - Page: 1, - PageSize: 500, - ActiveOnly: true, - ManagerGrantOnly: true, + RequestId: requestID, + AppCode: appCode, + ResourceType: resourceType, + Page: 1, + PageSize: 500, + ActiveOnly: true, }) if err != nil { return nil, fmt.Errorf("读取机器人%s资源失败: %w", resourceType, err) } resources := make([]*walletv1.Resource, 0, len(resp.GetResources())) for _, item := range resp.GetResources() { - if item != nil && item.GetResourceId() > 0 && item.GetStatus() == "active" && item.GetManagerGrantEnabled() { + if item != nil && item.GetResourceId() > 0 && item.GetStatus() == "active" { resources = append(resources, item) } } diff --git a/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go b/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go index 24564400..97f3b694 100644 --- a/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go +++ b/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go @@ -65,20 +65,22 @@ func (c *fakeRobotGameClient) RegisterDiceRobots(_ context.Context, req *gamev1. type fakeRobotWalletClient struct { walletclient.Client - grants []string - equips []int64 + listRequests []*walletv1.ListResourcesRequest + grants []string + equips []int64 } func (c *fakeRobotWalletClient) ListResources(_ context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error) { + c.listRequests = append(c.listRequests, req) switch req.GetResourceType() { case "avatar_frame": - return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{{ResourceId: 101, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: true}}}, nil + return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{{ResourceId: 101, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false}}}, nil case "vehicle": - return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{{ResourceId: 201, ResourceType: "vehicle", Status: "active", ManagerGrantEnabled: true}}}, nil + return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{{ResourceId: 201, ResourceType: "vehicle", Status: "active", ManagerGrantEnabled: false}}}, nil case "badge": return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{ - {ResourceId: 301, ResourceType: "badge", Status: "active", ManagerGrantEnabled: true, MetadataJson: `{"badge_form":"strip"}`}, - {ResourceId: 302, ResourceType: "badge", Status: "active", ManagerGrantEnabled: true, MetadataJson: `{"badge_form":"tile"}`}, + {ResourceId: 301, ResourceType: "badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"strip"}`}, + {ResourceId: 302, ResourceType: "badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"tile"}`}, }}, nil default: return &walletv1.ListResourcesResponse{}, nil @@ -158,6 +160,14 @@ func TestGenerateDiceRobotsUsesLikeiProfiles(t *testing.T) { if len(wallets.grants) != 8 || len(wallets.equips) != 4 || len(activities.levels) != 4 { t.Fatalf("robot appearance init mismatch: grants=%d equips=%d levels=%d", len(wallets.grants), len(wallets.equips), len(activities.levels)) } + for _, req := range wallets.listRequests { + if req.GetManagerGrantOnly() { + t.Fatalf("robot appearance resource list must not require manager grant: %+v", req) + } + if !req.GetActiveOnly() { + t.Fatalf("robot appearance resource list must still require active resources: %+v", req) + } + } for _, level := range activities.levels { if level.GetLevel() < robotMinDisplayLevel || level.GetLevel() > robotMaxDisplayLevel { t.Fatalf("robot level out of range: %+v", level) diff --git a/services/wallet-service/internal/domain/resource/resource.go b/services/wallet-service/internal/domain/resource/resource.go index 09ea6080..2410d1e2 100644 --- a/services/wallet-service/internal/domain/resource/resource.go +++ b/services/wallet-service/internal/domain/resource/resource.go @@ -37,6 +37,7 @@ const ( GrantSourceGrowthLevel = "growth_level" GrantSourceAchievement = "achievement" GrantSourceResourceShop = "resource_shop" + GrantSourceGameRobotInit = "game_robot_init" GrantStatusDone = "succeeded" ResultWalletCredit = "wallet_credit" ResultEntitlement = "entitlement" diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index dcf6a237..1ecb5197 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -3863,6 +3863,46 @@ func TestManagerCenterResourceGrantFiltersAndRechecksSwitch(t *testing.T) { } } +// TestGameRobotInitCanGrantActiveNonGrantableResource 锁定机器人初始化发放不跟随人工发放开关; +// 只要资源仍处于 active,就允许机器人账号获得装扮,避免运营库没有打开 grantable 时生成失败。 +func TestGameRobotInitCanGrantActiveNonGrantableResource(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + ctx := context.Background() + + resource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "robot_init_frame", + ResourceType: resourcedomain.TypeAvatarFrame, + Name: "Robot Init Frame", + Status: resourcedomain.StatusActive, + Grantable: false, + ManagerGrantEnabled: false, + GrantStrategy: resourcedomain.GrantStrategySetActiveFlag, + UsageScopes: []string{"profile"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create robot init resource failed: %v", err) + } + + grant, err := svc.GrantResource(ctx, resourcedomain.GrantResourceCommand{ + CommandID: "cmd-game-robot-init-non-grantable", + TargetUserID: 43009, + ResourceID: resource.ResourceID, + Quantity: 1, + DurationMS: int64(24 * time.Hour / time.Millisecond), + Reason: "game robot init", + OperatorUserID: 91001, + GrantSource: resourcedomain.GrantSourceGameRobotInit, + }) + if err != nil { + t.Fatalf("game robot init should grant active non-grantable resource: %v", err) + } + if grant.GrantSource != resourcedomain.GrantSourceGameRobotInit || len(grant.Items) != 1 { + t.Fatalf("game robot init grant response mismatch: %+v", grant) + } +} + func TestCreateResourceRejectsDuplicateResourceCodeAsConflict(t *testing.T) { repository := mysqltest.NewRepository(t) svc := walletservice.New(repository) diff --git a/services/wallet-service/internal/storage/mysql/resource_repository.go b/services/wallet-service/internal/storage/mysql/resource_repository.go index 97ad7bda..5292c299 100644 --- a/services/wallet-service/internal/storage/mysql/resource_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_repository.go @@ -3561,6 +3561,18 @@ func validateGrantResourceGroupCommand(command resourcedomain.GrantResourceGroup } func validateGrantableResourceForSource(resource resourcedomain.Resource, grantSource string) error { + // 机器人初始化只从后台维护的 active 装扮池里挑展示资源;它不是人工管理端发放, + // 因此不继承 grantable 和 manager_grant_enabled 这两个运营开关,避免测试/运营库 + // 已上架资源因为未显式打开管理端发放而无法生成机器人。 + if grantSource == resourcedomain.GrantSourceGameRobotInit { + if resource.Status != resourcedomain.StatusActive { + return xerr.New(xerr.Conflict, "resource is disabled") + } + if resourcedomain.IsWalletCredit(resource) && !resourcedomain.IsCoinResource(resource) { + return xerr.New(xerr.InvalidArgument, "wallet credit resource must be coin") + } + return nil + } if err := validateGrantableResource(resource); err != nil { return err }