From 93f44ecf52c9db760e0a797f3604b6209f39c25d Mon Sep 17 00:00:00 2001 From: zhx Date: Mon, 13 Jul 2026 23:04:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E6=95=9B=E8=BD=AC=E7=9B=98=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E7=A4=BC=E7=89=A9=E8=A1=A5=E5=81=BF=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/domain/resource/constants.go | 13 +- .../internal/service/wallet/service_test.go | 290 ++++++++++++++++++ .../mysql/resource_grant_repository.go | 76 ++++- 3 files changed, 370 insertions(+), 9 deletions(-) diff --git a/services/wallet-service/internal/domain/resource/constants.go b/services/wallet-service/internal/domain/resource/constants.go index 36cc7f89..90946e14 100644 --- a/services/wallet-service/internal/domain/resource/constants.go +++ b/services/wallet-service/internal/domain/resource/constants.go @@ -14,7 +14,7 @@ const ( TypeEmojiPack = "emoji_pack" // TypeVIPTrialCard 是可在背包中独立计时、单选佩戴的 VIP 体验卡。 // VIP 等级和有效期事实仍由 user_vip_trial_cards 保存,资源目录只负责素材和通用背包展示。 - TypeVIPTrialCard = "vip_trial_card" + TypeVIPTrialCard = "vip_trial_card" StatusActive = "active" StatusDisabled = "disabled" @@ -35,10 +35,13 @@ const ( GrantSourceAchievement = "achievement" GrantSourceResourceShop = "resource_shop" GrantSourceGameRobotInit = "game_robot_init" - GrantStatusDone = "succeeded" - GrantStatusRevoked = "revoked" - ResultWalletCredit = "wallet_credit" - ResultEntitlement = "entitlement" + // GrantSourceWheelReward 标识 activity-service 从转盘不可变结算快照发起的奖品发放。 + // 历史兼容还必须满足固定 reason/command namespace、本人操作和 disabled grantable gift 等条件;source 本身不是授权开关。 + GrantSourceWheelReward = "wheel_reward" + GrantStatusDone = "succeeded" + GrantStatusRevoked = "revoked" + ResultWalletCredit = "wallet_credit" + ResultEntitlement = "entitlement" GroupItemTypeResource = "resource" GroupItemTypeWalletAsset = "wallet_asset" diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index d2b3ec7b..1d4634b1 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -5803,6 +5803,296 @@ func TestGameRobotInitCanGrantActiveNonGrantableResource(t *testing.T) { } } +// TestWheelRewardCompensationCanGrantOnlyDisabledGrantableResource 锁定历史转盘补偿的最窄豁免边界: +// 协议标记匹配后仍只允许本人补偿 disabled grantable gift,且重放必须直接返回首次 grant,不能重复增加权益或 outbox。 +func TestWheelRewardCompensationCanGrantOnlyDisabledGrantableResource(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + ctx := context.Background() + + disabledGrantable, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "wheel_disabled_grantable_gift", + ResourceType: resourcedomain.TypeGift, + Name: "Disabled Historical Wheel Gift", + Status: resourcedomain.StatusDisabled, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity, + UsageScopes: []string{"wheel_reward"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create disabled wheel resource failed: %v", err) + } + const baseCommandID = "wheel_resource:wheel_draw_1720000000000_0123456789abcdef" + command := resourcedomain.GrantResourceCommand{ + CommandID: baseCommandID, + TargetUserID: 43101, + ResourceID: disabledGrantable.ResourceID, + Quantity: 2, + Reason: resourcedomain.GrantSourceWheelReward, + OperatorUserID: 43101, + GrantSource: resourcedomain.GrantSourceWheelReward, + } + first, err := svc.GrantResource(ctx, command) + if err != nil { + t.Fatalf("historical wheel compensation should grant disabled grantable resource: %v", err) + } + replayed, err := svc.GrantResource(ctx, command) + if err != nil { + t.Fatalf("historical wheel compensation replay failed: %v", err) + } + if first.GrantID == "" || replayed.GrantID != first.GrantID || len(first.Items) != 1 || len(replayed.Items) != 1 || replayed.Items[0].GrantItemID != first.Items[0].GrantItemID { + t.Fatalf("wheel compensation idempotency mismatch: first=%+v replayed=%+v", first, replayed) + } + _, quantity, remaining := repository.ActiveResourceEntitlement(command.TargetUserID, disabledGrantable.ResourceID) + if quantity != command.Quantity || remaining != command.Quantity { + t.Fatalf("wheel compensation replay must not double entitlement: quantity=%d remaining=%d command=%+v", quantity, remaining, command) + } + if got := repository.CountRows("resource_grants", "command_id = ?", command.CommandID); got != 1 { + t.Fatalf("wheel compensation replay must keep one grant, got %d", got) + } + if got := repository.CountRows("resource_grant_items", "grant_id = ?", first.GrantID); got != 1 { + t.Fatalf("wheel compensation replay must keep one grant item, got %d", got) + } + if got := repository.CountRows("wallet_outbox", "command_id = ? AND event_type = ?", command.CommandID, "ResourceGranted"); got != 1 { + t.Fatalf("wheel compensation replay must publish ResourceGranted once, got %d", got) + } + + // 后台即使复用 wheel command/reason 也不能发 disabled 资源;admin source 必须继续沿用原有 active-only 语义。 + adminCommand := command + adminCommand.CommandID = baseCommandID + ":2" + adminCommand.TargetUserID = 43102 + adminCommand.GrantSource = resourcedomain.GrantSourceAdmin + if _, err := svc.GrantResource(ctx, adminCommand); !xerr.IsCode(err, xerr.Conflict) { + t.Fatalf("admin must not grant disabled wheel resource, got %v", err) + } + + deletedResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "wheel_deleted_grantable_gift", + ResourceType: resourcedomain.TypeGift, + Name: "Deleted Historical Wheel Gift", + Status: resourcedomain.StatusDisabled, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity, + UsageScopes: []string{"wheel_reward"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create deletable wheel resource failed: %v", err) + } + if _, err := svc.DeleteResource(ctx, resourcedomain.StatusCommand{ID: deletedResource.ResourceID, OperatorUserID: 90001}); err != nil { + t.Fatalf("delete wheel resource failed: %v", err) + } + deletedCommand := command + deletedCommand.CommandID = baseCommandID + ":3" + deletedCommand.TargetUserID = 43103 + deletedCommand.OperatorUserID = deletedCommand.TargetUserID + deletedCommand.ResourceID = deletedResource.ResourceID + if _, err := svc.GrantResource(ctx, deletedCommand); !xerr.IsCode(err, xerr.Conflict) { + t.Fatalf("deleted resource must remain rejected for wheel compensation, got %v", err) + } + + nonGrantable, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "wheel_disabled_non_grantable_gift", + ResourceType: resourcedomain.TypeGift, + Name: "Non Grantable Historical Wheel Gift", + Status: resourcedomain.StatusDisabled, + Grantable: false, + GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity, + UsageScopes: []string{"wheel_reward"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create non-grantable wheel resource failed: %v", err) + } + nonGrantableCommand := command + nonGrantableCommand.CommandID = baseCommandID + ":4" + nonGrantableCommand.TargetUserID = 43104 + nonGrantableCommand.OperatorUserID = nonGrantableCommand.TargetUserID + nonGrantableCommand.ResourceID = nonGrantable.ResourceID + if _, err := svc.GrantResource(ctx, nonGrantableCommand); !xerr.IsCode(err, xerr.Conflict) { + t.Fatalf("non-grantable resource must remain rejected for wheel compensation, got %v", err) + } + + // 只伪造 wheel_reward source 不足以进入兼容分支;reason 或命名空间任一不匹配都应在读取资源前作为畸形 Wheel 命令拒绝。 + for _, forged := range []resourcedomain.GrantResourceCommand{ + { + CommandID: baseCommandID + ":5", TargetUserID: 43105, ResourceID: disabledGrantable.ResourceID, Quantity: 1, + Reason: "manual repair", OperatorUserID: 43105, GrantSource: resourcedomain.GrantSourceWheelReward, + }, + { + CommandID: "manual_resource:forged-command", TargetUserID: 43106, ResourceID: disabledGrantable.ResourceID, Quantity: 1, + Reason: resourcedomain.GrantSourceWheelReward, OperatorUserID: 43106, GrantSource: resourcedomain.GrantSourceWheelReward, + }, + } { + if _, err := svc.GrantResource(ctx, forged); !xerr.IsCode(err, xerr.InvalidArgument) { + t.Fatalf("forged wheel identity must not grant disabled resource: command=%+v err=%v", forged, err) + } + } + + operatorMismatch := command + operatorMismatch.CommandID = baseCommandID + ":6" + operatorMismatch.TargetUserID = 43107 + operatorMismatch.OperatorUserID = 90001 + if _, err := svc.GrantResource(ctx, operatorMismatch); !xerr.IsCode(err, xerr.InvalidArgument) { + t.Fatalf("wheel compensation must not grant for a different operator, got %v", err) + } + + disabledBadge, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "wheel_disabled_badge", + ResourceType: resourcedomain.TypeBadge, + Name: "Disabled Wheel Badge", + Status: resourcedomain.StatusDisabled, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategySetActiveFlag, + UsageScopes: []string{"profile"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create disabled non-gift resource failed: %v", err) + } + nonGiftCommand := command + nonGiftCommand.CommandID = baseCommandID + ":7" + nonGiftCommand.TargetUserID = 43108 + nonGiftCommand.OperatorUserID = nonGiftCommand.TargetUserID + nonGiftCommand.ResourceID = disabledBadge.ResourceID + if _, err := svc.GrantResource(ctx, nonGiftCommand); !xerr.IsCode(err, xerr.Conflict) { + t.Fatalf("disabled non-gift resource must remain rejected for wheel compensation, got %v", err) + } + + disabledCoin, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "wheel_disabled_coin", + ResourceType: resourcedomain.TypeCoin, + Name: "Disabled Wheel Coin", + Status: resourcedomain.StatusDisabled, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategyWalletCredit, + WalletAssetType: ledger.AssetCoin, + WalletAssetAmount: 100, + UsageScopes: []string{"wallet"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create disabled coin resource failed: %v", err) + } + coinCommand := command + coinCommand.CommandID = baseCommandID + ":8" + coinCommand.TargetUserID = 43109 + coinCommand.OperatorUserID = coinCommand.TargetUserID + coinCommand.ResourceID = disabledCoin.ResourceID + if _, err := svc.GrantResource(ctx, coinCommand); !xerr.IsCode(err, xerr.Conflict) { + t.Fatalf("disabled wallet credit/coin resource must remain rejected for wheel compensation, got %v", err) + } + + disabledWrongStrategy, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "wheel_disabled_wrong_strategy_gift", + ResourceType: resourcedomain.TypeGift, + Name: "Disabled Wheel Set Active Gift", + Status: resourcedomain.StatusDisabled, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategySetActiveFlag, + UsageScopes: []string{"wheel_reward"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create disabled wrong-strategy gift failed: %v", err) + } + wrongStrategyCommand := command + wrongStrategyCommand.CommandID = baseCommandID + ":9" + wrongStrategyCommand.TargetUserID = 43110 + wrongStrategyCommand.OperatorUserID = wrongStrategyCommand.TargetUserID + wrongStrategyCommand.ResourceID = disabledWrongStrategy.ResourceID + if _, err := svc.GrantResource(ctx, wrongStrategyCommand); !xerr.IsCode(err, xerr.Conflict) { + t.Fatalf("disabled gift with non-quantity strategy must remain rejected, got %v", err) + } + + durationCommand := command + durationCommand.CommandID = baseCommandID + ":10" + durationCommand.TargetUserID = 43111 + durationCommand.OperatorUserID = durationCommand.TargetUserID + durationCommand.DurationMS = int64(time.Hour / time.Millisecond) + if _, err := svc.GrantResource(ctx, durationCommand); !xerr.IsCode(err, xerr.Conflict) { + t.Fatalf("disabled wheel gift with duration must remain rejected, got %v", err) + } +} + +// TestWheelRewardGrantRejectsMalformedCommandsForActiveResource 验证严格 Wheel 协议校验不依赖 disabled 分支; +// active 资源也不能接受被 trim 修复、非 canonical 数字、大小写漂移或多段 suffix 的 command_id。 +func TestWheelRewardGrantRejectsMalformedCommandsForActiveResource(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + ctx := context.Background() + resource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "active_wheel_command_validation_gift", + ResourceType: resourcedomain.TypeGift, + Name: "Active Wheel Command Validation Gift", + Status: resourcedomain.StatusActive, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity, + UsageScopes: []string{"wheel_reward"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create active wheel command validation resource failed: %v", err) + } + overlong := "wheel_resource:wheel_draw_1720000000000_" + strings.Repeat("a", 100) + if len(overlong) <= 128 { + t.Fatalf("test setup must exceed command_id limit: len=%d", len(overlong)) + } + invalidCommandIDs := []string{ + "", + "wheel_resource:wheel_draw__0123456789abcdef", + "wheel_resource:wheel_draw_0_0123456789abcdef", + "wheel_resource:wheel_draw_-1_0123456789abcdef", + "wheel_resource:wheel_draw_now_0123456789abcdef", + "wheel_resource:wheel_draw_01720000000000_0123456789abcdef", + "wheel_resource:wheel_draw_1720000000000_", + "wheel_resource:wheel_draw_1720000000000_abcdef", + "wheel_resource:wheel_draw_1720000000000_0123456789ABCDEF", + "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:", + "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:0", + "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:-1", + "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:item", + "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:01", + "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:1:2", + " wheel_resource:wheel_draw_1720000000000_0123456789abcdef", + "wheel_resource:wheel_draw_1720000000000_0123456789abcdef ", + overlong, + } + for index, commandID := range invalidCommandIDs { + _, err := svc.GrantResource(ctx, resourcedomain.GrantResourceCommand{ + CommandID: commandID, + TargetUserID: 43200 + int64(index), + ResourceID: resource.ResourceID, + Quantity: 1, + Reason: resourcedomain.GrantSourceWheelReward, + OperatorUserID: 43200 + int64(index), + GrantSource: resourcedomain.GrantSourceWheelReward, + }) + if !xerr.IsCode(err, xerr.InvalidArgument) { + t.Fatalf("active resource accepted malformed wheel command[%d]=%q: %v", index, commandID, err) + } + } + validCommandID := "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:1" + for _, malformedIdentity := range []resourcedomain.GrantResourceCommand{ + { + CommandID: validCommandID, TargetUserID: 43250, ResourceID: resource.ResourceID, Quantity: 1, + Reason: "manual repair", OperatorUserID: 43250, GrantSource: resourcedomain.GrantSourceWheelReward, + }, + { + CommandID: validCommandID, TargetUserID: 43251, ResourceID: resource.ResourceID, Quantity: 1, + Reason: resourcedomain.GrantSourceWheelReward, OperatorUserID: 90001, GrantSource: resourcedomain.GrantSourceWheelReward, + }, + } { + if _, err := svc.GrantResource(ctx, malformedIdentity); !xerr.IsCode(err, xerr.InvalidArgument) { + t.Fatalf("active resource accepted malformed wheel identity: command=%+v err=%v", malformedIdentity, err) + } + } + if got := repository.CountRows("resource_grants", "resource_id = ?", resource.ResourceID); got != 0 { + t.Fatalf("malformed active wheel commands must not persist grants, got %d", got) + } +} + func TestCreateResourceRejectsDuplicateResourceCodeAsConflict(t *testing.T) { repository := mysqltest.NewRepository(t) svc := walletservice.New(repository) diff --git a/services/wallet-service/internal/storage/mysql/resource_grant_repository.go b/services/wallet-service/internal/storage/mysql/resource_grant_repository.go index 9d42a33e..9751e58f 100644 --- a/services/wallet-service/internal/storage/mysql/resource_grant_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_grant_repository.go @@ -48,7 +48,7 @@ func (r *Repository) GrantResource(ctx context.Context, command resourcedomain.G if resourcedomain.NormalizeResourceType(resource.ResourceType) == resourcedomain.TypeVIPTrialCard { return resourcedomain.ResourceGrant{}, xerr.New(xerr.InvalidArgument, "vip_trial_card must use GrantVipTrialCard") } - if err := validateGrantableResourceForSource(resource, command.GrantSource); err != nil { + if err := validateGrantableResourceForCommand(resource, command); err != nil { return resourcedomain.ResourceGrant{}, err } grantID := resourceGrantID(command.AppCode, command.CommandID) @@ -786,9 +786,12 @@ func normalizeResourceGrantsQuery(query resourcedomain.ListResourceGrantsQuery) } func normalizeGrantResourceCommand(command resourcedomain.GrantResourceCommand) resourcedomain.GrantResourceCommand { - command.CommandID = strings.TrimSpace(command.CommandID) - command.Reason = strings.TrimSpace(command.Reason) command.GrantSource = resourcedomain.NormalizeGrantSource(command.GrantSource) + // 普通历史调用继续保留 trim 兼容;Wheel command 是结算事实标识,必须保留原始字节交给严格格式校验,不能把带空白的畸形输入静默修正成合法命令。 + if command.GrantSource != resourcedomain.GrantSourceWheelReward { + command.CommandID = strings.TrimSpace(command.CommandID) + } + command.Reason = strings.TrimSpace(command.Reason) if command.Quantity == 0 { command.Quantity = 1 } @@ -802,9 +805,53 @@ func validateGrantResourceCommand(command resourcedomain.GrantResourceCommand) e if command.DurationMS < 0 { return xerr.New(xerr.InvalidArgument, "duration_ms is invalid") } + if command.GrantSource == resourcedomain.GrantSourceWheelReward { + // Wheel source 由 activity owner 使用:即便资源当前 active,也必须先固定命令形状和本人操作语义,不能把可伪造的协议字符串当成 disabled 例外的授权依据。 + if command.Reason != resourcedomain.GrantSourceWheelReward || command.OperatorUserID != command.TargetUserID || !validWheelResourceGrantCommandID(command.CommandID) { + return xerr.New(xerr.InvalidArgument, "wheel reward resource grant command is invalid") + } + } return nil } +func validWheelResourceGrantCommandID(commandID string) bool { + const ( + commandPrefix = "wheel_resource" + drawPrefix = "wheel_draw_" + entropyLength = 16 // pkg/idgen.New 固定使用 8 bytes entropy,编码后必须是 16 位小写 hex。 + maxLength = 128 + ) + if commandID == "" || len(commandID) > maxLength || strings.TrimSpace(commandID) != commandID { + return false + } + parts := strings.Split(commandID, ":") + if len(parts) != 2 && len(parts) != 3 { + return false + } + if parts[0] != commandPrefix || !strings.HasPrefix(parts[1], drawPrefix) { + return false + } + drawParts := strings.Split(strings.TrimPrefix(parts[1], drawPrefix), "_") + if len(drawParts) != 2 || !validCanonicalPositiveDecimal(drawParts[0]) || len(drawParts[1]) != entropyLength || !isLowerHex(drawParts[1]) { + return false + } + return len(parts) == 2 || validCanonicalPositiveDecimal(parts[2]) +} + +func validCanonicalPositiveDecimal(value string) bool { + parsed, err := strconv.ParseUint(value, 10, 64) + return err == nil && parsed > 0 && strconv.FormatUint(parsed, 10) == value +} + +func isLowerHex(value string) bool { + for _, char := range value { + if (char < '0' || char > '9') && (char < 'a' || char > 'f') { + return false + } + } + return value != "" +} + func normalizeGrantResourceGroupCommand(command resourcedomain.GrantResourceGroupCommand) resourcedomain.GrantResourceGroupCommand { command.CommandID = strings.TrimSpace(command.CommandID) command.Reason = strings.TrimSpace(command.Reason) @@ -837,7 +884,6 @@ func validateRevokeResourceGrantCommand(command resourcedomain.RevokeResourceGra } func validateGrantableResourceForSource(resource resourcedomain.Resource, grantSource string) error { - if grantSource == resourcedomain.GrantSourceGameRobotInit { if resource.Status != resourcedomain.StatusActive { return xerr.New(xerr.Conflict, "resource is disabled") @@ -856,6 +902,28 @@ func validateGrantableResourceForSource(resource resourcedomain.Resource, grantS return nil } +func validateGrantableResourceForCommand(resource resourcedomain.Resource, command resourcedomain.GrantResourceCommand) error { + // 历史转盘结算可能在 outbox 堆积期间遇到奖品下架:disabled 仅表示停止新配置/展示,不应抹掉抽奖时已经固化的中奖事实。 + // source/reason/command 前缀只用于识别协议,不能独立充当授权;本人操作和 gift-only 约束共同阻断后台借道、加币与自动装备副作用。 + if resource.Status == resourcedomain.StatusDisabled && command.GrantSource == resourcedomain.GrantSourceWheelReward { + // deleted 表示资源事实已回收,且只会走下方原有校验;这里仍要求 grantable,确保运营明确关闭发放能力时补偿也会停住。 + if !resource.Grantable { + return xerr.New(xerr.Conflict, "resource is not grantable") + } + if command.OperatorUserID != command.TargetUserID { + return xerr.New(xerr.PermissionDenied, "wheel reward compensation must be performed for the same user") + } + if resourcedomain.NormalizeResourceType(resource.ResourceType) != resourcedomain.TypeGift || resourcedomain.IsCoinResource(resource) || resourcedomain.IsWalletCredit(resource) { + return xerr.New(xerr.Conflict, "disabled wheel reward compensation only supports non-wallet gift resources") + } + if resourcedomain.NormalizeGrantStrategy(resource.GrantStrategy) != resourcedomain.GrantStrategyIncreaseQuantity || command.DurationMS != 0 { + return xerr.New(xerr.Conflict, "disabled wheel reward compensation requires quantity-only grant semantics") + } + return nil + } + return validateGrantableResourceForSource(resource, command.GrantSource) +} + func validateGrantableResource(resource resourcedomain.Resource) error { if resource.Status != resourcedomain.StatusActive { return xerr.New(xerr.Conflict, "resource is disabled")