feat: generalize manager cross-region scope
This commit is contained in:
parent
2444a7a108
commit
f19f212e54
File diff suppressed because it is too large
Load Diff
@ -487,6 +487,8 @@ message RoleScopePolicy {
|
||||
string scene = 1;
|
||||
string base_scope = 2;
|
||||
bool region_expansion_configurable = 3;
|
||||
// expanded_scope 是经理通用范围开关开启后的范围;调用方不能自行假设一定是 region。
|
||||
string expanded_scope = 4;
|
||||
}
|
||||
|
||||
message GetRoleScopePolicyRequest {
|
||||
|
||||
@ -79,7 +79,7 @@ graph LR
|
||||
|
||||
当前 `user_id` 是内部稳定主键,后台仍必须在所有 host/Agency/BD 查询、幂等和审计 detail 中带上 `app_code`。如果某些现有表还使用全局 `PRIMARY KEY(user_id)`、`PRIMARY KEY(command_id)` 或 `PRIMARY KEY(event_id)`,这只能说明当前实现暂时要求这些 ID 全局唯一;不能据此省略 `app_code`,也不能在多 App 写入前声称同一个 `command_id` 可以跨 App 复用。
|
||||
|
||||
经理中心创建 BD Leader 时,`app_code` 必须来自已验证的 App token,不接受客户端自报租户。App 差异统一由 user-service 的角色范围策略解析:Huwaa 的基础范围是 `global`;其他 App 的基础范围是 `country`,且只有策略声明可配置时,经理的区域邀请开关才能把有效范围扩展为 `region`。搜索和创建入口只消费 `country|region|global` 策略结果,不直接判断具体 App;两类路径都必须保留 active Manager 和 `manager_add_bd_leader` 能力校验。
|
||||
经理中心所有目标用户操作都必须从已验证的 App token 取得 `app_code`,不接受客户端自报租户。App 差异统一由 user-service 的 `manager_operations` 范围策略解析:Huwaa 的基础范围是 `global`;其他 App 的基础范围是 `country`,后台开启“跨区经理”后有效范围扩展为同一有效 `region_id`,仍禁止跨区域。头像框、座驾、徽章、VIP、升级等级、BD Leader/Admin/Superadmin、封禁和转移国家的搜索与写入口共用同一个范围匹配器,不能各自判断具体 App 或让 `scope=all` 绕过数据范围;各动作原有功能权限仍独立校验。
|
||||
|
||||
```sql
|
||||
admin_operation_logs(
|
||||
@ -526,5 +526,5 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
||||
- BD/BD Leader base is downstream host salary only.
|
||||
- Leader direct Agency salary source must be de-duplicated.
|
||||
- Every admin mutation must be audited and idempotent.
|
||||
- Huwaa 跨区例外只适用于 Agency 邀请、Host 主动申请、BD 邀请和经理创建 BD Leader;Coin Seller 仍为同区域。
|
||||
- Huwaa 跨区例外适用于 Agency 邀请、Host 主动申请、BD 邀请和全部经理中心目标用户操作;Coin Seller 仍为同区域。
|
||||
- Redis cannot be the source of truth for policies, salary, relationships, or audit.
|
||||
|
||||
23
scripts/mysql/065_manager_cross_region_scope.sql
Normal file
23
scripts/mysql/065_manager_cross_region_scope.sql
Normal file
@ -0,0 +1,23 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE hyapp_user;
|
||||
|
||||
-- 通用跨区经理替代上一版 BD Leader 专用扩区开关;默认关闭,不扩大任何现有经理的数据范围。
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'manager_profiles' AND COLUMN_NAME = 'can_manage_cross_region') = 0,
|
||||
'ALTER TABLE manager_profiles ADD COLUMN can_manage_cross_region TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否允许经理中心所有目标用户操作跨区域执行'' AFTER can_add_bd_leader',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 若 064 已落库,则继承显式开启值;未执行 064 的环境保持默认关闭。
|
||||
SET @copy_legacy := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'manager_profiles' AND COLUMN_NAME = 'can_add_bd_leader_in_region') > 0,
|
||||
'UPDATE manager_profiles SET can_manage_cross_region = can_add_bd_leader_in_region WHERE can_add_bd_leader_in_region = 1',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @copy_legacy;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -122,6 +122,7 @@ type GetRoleScopePolicyRequest struct {
|
||||
type RoleScopePolicy struct {
|
||||
Scene string `json:"scene"`
|
||||
BaseScope string `json:"baseScope"`
|
||||
ExpandedScope string `json:"expandedScope"`
|
||||
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
||||
}
|
||||
|
||||
@ -846,6 +847,7 @@ func (c *GRPCClient) GetRoleScopePolicy(ctx context.Context, req GetRoleScopePol
|
||||
return &RoleScopePolicy{
|
||||
Scene: policy.GetScene(),
|
||||
BaseScope: policy.GetBaseScope(),
|
||||
ExpandedScope: policy.GetExpandedScope(),
|
||||
RegionExpansionConfigurable: policy.GetRegionExpansionConfigurable(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ func (h *Handler) ListManagers(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, ManagerPageResponse{
|
||||
Items: items, Page: query.Page, PageSize: query.PageSize, Total: total,
|
||||
BDLeaderInviteScopePolicy: policy,
|
||||
ManagerOperationScopePolicy: policy,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -68,45 +68,47 @@ type UserIdentity struct {
|
||||
}
|
||||
|
||||
type ManagerListItem struct {
|
||||
UserID int64 `json:"userId,string"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
RegionName string `json:"regionName"`
|
||||
Status string `json:"status"`
|
||||
Contact string `json:"contact"`
|
||||
CanGrantAvatarFrame bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge bool `json:"canGrantBadge"`
|
||||
CanGrantVIP bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader bool `json:"canAddBdLeader"`
|
||||
CanAddBDLeaderInRegion bool `json:"canAddBdLeaderInRegion"`
|
||||
BDLeaderInviteBaseScope string `json:"bdLeaderInviteBaseScope"`
|
||||
BDLeaderInviteEffectiveScope string `json:"bdLeaderInviteEffectiveScope"`
|
||||
BDLeaderRegionExpansionConfigurable bool `json:"bdLeaderRegionExpansionConfigurable"`
|
||||
CanAddAdmin bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser bool `json:"canBlockUser"`
|
||||
CanTransferCountry bool `json:"canTransferUserCountry"`
|
||||
BDLeaderCount int64 `json:"bdLeaderCount"`
|
||||
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
UserID int64 `json:"userId,string"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
RegionName string `json:"regionName"`
|
||||
Status string `json:"status"`
|
||||
Contact string `json:"contact"`
|
||||
CanGrantAvatarFrame bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge bool `json:"canGrantBadge"`
|
||||
CanGrantVIP bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader bool `json:"canAddBdLeader"`
|
||||
CanManageCrossRegion bool `json:"canManageCrossRegion"`
|
||||
LegacyBDLeaderInRegion bool `json:"canAddBdLeaderInRegion"`
|
||||
ManagerOperationBaseScope string `json:"managerOperationBaseScope"`
|
||||
ManagerOperationEffectiveScope string `json:"managerOperationEffectiveScope"`
|
||||
ManagerCrossRegionConfigurable bool `json:"managerCrossRegionConfigurable"`
|
||||
CanAddAdmin bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser bool `json:"canBlockUser"`
|
||||
CanTransferCountry bool `json:"canTransferUserCountry"`
|
||||
BDLeaderCount int64 `json:"bdLeaderCount"`
|
||||
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
type ManagerRoleScopePolicy struct {
|
||||
BaseScope string `json:"baseScope"`
|
||||
ExpandedScope string `json:"expandedScope"`
|
||||
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
||||
}
|
||||
|
||||
type ManagerPageResponse struct {
|
||||
Items []*ManagerListItem `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int64 `json:"total"`
|
||||
BDLeaderInviteScopePolicy ManagerRoleScopePolicy `json:"bdLeaderInviteScopePolicy"`
|
||||
Items []*ManagerListItem `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int64 `json:"total"`
|
||||
ManagerOperationScopePolicy ManagerRoleScopePolicy `json:"managerOperationScopePolicy"`
|
||||
}
|
||||
|
||||
type CoinSellerSalaryRateTier struct {
|
||||
@ -662,7 +664,7 @@ func (r *Reader) CreateManagerProfile(ctx context.Context, userID int64, actorID
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO manager_profiles (
|
||||
app_code, user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
|
||||
can_grant_vip, can_update_user_level, can_add_bd_leader, can_add_bd_leader_in_region, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
|
||||
can_grant_vip, can_update_user_level, can_add_bd_leader, can_manage_cross_region, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
@ -674,14 +676,14 @@ func (r *Reader) CreateManagerProfile(ctx context.Context, userID int64, actorID
|
||||
can_grant_vip = VALUES(can_grant_vip),
|
||||
can_update_user_level = VALUES(can_update_user_level),
|
||||
can_add_bd_leader = VALUES(can_add_bd_leader),
|
||||
can_add_bd_leader_in_region = VALUES(can_add_bd_leader_in_region),
|
||||
can_manage_cross_region = VALUES(can_manage_cross_region),
|
||||
can_add_admin = VALUES(can_add_admin),
|
||||
can_add_superadmin = VALUES(can_add_superadmin),
|
||||
can_block_user = VALUES(can_block_user),
|
||||
can_transfer_user_country = VALUES(can_transfer_user_country),
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, appCode, userID, contact, permissions.CanGrantAvatarFrame, permissions.CanGrantVehicle,
|
||||
permissions.CanGrantBadge, permissions.CanGrantVIP, permissions.CanUpdateUserLevel, permissions.CanAddBDLeader, permissions.CanAddBDLeaderInRegion, permissions.CanAddAdmin,
|
||||
permissions.CanGrantBadge, permissions.CanGrantVIP, permissions.CanUpdateUserLevel, permissions.CanAddBDLeader, permissions.CanManageCrossRegion, permissions.CanAddAdmin,
|
||||
permissions.CanAddSuperadmin, permissions.CanBlockUser, permissions.CanTransferCountry, actorID, nowMs, nowMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -789,14 +791,14 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
|
||||
COALESCE(manager.avatar, ''), `+userCountryRegionIDSQL("manager_region")+`, `+userCountryRegionNameSQL("manager_region")+`,
|
||||
mp.status, COALESCE(mp.contact_info, ''),
|
||||
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_grant_badge, mp.can_grant_vip, mp.can_update_user_level,
|
||||
mp.can_add_bd_leader, mp.can_add_bd_leader_in_region, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
|
||||
mp.can_add_bd_leader, mp.can_manage_cross_region, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
|
||||
COUNT(DISTINCT bl.user_id), COALESCE(MAX(bl.created_at_ms), 0),
|
||||
mp.created_at_ms, mp.updated_at_ms
|
||||
%s
|
||||
GROUP BY mp.user_id, manager.current_display_user_id, manager.username, manager.avatar,
|
||||
manager_region.region_id, manager_region.name, mp.status, mp.contact_info,
|
||||
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_grant_badge, mp.can_grant_vip, mp.can_update_user_level,
|
||||
mp.can_add_bd_leader, mp.can_add_bd_leader_in_region, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
|
||||
mp.can_add_bd_leader, mp.can_manage_cross_region, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
|
||||
mp.created_at_ms, mp.updated_at_ms
|
||||
ORDER BY mp.updated_at_ms DESC, mp.user_id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@ -824,7 +826,7 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
|
||||
&item.CanGrantVIP,
|
||||
&item.CanUpdateUserLevel,
|
||||
&item.CanAddBDLeader,
|
||||
&item.CanAddBDLeaderInRegion,
|
||||
&item.CanManageCrossRegion,
|
||||
&item.CanAddAdmin,
|
||||
&item.CanAddSuperadmin,
|
||||
&item.CanBlockUser,
|
||||
|
||||
@ -75,7 +75,7 @@ func managerListRows() *sqlmock.Rows {
|
||||
"can_grant_vip",
|
||||
"can_update_user_level",
|
||||
"can_add_bd_leader",
|
||||
"can_add_bd_leader_in_region",
|
||||
"can_manage_cross_region",
|
||||
"can_add_admin",
|
||||
"can_add_superadmin",
|
||||
"can_block_user",
|
||||
|
||||
@ -77,7 +77,8 @@ type createManagerRequest struct {
|
||||
CanGrantVIP *bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||
CanAddBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||
CanManageCrossRegion *bool `json:"canManageCrossRegion"`
|
||||
LegacyBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser *bool `json:"canBlockUser"`
|
||||
@ -93,7 +94,8 @@ type updateManagerRequest struct {
|
||||
CanGrantVIP *bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||
CanAddBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||
CanManageCrossRegion *bool `json:"canManageCrossRegion"`
|
||||
LegacyBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser *bool `json:"canBlockUser"`
|
||||
@ -118,32 +120,32 @@ func (r updateManagerRequest) appendStatusAssignment(assignments *[]string, args
|
||||
}
|
||||
|
||||
type managerPermissions struct {
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanAddBDLeaderInRegion bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanManageCrossRegion bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
}
|
||||
|
||||
func (r createManagerRequest) permissionsWithDefault() managerPermissions {
|
||||
return managerPermissions{
|
||||
CanGrantAvatarFrame: boolDefaultTrue(r.CanGrantAvatarFrame),
|
||||
CanGrantVehicle: boolDefaultTrue(r.CanGrantVehicle),
|
||||
CanGrantBadge: boolDefaultTrue(r.CanGrantBadge),
|
||||
CanGrantVIP: boolDefaultTrue(r.CanGrantVIP),
|
||||
CanUpdateUserLevel: boolDefaultTrue(r.CanUpdateUserLevel),
|
||||
CanAddBDLeader: boolDefaultTrue(r.CanAddBDLeader),
|
||||
CanAddBDLeaderInRegion: boolDefaultFalse(r.CanAddBDLeaderInRegion),
|
||||
CanAddAdmin: boolDefaultTrue(r.CanAddAdmin),
|
||||
CanAddSuperadmin: boolDefaultTrue(r.CanAddSuperadmin),
|
||||
CanBlockUser: boolDefaultTrue(r.CanBlockUser),
|
||||
CanTransferCountry: boolDefaultTrue(r.CanTransferCountry),
|
||||
CanGrantAvatarFrame: boolDefaultTrue(r.CanGrantAvatarFrame),
|
||||
CanGrantVehicle: boolDefaultTrue(r.CanGrantVehicle),
|
||||
CanGrantBadge: boolDefaultTrue(r.CanGrantBadge),
|
||||
CanGrantVIP: boolDefaultTrue(r.CanGrantVIP),
|
||||
CanUpdateUserLevel: boolDefaultTrue(r.CanUpdateUserLevel),
|
||||
CanAddBDLeader: boolDefaultTrue(r.CanAddBDLeader),
|
||||
CanManageCrossRegion: boolDefaultFalse(firstBool(r.CanManageCrossRegion, r.LegacyBDLeaderInRegion)),
|
||||
CanAddAdmin: boolDefaultTrue(r.CanAddAdmin),
|
||||
CanAddSuperadmin: boolDefaultTrue(r.CanAddSuperadmin),
|
||||
CanBlockUser: boolDefaultTrue(r.CanBlockUser),
|
||||
CanTransferCountry: boolDefaultTrue(r.CanTransferCountry),
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,7 +156,7 @@ func (r updateManagerRequest) appendPermissionAssignments(assignments *[]string,
|
||||
appendBoolAssignment(assignments, args, "can_grant_vip", r.CanGrantVIP)
|
||||
appendBoolAssignment(assignments, args, "can_update_user_level", r.CanUpdateUserLevel)
|
||||
appendBoolAssignment(assignments, args, "can_add_bd_leader", r.CanAddBDLeader)
|
||||
appendBoolAssignment(assignments, args, "can_add_bd_leader_in_region", r.CanAddBDLeaderInRegion)
|
||||
appendBoolAssignment(assignments, args, "can_manage_cross_region", firstBool(r.CanManageCrossRegion, r.LegacyBDLeaderInRegion))
|
||||
appendBoolAssignment(assignments, args, "can_add_admin", r.CanAddAdmin)
|
||||
appendBoolAssignment(assignments, args, "can_add_superadmin", r.CanAddSuperadmin)
|
||||
appendBoolAssignment(assignments, args, "can_block_user", r.CanBlockUser)
|
||||
@ -177,6 +179,13 @@ func boolDefaultFalse(value *bool) bool {
|
||||
return value != nil && *value
|
||||
}
|
||||
|
||||
func firstBool(primary *bool, fallback *bool) *bool {
|
||||
if primary != nil {
|
||||
return primary
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
type createBDRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
TargetUserID flexibleInt64 `json:"targetUserId" binding:"required"`
|
||||
|
||||
@ -30,20 +30,28 @@ func TestFlexibleInt64AcceptsStringAndNumber(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerRegionExpansionDefaultsClosedAndPreservesExplicitEnable(t *testing.T) {
|
||||
func TestManagerCrossRegionDefaultsClosedAndAcceptsLegacyField(t *testing.T) {
|
||||
var omitted createManagerRequest
|
||||
if err := json.Unmarshal([]byte(`{"targetUserId":"1003"}`), &omitted); err != nil {
|
||||
t.Fatalf("unmarshal omitted permission: %v", err)
|
||||
}
|
||||
if omitted.permissionsWithDefault().CanAddBDLeaderInRegion {
|
||||
t.Fatalf("region expansion must default closed")
|
||||
if omitted.permissionsWithDefault().CanManageCrossRegion {
|
||||
t.Fatalf("cross-region manager must default closed")
|
||||
}
|
||||
|
||||
var enabled createManagerRequest
|
||||
if err := json.Unmarshal([]byte(`{"targetUserId":"1003","canAddBdLeaderInRegion":true}`), &enabled); err != nil {
|
||||
if err := json.Unmarshal([]byte(`{"targetUserId":"1003","canManageCrossRegion":true}`), &enabled); err != nil {
|
||||
t.Fatalf("unmarshal enabled permission: %v", err)
|
||||
}
|
||||
if !enabled.permissionsWithDefault().CanAddBDLeaderInRegion {
|
||||
t.Fatalf("explicit region expansion must be preserved")
|
||||
if !enabled.permissionsWithDefault().CanManageCrossRegion {
|
||||
t.Fatalf("explicit cross-region permission must be preserved")
|
||||
}
|
||||
|
||||
var legacy createManagerRequest
|
||||
if err := json.Unmarshal([]byte(`{"targetUserId":"1003","canAddBdLeaderInRegion":true}`), &legacy); err != nil {
|
||||
t.Fatalf("unmarshal legacy permission: %v", err)
|
||||
}
|
||||
if !legacy.permissionsWithDefault().CanManageCrossRegion {
|
||||
t.Fatalf("legacy region field must map to cross-region manager during rolling upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ const (
|
||||
sortByTotalRechargeUSDT = "total_recharge_usdt"
|
||||
bdLeaderPositionAliasMaxRunes = 64
|
||||
hostOrgInternalUserIDMinDigits = 15
|
||||
managerAddBDLeaderScopeScene = "manager_add_bd_leader"
|
||||
managerOperationScopeScene = "manager_operations"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@ -70,6 +70,7 @@ func (s *Service) ListManagers(ctx context.Context, query listQuery) ([]*Manager
|
||||
decorateManagerScopePolicy(items, policy)
|
||||
return items, total, ManagerRoleScopePolicy{
|
||||
BaseScope: policy.BaseScope,
|
||||
ExpandedScope: policy.ExpandedScope,
|
||||
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
||||
}, nil
|
||||
}
|
||||
@ -79,7 +80,8 @@ func (s *Service) CreateManager(ctx context.Context, actorID int64, requestID st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.CanAddBDLeaderInRegion = normalizedManagerRegionPermission(req.CanAddBDLeaderInRegion, policy.RegionExpansionConfigurable)
|
||||
req.CanManageCrossRegion = normalizedManagerCrossRegionPermission(firstBool(req.CanManageCrossRegion, req.LegacyBDLeaderInRegion), policy.RegionExpansionConfigurable)
|
||||
req.LegacyBDLeaderInRegion = nil
|
||||
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -98,7 +100,8 @@ func (s *Service) UpdateManager(ctx context.Context, actorID int64, userID int64
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.CanAddBDLeaderInRegion = normalizedManagerRegionPermission(req.CanAddBDLeaderInRegion, policy.RegionExpansionConfigurable)
|
||||
req.CanManageCrossRegion = normalizedManagerCrossRegionPermission(firstBool(req.CanManageCrossRegion, req.LegacyBDLeaderInRegion), policy.RegionExpansionConfigurable)
|
||||
req.LegacyBDLeaderInRegion = nil
|
||||
item, err := s.reader.UpdateManagerProfile(ctx, userID, actorID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -110,12 +113,12 @@ func (s *Service) UpdateManager(ctx context.Context, actorID int64, userID int64
|
||||
func (s *Service) managerRoleScopePolicy(ctx context.Context) (*userclient.RoleScopePolicy, error) {
|
||||
return s.userClient.GetRoleScopePolicy(ctx, userclient.GetRoleScopePolicyRequest{
|
||||
Caller: "hyapp-admin-server",
|
||||
Scene: managerAddBDLeaderScopeScene,
|
||||
Scene: managerOperationScopeScene,
|
||||
})
|
||||
}
|
||||
|
||||
// normalizedManagerRegionPermission 让 App 基础策略决定开关是否可配置;global App 即使收到篡改请求也只保存 false。
|
||||
func normalizedManagerRegionPermission(value *bool, configurable bool) *bool {
|
||||
// normalizedManagerCrossRegionPermission 让 App 基础策略决定开关是否可配置;global App 即使收到篡改请求也只保存 false。
|
||||
func normalizedManagerCrossRegionPermission(value *bool, configurable bool) *bool {
|
||||
if configurable {
|
||||
return value
|
||||
}
|
||||
@ -131,11 +134,12 @@ func decorateManagerScopePolicy(items []*ManagerListItem, policy *userclient.Rol
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
item.BDLeaderInviteBaseScope = policy.BaseScope
|
||||
item.BDLeaderRegionExpansionConfigurable = policy.RegionExpansionConfigurable
|
||||
item.BDLeaderInviteEffectiveScope = policy.BaseScope
|
||||
if policy.RegionExpansionConfigurable && item.CanAddBDLeaderInRegion {
|
||||
item.BDLeaderInviteEffectiveScope = "region"
|
||||
item.LegacyBDLeaderInRegion = item.CanManageCrossRegion
|
||||
item.ManagerOperationBaseScope = policy.BaseScope
|
||||
item.ManagerCrossRegionConfigurable = policy.RegionExpansionConfigurable
|
||||
item.ManagerOperationEffectiveScope = policy.BaseScope
|
||||
if policy.RegionExpansionConfigurable && item.CanManageCrossRegion {
|
||||
item.ManagerOperationEffectiveScope = policy.ExpandedScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,24 +130,26 @@ func TestManagerListItemJSONHasNoAgencyOrParentBD(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDecorateManagerScopePolicyUsesPolicyMetadataWithoutAppBranches(t *testing.T) {
|
||||
items := []*ManagerListItem{{CanAddBDLeaderInRegion: true}}
|
||||
items := []*ManagerListItem{{CanManageCrossRegion: true}}
|
||||
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
||||
BaseScope: "country",
|
||||
ExpandedScope: "region",
|
||||
RegionExpansionConfigurable: true,
|
||||
})
|
||||
if items[0].BDLeaderInviteEffectiveScope != "region" || !items[0].BDLeaderRegionExpansionConfigurable {
|
||||
if items[0].ManagerOperationEffectiveScope != "region" || !items[0].ManagerCrossRegionConfigurable || !items[0].LegacyBDLeaderInRegion {
|
||||
t.Fatalf("configurable policy decoration mismatch: %+v", items[0])
|
||||
}
|
||||
|
||||
items[0].CanAddBDLeaderInRegion = true
|
||||
items[0].CanManageCrossRegion = true
|
||||
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
||||
BaseScope: "global",
|
||||
ExpandedScope: "global",
|
||||
RegionExpansionConfigurable: false,
|
||||
})
|
||||
if items[0].BDLeaderInviteEffectiveScope != "global" || items[0].BDLeaderRegionExpansionConfigurable {
|
||||
if items[0].ManagerOperationEffectiveScope != "global" || items[0].ManagerCrossRegionConfigurable {
|
||||
t.Fatalf("global policy decoration mismatch: %+v", items[0])
|
||||
}
|
||||
if got := normalizedManagerRegionPermission(pointerBool(true), false); got == nil || *got {
|
||||
if got := normalizedManagerCrossRegionPermission(pointerBool(true), false); got == nil || *got {
|
||||
t.Fatalf("non-configurable app must persist region expansion as false")
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ func TestBusinessUserLookupForwardsSceneAndActor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchManagerUsersDefaultsSameCountryAndAllowsAllForCountryTransfer(t *testing.T) {
|
||||
func TestSearchManagerUsersDefaultsSameCountryForEveryAction(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
businessLookupResp: &userv1.BusinessUserLookupResponse{
|
||||
Users: []*userv1.BusinessUserLookupItem{
|
||||
@ -298,15 +298,15 @@ func TestSearchManagerUsersDefaultsSameCountryAndAllowsAllForCountryTransfer(t *
|
||||
if err := json.NewDecoder(allRecorder.Body).Decode(&allEnvelope); err != nil {
|
||||
t.Fatalf("decode all-country response failed: %v", err)
|
||||
}
|
||||
if allEnvelope.Data.Total != 2 || allEnvelope.Data.Items[1].UserID != "900002" {
|
||||
t.Fatalf("country transfer search must include cross-country users: %+v", allEnvelope.Data)
|
||||
if allEnvelope.Data.Total != 1 || allEnvelope.Data.Items[0].UserID != "900001" {
|
||||
t.Fatalf("scope=all must not bypass manager data scope: %+v", allEnvelope.Data)
|
||||
}
|
||||
if !hostClient.sawCapability("manager_transfer_user_country") {
|
||||
t.Fatalf("scope=all search must check transfer-country capability, saw %#v", hostClient.capabilities)
|
||||
if hostClient.sawCapability("manager_transfer_user_country") {
|
||||
t.Fatalf("HTTP scope must not become a second authorization path, saw %#v", hostClient.capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchManagerUsersUsesResolvedGlobalPolicyForHuwaaBDLeaderAction(t *testing.T) {
|
||||
func TestSearchManagerUsersUsesResolvedGlobalPolicyForAllHuwaaActions(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
||||
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||
@ -319,7 +319,7 @@ func TestSearchManagerUsersUsesResolvedGlobalPolicyForHuwaaBDLeaderAction(t *tes
|
||||
},
|
||||
}
|
||||
hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: "manager_add_bd_leader", BaseScope: "global", RegionExpansionConfigurable: false,
|
||||
Scene: "manager_operations", BaseScope: "global", ExpandedScope: "global", RegionExpansionConfigurable: false,
|
||||
}}}
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
||||
|
||||
@ -363,12 +363,12 @@ func TestSearchManagerUsersUsesResolvedGlobalPolicyForHuwaaBDLeaderAction(t *tes
|
||||
if err := json.NewDecoder(adminRecorder.Body).Decode(&adminEnvelope); err != nil {
|
||||
t.Fatalf("decode Huwaa admin search response failed: %v", err)
|
||||
}
|
||||
if adminEnvelope.Data.Total != 1 {
|
||||
t.Fatalf("Huwaa non-role manager actions must remain same-country: %+v", adminEnvelope.Data)
|
||||
if adminEnvelope.Data.Total != 2 {
|
||||
t.Fatalf("Huwaa manager actions must all use global scope: %+v", adminEnvelope.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchManagerUsersAppliesManagerRegionExpansionWithoutCrossingRegion(t *testing.T) {
|
||||
func TestSearchManagerUsersAppliesGeneralCrossRegionScopeToEveryAction(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
||||
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||
@ -384,38 +384,37 @@ func TestSearchManagerUsersAppliesManagerRegionExpansionWithoutCrossingRegion(t
|
||||
}
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
regionOpen bool
|
||||
crossRegion bool
|
||||
wantVisible int
|
||||
}{
|
||||
{name: "closed keeps country", regionOpen: false, wantVisible: 1},
|
||||
{name: "open expands within region", regionOpen: true, wantVisible: 2},
|
||||
{name: "closed keeps country", crossRegion: false, wantVisible: 1},
|
||||
{name: "open expands inside region", crossRegion: true, wantVisible: 2},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{
|
||||
"manager_add_bd_leader_in_region": testCase.regionOpen,
|
||||
"manager_cross_region": testCase.crossRegion,
|
||||
}}
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&scope=all&action=add-bd-leader", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-manager-region-search")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Total int `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Data.Total != testCase.wantVisible {
|
||||
t.Fatalf("visible total = %d, want %d", envelope.Data.Total, testCase.wantVisible)
|
||||
}
|
||||
if hostClient.sawCapability("manager_transfer_user_country") {
|
||||
t.Fatalf("scope=all must not override add-bd-leader policy")
|
||||
for _, action := range []string{"send-avatar-frame", "send-vehicle", "send-badge", "send-vip", "update-user-level", "add-bd-leader", "add-admin", "add-superadmin", "block-user", "transfer-user-country"} {
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&scope=all&action="+action, nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-manager-cross-region-search")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("%s status mismatch: got %d body=%s", action, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Total int `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||
t.Fatalf("decode %s response: %v", action, err)
|
||||
}
|
||||
if envelope.Data.Total != testCase.wantVisible {
|
||||
t.Fatalf("%s visible total = %d, want %d", action, envelope.Data.Total, testCase.wantVisible)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -423,8 +422,8 @@ func TestSearchManagerUsersAppliesManagerRegionExpansionWithoutCrossingRegion(t
|
||||
|
||||
func TestGrantManagerResourceUsesServerControlledGrantShape(t *testing.T) {
|
||||
walletClient := &fakeWalletClient{}
|
||||
hostClient := &fakeUserHostClient{}
|
||||
profileClient := &fakeUserProfileClient{}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
|
||||
profileClient := crossRegionManagerProfileClient()
|
||||
router := newManagerCenterTestRouter(walletClient, hostClient, profileClient)
|
||||
body := []byte(`{"command_id":"mgr-grant-1","target_user_id":"900001","resource_id":"101"}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/resource-grants", bytes.NewReader(body))
|
||||
@ -541,8 +540,11 @@ func TestGrantManagerResourceChecksCapabilityByWalletResourceType(t *testing.T)
|
||||
if walletClient.lastGetResource == nil || walletClient.lastGetResource.GetResourceId() != 202 {
|
||||
t.Fatalf("resource lookup mismatch: %+v", walletClient.lastGetResource)
|
||||
}
|
||||
if hostClient.lastCapability == nil || hostClient.lastCapability.GetCapability() != "manager_center" {
|
||||
t.Fatalf("last manager capability should be final same-country actor check, got %+v", hostClient.lastCapability)
|
||||
if hostClient.lastCapability == nil || hostClient.lastCapability.GetCapability() != "manager_cross_region" {
|
||||
t.Fatalf("last manager capability should resolve the actor's effective target scope, got %+v", hostClient.lastCapability)
|
||||
}
|
||||
if !hostClient.sawCapability("manager_center") {
|
||||
t.Fatalf("resource grant must still validate the active manager entry permission, saw %#v", hostClient.capabilities)
|
||||
}
|
||||
if !hostClient.sawCapability("manager_grant_vehicle") {
|
||||
t.Fatalf("vehicle grant must check manager_grant_vehicle, saw %#v", hostClient.capabilities)
|
||||
@ -640,7 +642,7 @@ func TestGrantManagerVIPRequiresManagerVIPCapability(t *testing.T) {
|
||||
|
||||
func TestGrantManagerVIPUsesManagerCenterGrantSource(t *testing.T) {
|
||||
walletClient := &fakeWalletClient{}
|
||||
router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{})
|
||||
router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}, crossRegionManagerProfileClient())
|
||||
body := []byte(`{"command_id":"mgr-vip-5","target_user_id":"900001","level":5}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
@ -707,8 +709,8 @@ func TestGrantManagerVIPUsesTrialCardForConfiguredApp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManagerBlockAndUnblockCallUserService(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{}
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, &fakeUserHostClient{}, profileClient)
|
||||
profileClient := crossRegionManagerProfileClient()
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}, profileClient)
|
||||
body := []byte(`{"command_id":"block-163006","target_user_id":"900001","blocked_until_ms":1780000000000}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/blocks", bytes.NewReader(body))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
@ -741,7 +743,7 @@ func TestManagerBlockAndUnblockCallUserService(t *testing.T) {
|
||||
|
||||
func TestSetManagerUserLevelCallsGrowthService(t *testing.T) {
|
||||
growthClient := &fakeGrowthLevelClient{}
|
||||
router := newManagerCenterTestRouterWithGrowth(&fakeWalletClient{}, &fakeUserHostClient{}, &fakeUserProfileClient{}, growthClient)
|
||||
router := newManagerCenterTestRouterWithGrowth(&fakeWalletClient{}, &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}, crossRegionManagerProfileClient(), growthClient)
|
||||
body := []byte(`{"command_id":"level-wealth-5","target_user_id":"900001","track":"wealth","level":5}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/levels", bytes.NewReader(body))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
@ -809,7 +811,7 @@ func TestCreateManagerBDLeaderRejectsCrossCountryTarget(t *testing.T) {
|
||||
42: {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
|
||||
}}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_add_bd_leader_in_region": false}}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": false}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||
body := []byte(`{"command_id":"mgr-bdleader-cross","target_user_id":"900001"}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader(body))
|
||||
@ -825,13 +827,13 @@ func TestCreateManagerBDLeaderRejectsCrossCountryTarget(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateManagerBDLeaderAllowsCrossCountryTargetWhenRegionExpansionEnabled(t *testing.T) {
|
||||
func TestCreateManagerBDLeaderAllowsSameRegionTargetForCrossRegionManager(t *testing.T) {
|
||||
hostAdminClient := &fakeUserHostAdminClient{}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
42: {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
|
||||
}}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_add_bd_leader_in_region": true}}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader([]byte(`{"command_id":"mgr-bdleader-region","target_user_id":"900001"}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
@ -848,6 +850,27 @@ func TestCreateManagerBDLeaderAllowsCrossCountryTargetWhenRegionExpansionEnabled
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateManagerBDLeaderRejectsDifferentRegionForCrossRegionManager(t *testing.T) {
|
||||
hostAdminClient := &fakeUserHostAdminClient{}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
42: {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 2002},
|
||||
}}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader([]byte(`{"command_id":"mgr-bdleader-other-region","target_user_id":"900001"}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-manager-bdleader-other-region")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodePermissionDenied, "req-manager-bdleader-other-region")
|
||||
if hostAdminClient.lastCreateBDLeader != nil {
|
||||
t.Fatalf("different-region target must not reach CreateBDLeader: %+v", hostAdminClient.lastCreateBDLeader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateManagerBDLeaderAllowsCrossCountryTargetForHuwaa(t *testing.T) {
|
||||
hostAdminClient := &fakeUserHostAdminClient{}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
@ -855,7 +878,7 @@ func TestCreateManagerBDLeaderAllowsCrossCountryTargetForHuwaa(t *testing.T) {
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, DisplayUserId: "163006", Country: "CA", RegionId: 2002},
|
||||
}}
|
||||
hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: "manager_add_bd_leader", BaseScope: "global", RegionExpansionConfigurable: false,
|
||||
Scene: "manager_operations", BaseScope: "global", ExpandedScope: "global", RegionExpansionConfigurable: false,
|
||||
}}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||
body := []byte(`{"command_id":"huwaa-mgr-bdleader-cross","target_user_id":"900001"}`)
|
||||
@ -874,11 +897,12 @@ func TestCreateManagerBDLeaderAllowsCrossCountryTargetForHuwaa(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferManagerUserCountryUsesAdminCountryChangeWithoutSameCountryFilter(t *testing.T) {
|
||||
func TestTransferManagerUserCountryUsesCrossRegionManagerScope(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
42: {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
|
||||
42: {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "MX", RegionId: 1001},
|
||||
}}
|
||||
hostClient := &fakeUserHostClient{}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
||||
body := []byte(`{"command_id":"mgr-country-1","target_user_id":"900001","country":"CA"}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/users/country", bytes.NewReader(body))
|
||||
@ -900,6 +924,13 @@ func TestTransferManagerUserCountryUsesAdminCountryChangeWithoutSameCountryFilte
|
||||
}
|
||||
}
|
||||
|
||||
func crossRegionManagerProfileClient() *fakeUserProfileClient {
|
||||
return &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
42: {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "MX", RegionId: 1001},
|
||||
}}
|
||||
}
|
||||
|
||||
// TestGetManagerOverviewIncludesTeamSalaryDashboard 验证经理概览聚合团队工资卡片:
|
||||
// user-service 组织树 Agency owner 去重后传给 wallet-service,按本月/上月两个 UTC 周期取回预收入并计算环比。
|
||||
func TestGetManagerOverviewIncludesTeamSalaryDashboard(t *testing.T) {
|
||||
|
||||
@ -24,7 +24,7 @@ const managerGrantBadgeCapability = "manager_grant_badge"
|
||||
const managerGrantVIPCapability = "manager_grant_vip"
|
||||
const managerUpdateUserLevelCapability = "manager_update_user_level"
|
||||
const managerAddBDLeaderCapability = "manager_add_bd_leader"
|
||||
const managerAddBDLeaderInRegionCapability = "manager_add_bd_leader_in_region"
|
||||
const managerCrossRegionCapability = "manager_cross_region"
|
||||
const managerAddAdminCapability = "manager_add_admin"
|
||||
const managerAddSuperadminCapability = "manager_add_superadmin"
|
||||
const managerBlockUserCapability = "manager_block_user"
|
||||
@ -38,6 +38,7 @@ const managerGrantMaxVIPLevel int32 = 5
|
||||
const managerRoleScopeCountry = "country"
|
||||
const managerRoleScopeRegion = "region"
|
||||
const managerRoleScopeGlobal = "global"
|
||||
const managerOperationScopeScene = "manager_operations"
|
||||
|
||||
type managerGrantResourceData struct {
|
||||
ResourceID string `json:"resource_id"`
|
||||
@ -171,25 +172,16 @@ func (h *Handler) searchManagerUsers(writer http.ResponseWriter, request *http.R
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
// 搜索先走已有 business lookup 保持短号/昵称解析一致,再用 BatchGetUsers 拿国家和等级投影做最终过滤。
|
||||
// BD Leader 的 App 差异来自 user-service 策略,handler 不直接判断 app_code。
|
||||
// 搜索先走已有 business lookup 保持短号/昵称解析一致,再用 BatchGetUsers 拿国家和区域投影做最终过滤。
|
||||
// 所有经理动作的 App 差异都来自 user-service 策略,handler 不直接判断 app_code。
|
||||
actor, ok := h.requireManagerCenterActor(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
scope := strings.ToLower(strings.TrimSpace(request.URL.Query().Get("scope")))
|
||||
bdLeaderSearch := managerSearchScene(request) == managerAddBDLeaderCapability
|
||||
requestedAllCountries := scope == "all" && !bdLeaderSearch
|
||||
if requestedAllCountries && !h.requireManagerCapability(writer, request, managerTransferUserCountryCapability) {
|
||||
managerTargetScope, ok := h.managerEffectiveScope(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
managerTargetScope := managerRoleScopeCountry
|
||||
if bdLeaderSearch {
|
||||
managerTargetScope, ok = h.managerBDLeaderEffectiveScope(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
@ -235,8 +227,7 @@ func (h *Handler) searchManagerUsers(writer http.ResponseWriter, request *http.R
|
||||
for _, userID := range userIDs {
|
||||
user := usersResp.GetUsers()[userID]
|
||||
// 搜索与最终创建共用 scope evaluator,避免同一 App 的规则在两个 handler 分叉。
|
||||
if user == nil || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE ||
|
||||
(!requestedAllCountries && !managerTargetMatchesScope(managerTargetScope, actor, user)) {
|
||||
if user == nil || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE || !managerTargetMatchesScope(managerTargetScope, actor, user) {
|
||||
continue
|
||||
}
|
||||
item := managerUserFromProto(user, levels[userID])
|
||||
@ -391,8 +382,8 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
||||
}
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
// 赠送资源前先确认目标用户仍然 active 且与经理同国家;wallet-service 还会在事务内复验资源白名单。
|
||||
if _, ok := h.requireSameCountryTarget(writer, request, targetUserID); !ok {
|
||||
// 所有经理目标用户动作共用同一有效范围;wallet-service 还会在事务内复验资源白名单。
|
||||
if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(body.Reason)
|
||||
@ -477,7 +468,7 @@ func (h *Handler) grantManagerVIP(writer http.ResponseWriter, request *http.Requ
|
||||
}
|
||||
// 经理中心先读取 App 级 program,再选择直接会员或体验卡专用命令。钱包会在写事务内再次校验,
|
||||
// 因此配置在两次 RPC 之间切换时只会明确失败,不会绕过当前发放语义。
|
||||
if _, ok := h.requireSameCountryTarget(writer, request, targetUserID); !ok {
|
||||
if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(body.Reason)
|
||||
@ -647,8 +638,8 @@ func (h *Handler) createManagerBlock(writer http.ResponseWriter, request *http.R
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
// user-service 会再次读取双方用户并校验同国家,这里保留 gateway 早拒绝以减少无效封禁事务。
|
||||
if _, ok := h.requireSameCountryTargetWithActor(writer, request, actor, targetUserID); !ok {
|
||||
// user-service 会按同一 App 策略复验,gateway 先拒绝越界目标以减少无效封禁事务。
|
||||
if _, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID); !ok {
|
||||
return
|
||||
}
|
||||
resp, err := h.userProfileClient.CreateManagerUserBlock(request.Context(), &userv1.CreateManagerUserBlockRequest{
|
||||
@ -732,7 +723,7 @@ func (h *Handler) setManagerUserLevel(writer http.ResponseWriter, request *http.
|
||||
return
|
||||
}
|
||||
// 等级设置会把目标轨道 total_value 直接改到对应规则阈值,并由 activity-service 补齐 1..目标等级奖励。
|
||||
if _, ok := h.requireSameCountryTarget(writer, request, targetUserID); !ok {
|
||||
if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
|
||||
return
|
||||
}
|
||||
resp, err := h.growthLevelClient.SetUserLevel(request.Context(), &activityv1.SetUserLevelRequest{
|
||||
@ -783,7 +774,7 @@ func (h *Handler) createManagerBDLeader(writer http.ResponseWriter, request *htt
|
||||
return
|
||||
}
|
||||
// 写入口重新解析策略并读取 active 目标,不能信任搜索结果或客户端自报范围。
|
||||
target, ok := h.requireManagerBDLeaderTarget(writer, request, actor, targetUserID)
|
||||
target, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -847,7 +838,10 @@ func (h *Handler) transferManagerUserCountry(writer http.ResponseWriter, request
|
||||
if reason == "" {
|
||||
reason = "manager_center_country_transfer"
|
||||
}
|
||||
// 国家转移是治理动作,不复用同国家目标校验;权限由 manager_profiles 独立开关控制,实际国家/区域事务由 user-service 统一落库和发 outbox。
|
||||
// 国家转移也属于经理数据范围;拥有功能权限不代表可以越过 country/region/global 有效范围选择任意目标。
|
||||
if _, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID); !ok {
|
||||
return
|
||||
}
|
||||
resp, err := h.userProfileClient.AdminChangeUserCountry(request.Context(), &userv1.AdminChangeUserCountryRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
TargetUserId: targetUserID,
|
||||
@ -950,10 +944,10 @@ func (h *Handler) requireManagerCapability(writer http.ResponseWriter, request *
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Handler) managerBDLeaderEffectiveScope(writer http.ResponseWriter, request *http.Request) (string, bool) {
|
||||
func (h *Handler) managerEffectiveScope(writer http.ResponseWriter, request *http.Request) (string, bool) {
|
||||
resp, err := h.userHostClient.GetRoleScopePolicy(request.Context(), &userv1.GetRoleScopePolicyRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
Scene: managerAddBDLeaderCapability,
|
||||
Scene: managerOperationScopeScene,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
@ -968,43 +962,40 @@ func (h *Handler) managerBDLeaderEffectiveScope(writer http.ResponseWriter, requ
|
||||
if !policy.GetRegionExpansionConfigurable() {
|
||||
return scope, true
|
||||
}
|
||||
expandedScope := policy.GetExpandedScope()
|
||||
if expandedScope == "" {
|
||||
// 兼容上一版 user-service;旧协议只支持扩到同区域,滚动升级期间保持旧边界而不是放大到 global。
|
||||
expandedScope = managerRoleScopeRegion
|
||||
}
|
||||
if !validManagerRoleScope(expandedScope) {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return "", false
|
||||
}
|
||||
capability, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||
Capability: managerAddBDLeaderInRegionCapability,
|
||||
Capability: managerCrossRegionCapability,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return "", false
|
||||
}
|
||||
if capability.GetAllowed() {
|
||||
scope = managerRoleScopeRegion
|
||||
scope = expandedScope
|
||||
}
|
||||
return scope, true
|
||||
}
|
||||
|
||||
func (h *Handler) requireSameCountryTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
|
||||
func (h *Handler) requireManagerTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
|
||||
actor, ok := h.requireManagerCenterActor(writer, request)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return h.requireSameCountryTargetWithActor(writer, request, actor, targetUserID)
|
||||
return h.requireManagerTargetWithActor(writer, request, actor, targetUserID)
|
||||
}
|
||||
|
||||
func (h *Handler) requireSameCountryTargetWithActor(writer http.ResponseWriter, request *http.Request, actor *userv1.User, targetUserID int64) (*userv1.User, bool) {
|
||||
target, ok := h.requireActiveTarget(writer, request, targetUserID)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if !sameCountry(actor.GetCountry(), target.GetCountry()) {
|
||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||
return nil, false
|
||||
}
|
||||
return target, true
|
||||
}
|
||||
|
||||
func (h *Handler) requireManagerBDLeaderTarget(writer http.ResponseWriter, request *http.Request, actor *userv1.User, targetUserID int64) (*userv1.User, bool) {
|
||||
scope, ok := h.managerBDLeaderEffectiveScope(writer, request)
|
||||
func (h *Handler) requireManagerTargetWithActor(writer http.ResponseWriter, request *http.Request, actor *userv1.User, targetUserID int64) (*userv1.User, bool) {
|
||||
scope, ok := h.managerEffectiveScope(writer, request)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
@ -1019,7 +1010,7 @@ func (h *Handler) requireManagerBDLeaderTarget(writer http.ResponseWriter, reque
|
||||
return target, true
|
||||
}
|
||||
|
||||
// requireActiveTarget 是经理写入口共同的目标用户真实性门禁;区域策略只能决定后续是否比较国家,不能绕过 active 用户校验。
|
||||
// requireActiveTarget 是经理写入口共同的目标用户真实性门禁;范围策略只能决定后续国家/区域匹配,不能绕过 active 用户校验。
|
||||
func (h *Handler) requireActiveTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
|
||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
@ -1066,30 +1057,30 @@ func managerTargetMatchesScope(scope string, actor *userv1.User, target *userv1.
|
||||
|
||||
func (h *Handler) managerPermissionOverview(request *http.Request) map[string]bool {
|
||||
permissions := map[string]bool{
|
||||
"can_grant_avatar_frame": false,
|
||||
"can_grant_vehicle": false,
|
||||
"can_grant_badge": false,
|
||||
"can_grant_vip": false,
|
||||
"can_update_user_level": false,
|
||||
"can_add_bd_leader": false,
|
||||
"can_add_bd_leader_in_region": false,
|
||||
"can_add_admin": false,
|
||||
"can_add_superadmin": false,
|
||||
"can_block_user": false,
|
||||
"can_transfer_user_country": false,
|
||||
"can_grant_avatar_frame": false,
|
||||
"can_grant_vehicle": false,
|
||||
"can_grant_badge": false,
|
||||
"can_grant_vip": false,
|
||||
"can_update_user_level": false,
|
||||
"can_add_bd_leader": false,
|
||||
"can_manage_cross_region": false,
|
||||
"can_add_admin": false,
|
||||
"can_add_superadmin": false,
|
||||
"can_block_user": false,
|
||||
"can_transfer_user_country": false,
|
||||
}
|
||||
for key, capability := range map[string]string{
|
||||
"can_grant_avatar_frame": managerGrantAvatarFrameCapability,
|
||||
"can_grant_vehicle": managerGrantVehicleCapability,
|
||||
"can_grant_badge": managerGrantBadgeCapability,
|
||||
"can_grant_vip": managerGrantVIPCapability,
|
||||
"can_update_user_level": managerUpdateUserLevelCapability,
|
||||
"can_add_bd_leader": managerAddBDLeaderCapability,
|
||||
"can_add_bd_leader_in_region": managerAddBDLeaderInRegionCapability,
|
||||
"can_add_admin": managerAddAdminCapability,
|
||||
"can_add_superadmin": managerAddSuperadminCapability,
|
||||
"can_block_user": managerBlockUserCapability,
|
||||
"can_transfer_user_country": managerTransferUserCountryCapability,
|
||||
"can_grant_avatar_frame": managerGrantAvatarFrameCapability,
|
||||
"can_grant_vehicle": managerGrantVehicleCapability,
|
||||
"can_grant_badge": managerGrantBadgeCapability,
|
||||
"can_grant_vip": managerGrantVIPCapability,
|
||||
"can_update_user_level": managerUpdateUserLevelCapability,
|
||||
"can_add_bd_leader": managerAddBDLeaderCapability,
|
||||
"can_manage_cross_region": managerCrossRegionCapability,
|
||||
"can_add_admin": managerAddAdminCapability,
|
||||
"can_add_superadmin": managerAddSuperadminCapability,
|
||||
"can_block_user": managerBlockUserCapability,
|
||||
"can_transfer_user_country": managerTransferUserCountryCapability,
|
||||
} {
|
||||
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
|
||||
@ -1979,6 +1979,9 @@ func (f *fakeUserHostClient) CheckBusinessCapability(_ context.Context, req *use
|
||||
if allowed, configured := f.capabilityAllowed[req.GetCapability()]; configured {
|
||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: allowed}, nil
|
||||
}
|
||||
if req.GetCapability() == "manager_cross_region" {
|
||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: false}, nil
|
||||
}
|
||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: true}, nil
|
||||
}
|
||||
|
||||
@ -1991,8 +1994,9 @@ func (f *fakeUserHostClient) GetRoleScopePolicy(_ context.Context, req *userv1.G
|
||||
return f.roleScopePolicyResp, nil
|
||||
}
|
||||
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: "manager_add_bd_leader",
|
||||
Scene: "manager_operations",
|
||||
BaseScope: "country",
|
||||
ExpandedScope: "region",
|
||||
RegionExpansionConfigurable: true,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
@ -887,6 +887,7 @@ CREATE TABLE IF NOT EXISTS manager_profiles (
|
||||
can_update_user_level TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心升级用户等级',
|
||||
can_add_bd_leader TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 BD Leader',
|
||||
can_add_bd_leader_in_region TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许把 BD Leader 邀请范围从本国家扩展到经理所属区域',
|
||||
can_manage_cross_region TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许经理中心所有目标用户操作跨区域执行',
|
||||
can_add_admin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Admin',
|
||||
can_add_superadmin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Superadmin',
|
||||
can_block_user TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心封禁用户',
|
||||
@ -1341,6 +1342,8 @@ CREATE TABLE IF NOT EXISTS login_audit (
|
||||
provider VARCHAR(64) NULL COMMENT '提供方',
|
||||
channel VARCHAR(32) NOT NULL DEFAULT '' COMMENT '渠道',
|
||||
platform VARCHAR(16) NOT NULL DEFAULT '' COMMENT '平台',
|
||||
app_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '本次认证客户端版本',
|
||||
build_number VARCHAR(64) NOT NULL DEFAULT '' COMMENT '本次认证客户端构建号',
|
||||
result VARCHAR(32) NOT NULL COMMENT '结果',
|
||||
failure_code VARCHAR(64) NULL COMMENT '失败编码',
|
||||
client_ip VARCHAR(64) NULL COMMENT '客户端IP',
|
||||
@ -1353,6 +1356,7 @@ CREATE TABLE IF NOT EXISTS login_audit (
|
||||
KEY idx_login_audit_request_id (app_code, request_id),
|
||||
KEY idx_login_audit_created_at (app_code, created_at_ms),
|
||||
KEY idx_login_audit_user_created (app_code, user_id, created_at_ms),
|
||||
KEY idx_login_audit_latest_success (app_code, user_id, result, blocked, login_type, created_at_ms, id),
|
||||
KEY idx_login_audit_blocked (app_code, blocked, created_at_ms),
|
||||
KEY idx_login_audit_ip_country (app_code, ip_country_code, created_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录审计表';
|
||||
|
||||
@ -231,24 +231,24 @@ type BDProfile struct {
|
||||
// ManagerProfile 是经理身份事实。
|
||||
// 经理是 BD Leader/Admin 的上级,和 Host/Agency/BD 链路没有父子关系。
|
||||
type ManagerProfile struct {
|
||||
UserID int64
|
||||
RegionID int64
|
||||
Status string
|
||||
Contact string
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanAddBDLeaderInRegion bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
CreatedByAdminID int64
|
||||
CreatedAtMs int64
|
||||
UpdatedAtMs int64
|
||||
UserID int64
|
||||
RegionID int64
|
||||
Status string
|
||||
Contact string
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanManageCrossRegion bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
CreatedByAdminID int64
|
||||
CreatedAtMs int64
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
// CoinSellerProfile 是币商角色事实,余额不保存在 user-service。
|
||||
|
||||
@ -8,14 +8,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CapabilityManagerCenter = "manager_center"
|
||||
CapabilityManagerResourceGrant = "manager_resource_grant"
|
||||
CapabilityManagerGrantAvatarFrame = "manager_grant_avatar_frame"
|
||||
CapabilityManagerGrantVehicle = "manager_grant_vehicle"
|
||||
CapabilityManagerGrantBadge = "manager_grant_badge"
|
||||
CapabilityManagerGrantVIP = "manager_grant_vip"
|
||||
CapabilityManagerUpdateUserLevel = "manager_update_user_level"
|
||||
CapabilityManagerAddBDLeader = "manager_add_bd_leader"
|
||||
CapabilityManagerCenter = "manager_center"
|
||||
CapabilityManagerResourceGrant = "manager_resource_grant"
|
||||
CapabilityManagerGrantAvatarFrame = "manager_grant_avatar_frame"
|
||||
CapabilityManagerGrantVehicle = "manager_grant_vehicle"
|
||||
CapabilityManagerGrantBadge = "manager_grant_badge"
|
||||
CapabilityManagerGrantVIP = "manager_grant_vip"
|
||||
CapabilityManagerUpdateUserLevel = "manager_update_user_level"
|
||||
CapabilityManagerAddBDLeader = "manager_add_bd_leader"
|
||||
CapabilityManagerCrossRegion = "manager_cross_region"
|
||||
// CapabilityManagerAddBDLeaderInRegion 兼容上一版 gateway,底层读取同一个通用跨区经理开关。
|
||||
CapabilityManagerAddBDLeaderInRegion = "manager_add_bd_leader_in_region"
|
||||
CapabilityManagerAddAdmin = "manager_add_admin"
|
||||
CapabilityManagerAddSuperadmin = "manager_add_superadmin"
|
||||
@ -50,6 +52,7 @@ func (s *Service) CheckBusinessCapability(ctx context.Context, actorUserID int64
|
||||
CapabilityManagerGrantVIP,
|
||||
CapabilityManagerUpdateUserLevel,
|
||||
CapabilityManagerAddBDLeader,
|
||||
CapabilityManagerCrossRegion,
|
||||
CapabilityManagerAddBDLeaderInRegion,
|
||||
CapabilityManagerAddAdmin,
|
||||
CapabilityManagerAddSuperadmin,
|
||||
|
||||
@ -15,24 +15,29 @@ const (
|
||||
RoleScopeGlobal RoleScope = "global"
|
||||
|
||||
RoleScopeSceneOrganizationExpansion = "organization_role_expansion"
|
||||
RoleScopeSceneManagerAddBDLeader = "manager_add_bd_leader"
|
||||
RoleScopeSceneManagerOperations = "manager_operations"
|
||||
// RoleScopeSceneManagerAddBDLeader 兼容上一版 gateway/admin 的策略查询,解析后统一返回 manager_operations。
|
||||
RoleScopeSceneManagerAddBDLeader = "manager_add_bd_leader"
|
||||
)
|
||||
|
||||
// RoleScopePolicy 把 App 固有边界和可运营扩展能力分开;业务入口只根据解析结果判断目标范围。
|
||||
type RoleScopePolicy struct {
|
||||
Scene string
|
||||
BaseScope RoleScope
|
||||
ExpandedScope RoleScope
|
||||
RegionExpansionConfigurable bool
|
||||
}
|
||||
|
||||
var defaultRoleScopePolicies = map[string]RoleScopePolicy{
|
||||
RoleScopeSceneOrganizationExpansion: {
|
||||
Scene: RoleScopeSceneOrganizationExpansion,
|
||||
BaseScope: RoleScopeRegion,
|
||||
Scene: RoleScopeSceneOrganizationExpansion,
|
||||
BaseScope: RoleScopeRegion,
|
||||
ExpandedScope: RoleScopeRegion,
|
||||
},
|
||||
RoleScopeSceneManagerAddBDLeader: {
|
||||
Scene: RoleScopeSceneManagerAddBDLeader,
|
||||
RoleScopeSceneManagerOperations: {
|
||||
Scene: RoleScopeSceneManagerOperations,
|
||||
BaseScope: RoleScopeCountry,
|
||||
ExpandedScope: RoleScopeRegion,
|
||||
RegionExpansionConfigurable: true,
|
||||
},
|
||||
}
|
||||
@ -40,12 +45,14 @@ var defaultRoleScopePolicies = map[string]RoleScopePolicy{
|
||||
var appRoleScopePolicyOverrides = map[string]map[string]RoleScopePolicy{
|
||||
"huwaa": {
|
||||
RoleScopeSceneOrganizationExpansion: {
|
||||
Scene: RoleScopeSceneOrganizationExpansion,
|
||||
BaseScope: RoleScopeGlobal,
|
||||
Scene: RoleScopeSceneOrganizationExpansion,
|
||||
BaseScope: RoleScopeGlobal,
|
||||
ExpandedScope: RoleScopeGlobal,
|
||||
},
|
||||
RoleScopeSceneManagerAddBDLeader: {
|
||||
Scene: RoleScopeSceneManagerAddBDLeader,
|
||||
BaseScope: RoleScopeGlobal,
|
||||
RoleScopeSceneManagerOperations: {
|
||||
Scene: RoleScopeSceneManagerOperations,
|
||||
BaseScope: RoleScopeGlobal,
|
||||
ExpandedScope: RoleScopeGlobal,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -53,6 +60,9 @@ var appRoleScopePolicyOverrides = map[string]map[string]RoleScopePolicy{
|
||||
// ResolveRoleScopePolicy 是 App 差异的唯一解析入口;新增 App 只扩展注册表,不修改搜索或写入 handler。
|
||||
func ResolveRoleScopePolicy(rawAppCode string, rawScene string) (RoleScopePolicy, error) {
|
||||
scene := strings.ToLower(strings.TrimSpace(rawScene))
|
||||
if scene == RoleScopeSceneManagerAddBDLeader {
|
||||
scene = RoleScopeSceneManagerOperations
|
||||
}
|
||||
policy, ok := defaultRoleScopePolicies[scene]
|
||||
if !ok {
|
||||
return RoleScopePolicy{}, xerr.New(xerr.InvalidArgument, "role scope scene is invalid")
|
||||
@ -68,7 +78,7 @@ func ResolveRoleScopePolicy(rawAppCode string, rawScene string) (RoleScopePolicy
|
||||
// EffectiveScope 只允许声明为 configurable 的策略接受经理个人扩区开关。
|
||||
func (p RoleScopePolicy) EffectiveScope(regionExpansionEnabled bool) RoleScope {
|
||||
if p.RegionExpansionConfigurable && regionExpansionEnabled {
|
||||
return RoleScopeRegion
|
||||
return p.ExpandedScope
|
||||
}
|
||||
return p.BaseScope
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ func TestResolveRoleScopePolicyKeepsAppDifferencesInRegistry(t *testing.T) {
|
||||
}
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
policy, err := ResolveRoleScopePolicy(testCase.appCode, RoleScopeSceneManagerAddBDLeader)
|
||||
policy, err := ResolveRoleScopePolicy(testCase.appCode, RoleScopeSceneManagerOperations)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve policy: %v", err)
|
||||
}
|
||||
@ -28,3 +28,13 @@ func TestResolveRoleScopePolicyKeepsAppDifferencesInRegistry(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRoleScopePolicyKeepsLegacyManagerSceneCompatible(t *testing.T) {
|
||||
policy, err := ResolveRoleScopePolicy("lalu", RoleScopeSceneManagerAddBDLeader)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve legacy manager scene: %v", err)
|
||||
}
|
||||
if policy.Scene != RoleScopeSceneManagerOperations || policy.ExpandedScope != RoleScopeRegion {
|
||||
t.Fatalf("legacy scene must resolve to general manager policy: %+v", policy)
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,12 +135,12 @@ func TestCheckBusinessCapabilityRequiresActiveManagerProfile(t *testing.T) {
|
||||
t.Fatalf("active manager should be allowed: allowed=%v reason=%q", allowed, reason)
|
||||
}
|
||||
|
||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerAddBDLeaderInRegion)
|
||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerCrossRegion)
|
||||
if err != nil || allowed || reason != "manager_capability_required" {
|
||||
t.Fatalf("region expansion must default closed: allowed=%v reason=%q err=%v", allowed, reason, err)
|
||||
}
|
||||
repository.SetManagerAddBDLeaderInRegion(101, true)
|
||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerAddBDLeaderInRegion)
|
||||
repository.SetManagerCrossRegion(101, true)
|
||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerCrossRegion)
|
||||
if err != nil || !allowed || reason != "" {
|
||||
t.Fatalf("enabled region expansion should be allowed: allowed=%v reason=%q err=%v", allowed, reason, err)
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/xerr"
|
||||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||||
hostservice "hyapp/services/user-service/internal/service/host"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -160,7 +161,7 @@ func (s *Service) statusResultAfterPersistence(ctx context.Context, command User
|
||||
}
|
||||
|
||||
// CreateManagerUserBlock 创建经理封禁记录并把目标用户状态置为 banned。
|
||||
// 国家和 active 状态在 service 层校验,保证任何 gateway/H5 入口都不能跨国家或重复封禁非 active 用户。
|
||||
// 目标范围由 App 策略与经理通用跨区开关共同决定,user-service 必须复验,不能只依赖 gateway 早拒绝。
|
||||
func (s *Service) CreateManagerUserBlock(ctx context.Context, command ManagerUserBlockCommand) (ManagerUserBlock, UserStatusResult, error) {
|
||||
if s.userRepository == nil || s.moderationRepository == nil {
|
||||
return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
||||
@ -187,8 +188,20 @@ func (s *Service) CreateManagerUserBlock(ctx context.Context, command ManagerUse
|
||||
if err != nil {
|
||||
return ManagerUserBlock{}, UserStatusResult{}, err
|
||||
}
|
||||
if manager.Country == "" || target.Country == "" || !strings.EqualFold(manager.Country, target.Country) {
|
||||
return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.PermissionDenied, "target user is outside manager country")
|
||||
policy, err := hostservice.ResolveRoleScopePolicy(appcode.FromContext(ctx), hostservice.RoleScopeSceneManagerOperations)
|
||||
if err != nil {
|
||||
return ManagerUserBlock{}, UserStatusResult{}, err
|
||||
}
|
||||
effectiveScope := policy.BaseScope
|
||||
if policy.RegionExpansionConfigurable {
|
||||
crossRegionEnabled, err := s.moderationRepository.ManagerCrossRegionEnabled(ctx, command.ManagerUserID)
|
||||
if err != nil {
|
||||
return ManagerUserBlock{}, UserStatusResult{}, err
|
||||
}
|
||||
effectiveScope = policy.EffectiveScope(crossRegionEnabled)
|
||||
}
|
||||
if !managerModerationTargetAllowed(effectiveScope, manager, target) {
|
||||
return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.PermissionDenied, "target user is outside manager scope")
|
||||
}
|
||||
if target.Status != userdomain.StatusActive {
|
||||
return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "target user is not active")
|
||||
@ -213,6 +226,19 @@ func (s *Service) CreateManagerUserBlock(ctx context.Context, command ManagerUse
|
||||
return block, status, nil
|
||||
}
|
||||
|
||||
func managerModerationTargetAllowed(scope hostservice.RoleScope, manager userdomain.User, target userdomain.User) bool {
|
||||
switch scope {
|
||||
case hostservice.RoleScopeGlobal:
|
||||
return true
|
||||
case hostservice.RoleScopeRegion:
|
||||
return manager.RegionID > 0 && manager.RegionID == target.RegionID
|
||||
case hostservice.RoleScopeCountry:
|
||||
return manager.Country != "" && target.Country != "" && strings.EqualFold(manager.Country, target.Country)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]ManagerUserBlock, error) {
|
||||
if s.moderationRepository == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "moderation repository is not configured")
|
||||
|
||||
@ -103,6 +103,43 @@ func TestSetUserStatusSurfacesAccessDenylistError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateManagerUserBlockUsesGeneralCrossRegionScope(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
appCode string
|
||||
crossRegion bool
|
||||
targetRegion int64
|
||||
wantAllowed bool
|
||||
}{
|
||||
{name: "default manager denied", appCode: "lalu", targetRegion: 1, wantAllowed: false},
|
||||
{name: "cross-region manager allowed inside region", appCode: "lalu", crossRegion: true, targetRegion: 1, wantAllowed: true},
|
||||
{name: "cross-region manager denied outside region", appCode: "lalu", crossRegion: true, targetRegion: 2, wantAllowed: false},
|
||||
{name: "huwaa globally allowed", appCode: "huwaa", targetRegion: 2, wantAllowed: true},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
repository := &fakeModerationRepository{
|
||||
managerCrossRegionEnabled: testCase.crossRegion,
|
||||
users: map[int64]userdomain.User{
|
||||
100: {UserID: 100, Country: "US", RegionID: 1, Status: userdomain.StatusActive},
|
||||
200: {UserID: 200, Country: "CA", RegionID: testCase.targetRegion, Status: userdomain.StatusActive},
|
||||
},
|
||||
}
|
||||
svc := New(repository, WithModerationRepository(repository), WithClock(func() time.Time {
|
||||
return time.UnixMilli(1_000).UTC()
|
||||
}))
|
||||
_, _, err := svc.CreateManagerUserBlock(appcode.WithContext(context.Background(), testCase.appCode), ManagerUserBlockCommand{
|
||||
CommandID: "manager-block", ManagerUserID: 100, TargetUserID: 200, BlockedUntilMS: 2_000,
|
||||
})
|
||||
if testCase.wantAllowed && err != nil {
|
||||
t.Fatalf("cross-region block should be allowed: %v", err)
|
||||
}
|
||||
if !testCase.wantAllowed && !xerr.IsCode(err, xerr.PermissionDenied) {
|
||||
t.Fatalf("out-of-scope block should be denied: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminBanUserAcceptsPermanentBanAndRunsRealtimeSideEffectsOnce(t *testing.T) {
|
||||
repository := &fakeModerationRepository{
|
||||
user: userdomain.User{AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned},
|
||||
@ -232,6 +269,7 @@ func TestBatchGetUserAdminProfilesDeduplicatesAndLimitsInput(t *testing.T) {
|
||||
|
||||
type fakeModerationRepository struct {
|
||||
user userdomain.User
|
||||
users map[int64]userdomain.User
|
||||
inviteReferrer userdomain.User
|
||||
sessionIDs []string
|
||||
countrySessionIDs []string
|
||||
@ -250,6 +288,7 @@ type fakeModerationRepository struct {
|
||||
adminBanPersistence AdminUserBanPersistenceResult
|
||||
adminBanRelease AdminUserBanRelease
|
||||
adminBanExpirations []AdminUserBanRelease
|
||||
managerCrossRegionEnabled bool
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) SetUserStatus(_ context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error) {
|
||||
@ -258,6 +297,10 @@ func (r *fakeModerationRepository) SetUserStatus(_ context.Context, command User
|
||||
return UserStatusPersistenceResult{User: r.user, RevokedSessionIDs: append([]string{}, r.sessionIDs...)}, nil
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) ManagerCrossRegionEnabled(context.Context, int64) (bool, error) {
|
||||
return r.managerCrossRegionEnabled, nil
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) GetInviteAttribution(context.Context, int64) (invitedomain.Attribution, error) {
|
||||
return invitedomain.Attribution{}, nil
|
||||
}
|
||||
@ -317,7 +360,10 @@ func (r *fakeModerationRepository) ExpireAdminUserBans(context.Context, int64, i
|
||||
return r.adminBanExpirations, nil
|
||||
}
|
||||
|
||||
func (r *fakeModerationRepository) GetUser(context.Context, int64) (userdomain.User, error) {
|
||||
func (r *fakeModerationRepository) GetUser(_ context.Context, userID int64) (userdomain.User, error) {
|
||||
if user, ok := r.users[userID]; ok {
|
||||
return user, nil
|
||||
}
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
|
||||
@ -168,6 +168,7 @@ type DeviceRepository interface {
|
||||
// ModerationRepository 持有用户状态和认证 session 的同库事务。
|
||||
type ModerationRepository interface {
|
||||
SetUserStatus(ctx context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error)
|
||||
ManagerCrossRegionEnabled(ctx context.Context, managerUserID int64) (bool, error)
|
||||
CreateManagerUserBlock(ctx context.Context, command ManagerUserBlockCommand, target userdomain.User) (ManagerUserBlock, error)
|
||||
ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]ManagerUserBlock, error)
|
||||
ReleaseManagerUserBlock(ctx context.Context, command ReleaseManagerUserBlockCommand) (ManagerUserBlockRelease, error)
|
||||
|
||||
@ -373,8 +373,8 @@ func (r *Repository) HasActiveManagerCapability(ctx context.Context, userID int6
|
||||
columnSQL = "can_update_user_level = 1"
|
||||
case hostservice.CapabilityManagerAddBDLeader:
|
||||
columnSQL = "can_add_bd_leader = 1"
|
||||
case hostservice.CapabilityManagerAddBDLeaderInRegion:
|
||||
columnSQL = "can_add_bd_leader_in_region = 1"
|
||||
case hostservice.CapabilityManagerCrossRegion, hostservice.CapabilityManagerAddBDLeaderInRegion:
|
||||
columnSQL = "can_manage_cross_region = 1"
|
||||
case hostservice.CapabilityManagerAddAdmin:
|
||||
columnSQL = "can_add_admin = 1"
|
||||
case hostservice.CapabilityManagerAddSuperadmin:
|
||||
|
||||
@ -13,6 +13,21 @@ import (
|
||||
userservice "hyapp/services/user-service/internal/service/user"
|
||||
)
|
||||
|
||||
// ManagerCrossRegionEnabled 读取 active 经理的通用数据范围开关;功能权限仍由 gateway/user host service 分别校验。
|
||||
func (r *Repository) ManagerCrossRegionEnabled(ctx context.Context, managerUserID int64) (bool, error) {
|
||||
var enabled bool
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT can_manage_cross_region = 1
|
||||
FROM manager_profiles
|
||||
WHERE app_code = ? AND user_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`, appcode.FromContext(ctx), managerUserID).Scan(&enabled)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return enabled, err
|
||||
}
|
||||
|
||||
// SetUserStatus 原子更新用户主状态,并在非 active 状态下吊销该用户所有 active refresh session。
|
||||
func (r *Repository) SetUserStatus(ctx context.Context, command userservice.UserStatusCommand) (userservice.UserStatusPersistenceResult, error) {
|
||||
if r == nil || r.db == nil {
|
||||
|
||||
@ -829,7 +829,7 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||
INSERT INTO manager_profiles (
|
||||
user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
|
||||
can_grant_vip, can_update_user_level, can_add_bd_leader, can_add_bd_leader_in_region, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
|
||||
can_grant_vip, can_update_user_level, can_add_bd_leader, can_manage_cross_region, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
@ -841,7 +841,7 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
can_grant_vip = VALUES(can_grant_vip),
|
||||
can_update_user_level = VALUES(can_update_user_level),
|
||||
can_add_bd_leader = VALUES(can_add_bd_leader),
|
||||
can_add_bd_leader_in_region = VALUES(can_add_bd_leader_in_region),
|
||||
can_manage_cross_region = VALUES(can_manage_cross_region),
|
||||
can_add_admin = VALUES(can_add_admin),
|
||||
can_add_superadmin = VALUES(can_add_superadmin),
|
||||
can_block_user = VALUES(can_block_user),
|
||||
@ -849,7 +849,7 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, profile.UserID, profile.Status, profile.Contact, defaultTrue(profile.CanGrantAvatarFrame),
|
||||
defaultTrue(profile.CanGrantVehicle), defaultTrue(profile.CanGrantBadge), defaultTrue(profile.CanGrantVIP),
|
||||
defaultTrue(profile.CanUpdateUserLevel), defaultTrue(profile.CanAddBDLeader), profile.CanAddBDLeaderInRegion, defaultTrue(profile.CanAddAdmin),
|
||||
defaultTrue(profile.CanUpdateUserLevel), defaultTrue(profile.CanAddBDLeader), profile.CanManageCrossRegion, defaultTrue(profile.CanAddAdmin),
|
||||
defaultTrue(profile.CanAddSuperadmin), defaultTrue(profile.CanBlockUser), defaultTrue(profile.CanTransferCountry),
|
||||
profile.CreatedByAdminID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||||
if err != nil {
|
||||
@ -857,12 +857,12 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) SetManagerAddBDLeaderInRegion(userID int64, enabled bool) {
|
||||
func (r *Repository) SetManagerCrossRegion(userID int64, enabled bool) {
|
||||
r.t.Helper()
|
||||
|
||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||
UPDATE manager_profiles
|
||||
SET can_add_bd_leader_in_region = ?
|
||||
SET can_manage_cross_region = ?
|
||||
WHERE user_id = ?
|
||||
`, enabled, userID)
|
||||
if err != nil {
|
||||
|
||||
@ -550,6 +550,7 @@ func (s *Server) GetRoleScopePolicy(ctx context.Context, req *userv1.GetRoleScop
|
||||
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: policy.Scene,
|
||||
BaseScope: string(policy.BaseScope),
|
||||
ExpandedScope: string(policy.ExpandedScope),
|
||||
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user