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 scene = 1;
|
||||||
string base_scope = 2;
|
string base_scope = 2;
|
||||||
bool region_expansion_configurable = 3;
|
bool region_expansion_configurable = 3;
|
||||||
|
// expanded_scope 是经理通用范围开关开启后的范围;调用方不能自行假设一定是 region。
|
||||||
|
string expanded_scope = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetRoleScopePolicyRequest {
|
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 复用。
|
当前 `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
|
```sql
|
||||||
admin_operation_logs(
|
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.
|
- BD/BD Leader base is downstream host salary only.
|
||||||
- Leader direct Agency salary source must be de-duplicated.
|
- Leader direct Agency salary source must be de-duplicated.
|
||||||
- Every admin mutation must be audited and idempotent.
|
- 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.
|
- 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 {
|
type RoleScopePolicy struct {
|
||||||
Scene string `json:"scene"`
|
Scene string `json:"scene"`
|
||||||
BaseScope string `json:"baseScope"`
|
BaseScope string `json:"baseScope"`
|
||||||
|
ExpandedScope string `json:"expandedScope"`
|
||||||
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -846,6 +847,7 @@ func (c *GRPCClient) GetRoleScopePolicy(ctx context.Context, req GetRoleScopePol
|
|||||||
return &RoleScopePolicy{
|
return &RoleScopePolicy{
|
||||||
Scene: policy.GetScene(),
|
Scene: policy.GetScene(),
|
||||||
BaseScope: policy.GetBaseScope(),
|
BaseScope: policy.GetBaseScope(),
|
||||||
|
ExpandedScope: policy.GetExpandedScope(),
|
||||||
RegionExpansionConfigurable: policy.GetRegionExpansionConfigurable(),
|
RegionExpansionConfigurable: policy.GetRegionExpansionConfigurable(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,7 @@ func (h *Handler) ListManagers(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.OK(c, ManagerPageResponse{
|
response.OK(c, ManagerPageResponse{
|
||||||
Items: items, Page: query.Page, PageSize: query.PageSize, Total: total,
|
Items: items, Page: query.Page, PageSize: query.PageSize, Total: total,
|
||||||
BDLeaderInviteScopePolicy: policy,
|
ManagerOperationScopePolicy: policy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,45 +68,47 @@ type UserIdentity struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ManagerListItem struct {
|
type ManagerListItem struct {
|
||||||
UserID int64 `json:"userId,string"`
|
UserID int64 `json:"userId,string"`
|
||||||
DisplayUserID string `json:"displayUserId"`
|
DisplayUserID string `json:"displayUserId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
RegionID int64 `json:"regionId"`
|
RegionID int64 `json:"regionId"`
|
||||||
RegionName string `json:"regionName"`
|
RegionName string `json:"regionName"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Contact string `json:"contact"`
|
Contact string `json:"contact"`
|
||||||
CanGrantAvatarFrame bool `json:"canGrantAvatarFrame"`
|
CanGrantAvatarFrame bool `json:"canGrantAvatarFrame"`
|
||||||
CanGrantVehicle bool `json:"canGrantVehicle"`
|
CanGrantVehicle bool `json:"canGrantVehicle"`
|
||||||
CanGrantBadge bool `json:"canGrantBadge"`
|
CanGrantBadge bool `json:"canGrantBadge"`
|
||||||
CanGrantVIP bool `json:"canGrantVip"`
|
CanGrantVIP bool `json:"canGrantVip"`
|
||||||
CanUpdateUserLevel bool `json:"canUpdateUserLevel"`
|
CanUpdateUserLevel bool `json:"canUpdateUserLevel"`
|
||||||
CanAddBDLeader bool `json:"canAddBdLeader"`
|
CanAddBDLeader bool `json:"canAddBdLeader"`
|
||||||
CanAddBDLeaderInRegion bool `json:"canAddBdLeaderInRegion"`
|
CanManageCrossRegion bool `json:"canManageCrossRegion"`
|
||||||
BDLeaderInviteBaseScope string `json:"bdLeaderInviteBaseScope"`
|
LegacyBDLeaderInRegion bool `json:"canAddBdLeaderInRegion"`
|
||||||
BDLeaderInviteEffectiveScope string `json:"bdLeaderInviteEffectiveScope"`
|
ManagerOperationBaseScope string `json:"managerOperationBaseScope"`
|
||||||
BDLeaderRegionExpansionConfigurable bool `json:"bdLeaderRegionExpansionConfigurable"`
|
ManagerOperationEffectiveScope string `json:"managerOperationEffectiveScope"`
|
||||||
CanAddAdmin bool `json:"canAddAdmin"`
|
ManagerCrossRegionConfigurable bool `json:"managerCrossRegionConfigurable"`
|
||||||
CanAddSuperadmin bool `json:"canAddSuperadmin"`
|
CanAddAdmin bool `json:"canAddAdmin"`
|
||||||
CanBlockUser bool `json:"canBlockUser"`
|
CanAddSuperadmin bool `json:"canAddSuperadmin"`
|
||||||
CanTransferCountry bool `json:"canTransferUserCountry"`
|
CanBlockUser bool `json:"canBlockUser"`
|
||||||
BDLeaderCount int64 `json:"bdLeaderCount"`
|
CanTransferCountry bool `json:"canTransferUserCountry"`
|
||||||
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
|
BDLeaderCount int64 `json:"bdLeaderCount"`
|
||||||
CreatedAtMs int64 `json:"createdAtMs"`
|
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
|
||||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
CreatedAtMs int64 `json:"createdAtMs"`
|
||||||
|
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManagerRoleScopePolicy struct {
|
type ManagerRoleScopePolicy struct {
|
||||||
BaseScope string `json:"baseScope"`
|
BaseScope string `json:"baseScope"`
|
||||||
|
ExpandedScope string `json:"expandedScope"`
|
||||||
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManagerPageResponse struct {
|
type ManagerPageResponse struct {
|
||||||
Items []*ManagerListItem `json:"items"`
|
Items []*ManagerListItem `json:"items"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PageSize int `json:"pageSize"`
|
PageSize int `json:"pageSize"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
BDLeaderInviteScopePolicy ManagerRoleScopePolicy `json:"bdLeaderInviteScopePolicy"`
|
ManagerOperationScopePolicy ManagerRoleScopePolicy `json:"managerOperationScopePolicy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoinSellerSalaryRateTier struct {
|
type CoinSellerSalaryRateTier struct {
|
||||||
@ -662,7 +664,7 @@ func (r *Reader) CreateManagerProfile(ctx context.Context, userID int64, actorID
|
|||||||
if _, err := r.db.ExecContext(ctx, `
|
if _, err := r.db.ExecContext(ctx, `
|
||||||
INSERT INTO manager_profiles (
|
INSERT INTO manager_profiles (
|
||||||
app_code, user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
|
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
|
created_by_admin_id, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
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_grant_vip = VALUES(can_grant_vip),
|
||||||
can_update_user_level = VALUES(can_update_user_level),
|
can_update_user_level = VALUES(can_update_user_level),
|
||||||
can_add_bd_leader = VALUES(can_add_bd_leader),
|
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_admin = VALUES(can_add_admin),
|
||||||
can_add_superadmin = VALUES(can_add_superadmin),
|
can_add_superadmin = VALUES(can_add_superadmin),
|
||||||
can_block_user = VALUES(can_block_user),
|
can_block_user = VALUES(can_block_user),
|
||||||
can_transfer_user_country = VALUES(can_transfer_user_country),
|
can_transfer_user_country = VALUES(can_transfer_user_country),
|
||||||
updated_at_ms = VALUES(updated_at_ms)
|
updated_at_ms = VALUES(updated_at_ms)
|
||||||
`, appCode, userID, contact, permissions.CanGrantAvatarFrame, permissions.CanGrantVehicle,
|
`, 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 {
|
permissions.CanAddSuperadmin, permissions.CanBlockUser, permissions.CanTransferCountry, actorID, nowMs, nowMs); err != nil {
|
||||||
return nil, err
|
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")+`,
|
COALESCE(manager.avatar, ''), `+userCountryRegionIDSQL("manager_region")+`, `+userCountryRegionNameSQL("manager_region")+`,
|
||||||
mp.status, COALESCE(mp.contact_info, ''),
|
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_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),
|
COUNT(DISTINCT bl.user_id), COALESCE(MAX(bl.created_at_ms), 0),
|
||||||
mp.created_at_ms, mp.updated_at_ms
|
mp.created_at_ms, mp.updated_at_ms
|
||||||
%s
|
%s
|
||||||
GROUP BY mp.user_id, manager.current_display_user_id, manager.username, manager.avatar,
|
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,
|
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_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
|
mp.created_at_ms, mp.updated_at_ms
|
||||||
ORDER BY mp.updated_at_ms DESC, mp.user_id DESC
|
ORDER BY mp.updated_at_ms DESC, mp.user_id DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
@ -824,7 +826,7 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
|
|||||||
&item.CanGrantVIP,
|
&item.CanGrantVIP,
|
||||||
&item.CanUpdateUserLevel,
|
&item.CanUpdateUserLevel,
|
||||||
&item.CanAddBDLeader,
|
&item.CanAddBDLeader,
|
||||||
&item.CanAddBDLeaderInRegion,
|
&item.CanManageCrossRegion,
|
||||||
&item.CanAddAdmin,
|
&item.CanAddAdmin,
|
||||||
&item.CanAddSuperadmin,
|
&item.CanAddSuperadmin,
|
||||||
&item.CanBlockUser,
|
&item.CanBlockUser,
|
||||||
|
|||||||
@ -75,7 +75,7 @@ func managerListRows() *sqlmock.Rows {
|
|||||||
"can_grant_vip",
|
"can_grant_vip",
|
||||||
"can_update_user_level",
|
"can_update_user_level",
|
||||||
"can_add_bd_leader",
|
"can_add_bd_leader",
|
||||||
"can_add_bd_leader_in_region",
|
"can_manage_cross_region",
|
||||||
"can_add_admin",
|
"can_add_admin",
|
||||||
"can_add_superadmin",
|
"can_add_superadmin",
|
||||||
"can_block_user",
|
"can_block_user",
|
||||||
|
|||||||
@ -77,7 +77,8 @@ type createManagerRequest struct {
|
|||||||
CanGrantVIP *bool `json:"canGrantVip"`
|
CanGrantVIP *bool `json:"canGrantVip"`
|
||||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||||
CanAddBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
CanManageCrossRegion *bool `json:"canManageCrossRegion"`
|
||||||
|
LegacyBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||||
CanBlockUser *bool `json:"canBlockUser"`
|
CanBlockUser *bool `json:"canBlockUser"`
|
||||||
@ -93,7 +94,8 @@ type updateManagerRequest struct {
|
|||||||
CanGrantVIP *bool `json:"canGrantVip"`
|
CanGrantVIP *bool `json:"canGrantVip"`
|
||||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||||
CanAddBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
CanManageCrossRegion *bool `json:"canManageCrossRegion"`
|
||||||
|
LegacyBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||||
CanBlockUser *bool `json:"canBlockUser"`
|
CanBlockUser *bool `json:"canBlockUser"`
|
||||||
@ -118,32 +120,32 @@ func (r updateManagerRequest) appendStatusAssignment(assignments *[]string, args
|
|||||||
}
|
}
|
||||||
|
|
||||||
type managerPermissions struct {
|
type managerPermissions struct {
|
||||||
CanGrantAvatarFrame bool
|
CanGrantAvatarFrame bool
|
||||||
CanGrantVehicle bool
|
CanGrantVehicle bool
|
||||||
CanGrantBadge bool
|
CanGrantBadge bool
|
||||||
CanGrantVIP bool
|
CanGrantVIP bool
|
||||||
CanUpdateUserLevel bool
|
CanUpdateUserLevel bool
|
||||||
CanAddBDLeader bool
|
CanAddBDLeader bool
|
||||||
CanAddBDLeaderInRegion bool
|
CanManageCrossRegion bool
|
||||||
CanAddAdmin bool
|
CanAddAdmin bool
|
||||||
CanAddSuperadmin bool
|
CanAddSuperadmin bool
|
||||||
CanBlockUser bool
|
CanBlockUser bool
|
||||||
CanTransferCountry bool
|
CanTransferCountry bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r createManagerRequest) permissionsWithDefault() managerPermissions {
|
func (r createManagerRequest) permissionsWithDefault() managerPermissions {
|
||||||
return managerPermissions{
|
return managerPermissions{
|
||||||
CanGrantAvatarFrame: boolDefaultTrue(r.CanGrantAvatarFrame),
|
CanGrantAvatarFrame: boolDefaultTrue(r.CanGrantAvatarFrame),
|
||||||
CanGrantVehicle: boolDefaultTrue(r.CanGrantVehicle),
|
CanGrantVehicle: boolDefaultTrue(r.CanGrantVehicle),
|
||||||
CanGrantBadge: boolDefaultTrue(r.CanGrantBadge),
|
CanGrantBadge: boolDefaultTrue(r.CanGrantBadge),
|
||||||
CanGrantVIP: boolDefaultTrue(r.CanGrantVIP),
|
CanGrantVIP: boolDefaultTrue(r.CanGrantVIP),
|
||||||
CanUpdateUserLevel: boolDefaultTrue(r.CanUpdateUserLevel),
|
CanUpdateUserLevel: boolDefaultTrue(r.CanUpdateUserLevel),
|
||||||
CanAddBDLeader: boolDefaultTrue(r.CanAddBDLeader),
|
CanAddBDLeader: boolDefaultTrue(r.CanAddBDLeader),
|
||||||
CanAddBDLeaderInRegion: boolDefaultFalse(r.CanAddBDLeaderInRegion),
|
CanManageCrossRegion: boolDefaultFalse(firstBool(r.CanManageCrossRegion, r.LegacyBDLeaderInRegion)),
|
||||||
CanAddAdmin: boolDefaultTrue(r.CanAddAdmin),
|
CanAddAdmin: boolDefaultTrue(r.CanAddAdmin),
|
||||||
CanAddSuperadmin: boolDefaultTrue(r.CanAddSuperadmin),
|
CanAddSuperadmin: boolDefaultTrue(r.CanAddSuperadmin),
|
||||||
CanBlockUser: boolDefaultTrue(r.CanBlockUser),
|
CanBlockUser: boolDefaultTrue(r.CanBlockUser),
|
||||||
CanTransferCountry: boolDefaultTrue(r.CanTransferCountry),
|
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_grant_vip", r.CanGrantVIP)
|
||||||
appendBoolAssignment(assignments, args, "can_update_user_level", r.CanUpdateUserLevel)
|
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", 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_admin", r.CanAddAdmin)
|
||||||
appendBoolAssignment(assignments, args, "can_add_superadmin", r.CanAddSuperadmin)
|
appendBoolAssignment(assignments, args, "can_add_superadmin", r.CanAddSuperadmin)
|
||||||
appendBoolAssignment(assignments, args, "can_block_user", r.CanBlockUser)
|
appendBoolAssignment(assignments, args, "can_block_user", r.CanBlockUser)
|
||||||
@ -177,6 +179,13 @@ func boolDefaultFalse(value *bool) bool {
|
|||||||
return value != nil && *value
|
return value != nil && *value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func firstBool(primary *bool, fallback *bool) *bool {
|
||||||
|
if primary != nil {
|
||||||
|
return primary
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
type createBDRequest struct {
|
type createBDRequest struct {
|
||||||
CommandID string `json:"commandId" binding:"required"`
|
CommandID string `json:"commandId" binding:"required"`
|
||||||
TargetUserID flexibleInt64 `json:"targetUserId" 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
|
var omitted createManagerRequest
|
||||||
if err := json.Unmarshal([]byte(`{"targetUserId":"1003"}`), &omitted); err != nil {
|
if err := json.Unmarshal([]byte(`{"targetUserId":"1003"}`), &omitted); err != nil {
|
||||||
t.Fatalf("unmarshal omitted permission: %v", err)
|
t.Fatalf("unmarshal omitted permission: %v", err)
|
||||||
}
|
}
|
||||||
if omitted.permissionsWithDefault().CanAddBDLeaderInRegion {
|
if omitted.permissionsWithDefault().CanManageCrossRegion {
|
||||||
t.Fatalf("region expansion must default closed")
|
t.Fatalf("cross-region manager must default closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
var enabled createManagerRequest
|
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)
|
t.Fatalf("unmarshal enabled permission: %v", err)
|
||||||
}
|
}
|
||||||
if !enabled.permissionsWithDefault().CanAddBDLeaderInRegion {
|
if !enabled.permissionsWithDefault().CanManageCrossRegion {
|
||||||
t.Fatalf("explicit region expansion must be preserved")
|
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"
|
sortByTotalRechargeUSDT = "total_recharge_usdt"
|
||||||
bdLeaderPositionAliasMaxRunes = 64
|
bdLeaderPositionAliasMaxRunes = 64
|
||||||
hostOrgInternalUserIDMinDigits = 15
|
hostOrgInternalUserIDMinDigits = 15
|
||||||
managerAddBDLeaderScopeScene = "manager_add_bd_leader"
|
managerOperationScopeScene = "manager_operations"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -70,6 +70,7 @@ func (s *Service) ListManagers(ctx context.Context, query listQuery) ([]*Manager
|
|||||||
decorateManagerScopePolicy(items, policy)
|
decorateManagerScopePolicy(items, policy)
|
||||||
return items, total, ManagerRoleScopePolicy{
|
return items, total, ManagerRoleScopePolicy{
|
||||||
BaseScope: policy.BaseScope,
|
BaseScope: policy.BaseScope,
|
||||||
|
ExpandedScope: policy.ExpandedScope,
|
||||||
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -79,7 +80,8 @@ func (s *Service) CreateManager(ctx context.Context, actorID int64, requestID st
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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())
|
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -98,7 +100,8 @@ func (s *Service) UpdateManager(ctx context.Context, actorID int64, userID int64
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
item, err := s.reader.UpdateManagerProfile(ctx, userID, actorID, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (s *Service) managerRoleScopePolicy(ctx context.Context) (*userclient.RoleScopePolicy, error) {
|
||||||
return s.userClient.GetRoleScopePolicy(ctx, userclient.GetRoleScopePolicyRequest{
|
return s.userClient.GetRoleScopePolicy(ctx, userclient.GetRoleScopePolicyRequest{
|
||||||
Caller: "hyapp-admin-server",
|
Caller: "hyapp-admin-server",
|
||||||
Scene: managerAddBDLeaderScopeScene,
|
Scene: managerOperationScopeScene,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizedManagerRegionPermission 让 App 基础策略决定开关是否可配置;global App 即使收到篡改请求也只保存 false。
|
// normalizedManagerCrossRegionPermission 让 App 基础策略决定开关是否可配置;global App 即使收到篡改请求也只保存 false。
|
||||||
func normalizedManagerRegionPermission(value *bool, configurable bool) *bool {
|
func normalizedManagerCrossRegionPermission(value *bool, configurable bool) *bool {
|
||||||
if configurable {
|
if configurable {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
@ -131,11 +134,12 @@ func decorateManagerScopePolicy(items []*ManagerListItem, policy *userclient.Rol
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item.BDLeaderInviteBaseScope = policy.BaseScope
|
item.LegacyBDLeaderInRegion = item.CanManageCrossRegion
|
||||||
item.BDLeaderRegionExpansionConfigurable = policy.RegionExpansionConfigurable
|
item.ManagerOperationBaseScope = policy.BaseScope
|
||||||
item.BDLeaderInviteEffectiveScope = policy.BaseScope
|
item.ManagerCrossRegionConfigurable = policy.RegionExpansionConfigurable
|
||||||
if policy.RegionExpansionConfigurable && item.CanAddBDLeaderInRegion {
|
item.ManagerOperationEffectiveScope = policy.BaseScope
|
||||||
item.BDLeaderInviteEffectiveScope = "region"
|
if policy.RegionExpansionConfigurable && item.CanManageCrossRegion {
|
||||||
|
item.ManagerOperationEffectiveScope = policy.ExpandedScope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,24 +130,26 @@ func TestManagerListItemJSONHasNoAgencyOrParentBD(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDecorateManagerScopePolicyUsesPolicyMetadataWithoutAppBranches(t *testing.T) {
|
func TestDecorateManagerScopePolicyUsesPolicyMetadataWithoutAppBranches(t *testing.T) {
|
||||||
items := []*ManagerListItem{{CanAddBDLeaderInRegion: true}}
|
items := []*ManagerListItem{{CanManageCrossRegion: true}}
|
||||||
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
||||||
BaseScope: "country",
|
BaseScope: "country",
|
||||||
|
ExpandedScope: "region",
|
||||||
RegionExpansionConfigurable: true,
|
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])
|
t.Fatalf("configurable policy decoration mismatch: %+v", items[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
items[0].CanAddBDLeaderInRegion = true
|
items[0].CanManageCrossRegion = true
|
||||||
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
||||||
BaseScope: "global",
|
BaseScope: "global",
|
||||||
|
ExpandedScope: "global",
|
||||||
RegionExpansionConfigurable: false,
|
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])
|
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")
|
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{
|
profileClient := &fakeUserProfileClient{
|
||||||
businessLookupResp: &userv1.BusinessUserLookupResponse{
|
businessLookupResp: &userv1.BusinessUserLookupResponse{
|
||||||
Users: []*userv1.BusinessUserLookupItem{
|
Users: []*userv1.BusinessUserLookupItem{
|
||||||
@ -298,15 +298,15 @@ func TestSearchManagerUsersDefaultsSameCountryAndAllowsAllForCountryTransfer(t *
|
|||||||
if err := json.NewDecoder(allRecorder.Body).Decode(&allEnvelope); err != nil {
|
if err := json.NewDecoder(allRecorder.Body).Decode(&allEnvelope); err != nil {
|
||||||
t.Fatalf("decode all-country response failed: %v", err)
|
t.Fatalf("decode all-country response failed: %v", err)
|
||||||
}
|
}
|
||||||
if allEnvelope.Data.Total != 2 || allEnvelope.Data.Items[1].UserID != "900002" {
|
if allEnvelope.Data.Total != 1 || allEnvelope.Data.Items[0].UserID != "900001" {
|
||||||
t.Fatalf("country transfer search must include cross-country users: %+v", allEnvelope.Data)
|
t.Fatalf("scope=all must not bypass manager data scope: %+v", allEnvelope.Data)
|
||||||
}
|
}
|
||||||
if !hostClient.sawCapability("manager_transfer_user_country") {
|
if hostClient.sawCapability("manager_transfer_user_country") {
|
||||||
t.Fatalf("scope=all search must check transfer-country capability, saw %#v", hostClient.capabilities)
|
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{
|
profileClient := &fakeUserProfileClient{
|
||||||
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
||||||
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||||
@ -319,7 +319,7 @@ func TestSearchManagerUsersUsesResolvedGlobalPolicyForHuwaaBDLeaderAction(t *tes
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
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)
|
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
||||||
|
|
||||||
@ -363,12 +363,12 @@ func TestSearchManagerUsersUsesResolvedGlobalPolicyForHuwaaBDLeaderAction(t *tes
|
|||||||
if err := json.NewDecoder(adminRecorder.Body).Decode(&adminEnvelope); err != nil {
|
if err := json.NewDecoder(adminRecorder.Body).Decode(&adminEnvelope); err != nil {
|
||||||
t.Fatalf("decode Huwaa admin search response failed: %v", err)
|
t.Fatalf("decode Huwaa admin search response failed: %v", err)
|
||||||
}
|
}
|
||||||
if adminEnvelope.Data.Total != 1 {
|
if adminEnvelope.Data.Total != 2 {
|
||||||
t.Fatalf("Huwaa non-role manager actions must remain same-country: %+v", adminEnvelope.Data)
|
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{
|
profileClient := &fakeUserProfileClient{
|
||||||
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
||||||
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||||
@ -384,38 +384,37 @@ func TestSearchManagerUsersAppliesManagerRegionExpansionWithoutCrossingRegion(t
|
|||||||
}
|
}
|
||||||
for _, testCase := range []struct {
|
for _, testCase := range []struct {
|
||||||
name string
|
name string
|
||||||
regionOpen bool
|
crossRegion bool
|
||||||
wantVisible int
|
wantVisible int
|
||||||
}{
|
}{
|
||||||
{name: "closed keeps country", regionOpen: false, wantVisible: 1},
|
{name: "closed keeps country", crossRegion: false, wantVisible: 1},
|
||||||
{name: "open expands within region", regionOpen: true, wantVisible: 2},
|
{name: "open expands inside region", crossRegion: true, wantVisible: 2},
|
||||||
} {
|
} {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{
|
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{
|
||||||
"manager_add_bd_leader_in_region": testCase.regionOpen,
|
"manager_cross_region": testCase.crossRegion,
|
||||||
}}
|
}}
|
||||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
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)
|
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.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&scope=all&action="+action, nil)
|
||||||
request.Header.Set("X-Request-ID", "req-manager-region-search")
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
recorder := httptest.NewRecorder()
|
request.Header.Set("X-Request-ID", "req-manager-cross-region-search")
|
||||||
router.ServeHTTP(recorder, request)
|
recorder := httptest.NewRecorder()
|
||||||
if recorder.Code != http.StatusOK {
|
router.ServeHTTP(recorder, request)
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
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 {
|
var envelope struct {
|
||||||
Total int `json:"total"`
|
Data struct {
|
||||||
} `json:"data"`
|
Total int `json:"total"`
|
||||||
}
|
} `json:"data"`
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
}
|
||||||
t.Fatalf("decode response: %v", err)
|
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("visible total = %d, want %d", envelope.Data.Total, testCase.wantVisible)
|
if envelope.Data.Total != testCase.wantVisible {
|
||||||
}
|
t.Fatalf("%s visible total = %d, want %d", action, envelope.Data.Total, testCase.wantVisible)
|
||||||
if hostClient.sawCapability("manager_transfer_user_country") {
|
}
|
||||||
t.Fatalf("scope=all must not override add-bd-leader policy")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -423,8 +422,8 @@ func TestSearchManagerUsersAppliesManagerRegionExpansionWithoutCrossingRegion(t
|
|||||||
|
|
||||||
func TestGrantManagerResourceUsesServerControlledGrantShape(t *testing.T) {
|
func TestGrantManagerResourceUsesServerControlledGrantShape(t *testing.T) {
|
||||||
walletClient := &fakeWalletClient{}
|
walletClient := &fakeWalletClient{}
|
||||||
hostClient := &fakeUserHostClient{}
|
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
|
||||||
profileClient := &fakeUserProfileClient{}
|
profileClient := crossRegionManagerProfileClient()
|
||||||
router := newManagerCenterTestRouter(walletClient, hostClient, profileClient)
|
router := newManagerCenterTestRouter(walletClient, hostClient, profileClient)
|
||||||
body := []byte(`{"command_id":"mgr-grant-1","target_user_id":"900001","resource_id":"101"}`)
|
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))
|
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 {
|
if walletClient.lastGetResource == nil || walletClient.lastGetResource.GetResourceId() != 202 {
|
||||||
t.Fatalf("resource lookup mismatch: %+v", walletClient.lastGetResource)
|
t.Fatalf("resource lookup mismatch: %+v", walletClient.lastGetResource)
|
||||||
}
|
}
|
||||||
if hostClient.lastCapability == nil || hostClient.lastCapability.GetCapability() != "manager_center" {
|
if hostClient.lastCapability == nil || hostClient.lastCapability.GetCapability() != "manager_cross_region" {
|
||||||
t.Fatalf("last manager capability should be final same-country actor check, got %+v", hostClient.lastCapability)
|
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") {
|
if !hostClient.sawCapability("manager_grant_vehicle") {
|
||||||
t.Fatalf("vehicle grant must check manager_grant_vehicle, saw %#v", hostClient.capabilities)
|
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) {
|
func TestGrantManagerVIPUsesManagerCenterGrantSource(t *testing.T) {
|
||||||
walletClient := &fakeWalletClient{}
|
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}`)
|
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 := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body))
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
@ -707,8 +709,8 @@ func TestGrantManagerVIPUsesTrialCardForConfiguredApp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManagerBlockAndUnblockCallUserService(t *testing.T) {
|
func TestManagerBlockAndUnblockCallUserService(t *testing.T) {
|
||||||
profileClient := &fakeUserProfileClient{}
|
profileClient := crossRegionManagerProfileClient()
|
||||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, &fakeUserHostClient{}, profileClient)
|
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}`)
|
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 := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/blocks", bytes.NewReader(body))
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
@ -741,7 +743,7 @@ func TestManagerBlockAndUnblockCallUserService(t *testing.T) {
|
|||||||
|
|
||||||
func TestSetManagerUserLevelCallsGrowthService(t *testing.T) {
|
func TestSetManagerUserLevelCallsGrowthService(t *testing.T) {
|
||||||
growthClient := &fakeGrowthLevelClient{}
|
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}`)
|
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 := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/levels", bytes.NewReader(body))
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
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},
|
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},
|
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)
|
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||||
body := []byte(`{"command_id":"mgr-bdleader-cross","target_user_id":"900001"}`)
|
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))
|
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{}
|
hostAdminClient := &fakeUserHostAdminClient{}
|
||||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
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: "CA", 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)
|
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 := 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))
|
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) {
|
func TestCreateManagerBDLeaderAllowsCrossCountryTargetForHuwaa(t *testing.T) {
|
||||||
hostAdminClient := &fakeUserHostAdminClient{}
|
hostAdminClient := &fakeUserHostAdminClient{}
|
||||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
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},
|
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, DisplayUserId: "163006", Country: "CA", RegionId: 2002},
|
||||||
}}
|
}}
|
||||||
hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
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)
|
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||||
body := []byte(`{"command_id":"huwaa-mgr-bdleader-cross","target_user_id":"900001"}`)
|
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{
|
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)
|
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
||||||
body := []byte(`{"command_id":"mgr-country-1","target_user_id":"900001","country":"CA"}`)
|
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))
|
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 验证经理概览聚合团队工资卡片:
|
// TestGetManagerOverviewIncludesTeamSalaryDashboard 验证经理概览聚合团队工资卡片:
|
||||||
// user-service 组织树 Agency owner 去重后传给 wallet-service,按本月/上月两个 UTC 周期取回预收入并计算环比。
|
// user-service 组织树 Agency owner 去重后传给 wallet-service,按本月/上月两个 UTC 周期取回预收入并计算环比。
|
||||||
func TestGetManagerOverviewIncludesTeamSalaryDashboard(t *testing.T) {
|
func TestGetManagerOverviewIncludesTeamSalaryDashboard(t *testing.T) {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const managerGrantBadgeCapability = "manager_grant_badge"
|
|||||||
const managerGrantVIPCapability = "manager_grant_vip"
|
const managerGrantVIPCapability = "manager_grant_vip"
|
||||||
const managerUpdateUserLevelCapability = "manager_update_user_level"
|
const managerUpdateUserLevelCapability = "manager_update_user_level"
|
||||||
const managerAddBDLeaderCapability = "manager_add_bd_leader"
|
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 managerAddAdminCapability = "manager_add_admin"
|
||||||
const managerAddSuperadminCapability = "manager_add_superadmin"
|
const managerAddSuperadminCapability = "manager_add_superadmin"
|
||||||
const managerBlockUserCapability = "manager_block_user"
|
const managerBlockUserCapability = "manager_block_user"
|
||||||
@ -38,6 +38,7 @@ const managerGrantMaxVIPLevel int32 = 5
|
|||||||
const managerRoleScopeCountry = "country"
|
const managerRoleScopeCountry = "country"
|
||||||
const managerRoleScopeRegion = "region"
|
const managerRoleScopeRegion = "region"
|
||||||
const managerRoleScopeGlobal = "global"
|
const managerRoleScopeGlobal = "global"
|
||||||
|
const managerOperationScopeScene = "manager_operations"
|
||||||
|
|
||||||
type managerGrantResourceData struct {
|
type managerGrantResourceData struct {
|
||||||
ResourceID string `json:"resource_id"`
|
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")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 搜索先走已有 business lookup 保持短号/昵称解析一致,再用 BatchGetUsers 拿国家和等级投影做最终过滤。
|
// 搜索先走已有 business lookup 保持短号/昵称解析一致,再用 BatchGetUsers 拿国家和区域投影做最终过滤。
|
||||||
// BD Leader 的 App 差异来自 user-service 策略,handler 不直接判断 app_code。
|
// 所有经理动作的 App 差异都来自 user-service 策略,handler 不直接判断 app_code。
|
||||||
actor, ok := h.requireManagerCenterActor(writer, request)
|
actor, ok := h.requireManagerCenterActor(writer, request)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
scope := strings.ToLower(strings.TrimSpace(request.URL.Query().Get("scope")))
|
managerTargetScope, ok := h.managerEffectiveScope(writer, request)
|
||||||
bdLeaderSearch := managerSearchScene(request) == managerAddBDLeaderCapability
|
if !ok {
|
||||||
requestedAllCountries := scope == "all" && !bdLeaderSearch
|
|
||||||
if requestedAllCountries && !h.requireManagerCapability(writer, request, managerTransferUserCountryCapability) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
managerTargetScope := managerRoleScopeCountry
|
|
||||||
if bdLeaderSearch {
|
|
||||||
managerTargetScope, ok = h.managerBDLeaderEffectiveScope(writer, request)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||||
if !ok {
|
if !ok {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
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 {
|
for _, userID := range userIDs {
|
||||||
user := usersResp.GetUsers()[userID]
|
user := usersResp.GetUsers()[userID]
|
||||||
// 搜索与最终创建共用 scope evaluator,避免同一 App 的规则在两个 handler 分叉。
|
// 搜索与最终创建共用 scope evaluator,避免同一 App 的规则在两个 handler 分叉。
|
||||||
if user == nil || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE ||
|
if user == nil || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE || !managerTargetMatchesScope(managerTargetScope, actor, user) {
|
||||||
(!requestedAllCountries && !managerTargetMatchesScope(managerTargetScope, actor, user)) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item := managerUserFromProto(user, levels[userID])
|
item := managerUserFromProto(user, levels[userID])
|
||||||
@ -391,8 +382,8 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
}
|
}
|
||||||
resources = append(resources, resource)
|
resources = append(resources, resource)
|
||||||
}
|
}
|
||||||
// 赠送资源前先确认目标用户仍然 active 且与经理同国家;wallet-service 还会在事务内复验资源白名单。
|
// 所有经理目标用户动作共用同一有效范围;wallet-service 还会在事务内复验资源白名单。
|
||||||
if _, ok := h.requireSameCountryTarget(writer, request, targetUserID); !ok {
|
if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reason := strings.TrimSpace(body.Reason)
|
reason := strings.TrimSpace(body.Reason)
|
||||||
@ -477,7 +468,7 @@ func (h *Handler) grantManagerVIP(writer http.ResponseWriter, request *http.Requ
|
|||||||
}
|
}
|
||||||
// 经理中心先读取 App 级 program,再选择直接会员或体验卡专用命令。钱包会在写事务内再次校验,
|
// 经理中心先读取 App 级 program,再选择直接会员或体验卡专用命令。钱包会在写事务内再次校验,
|
||||||
// 因此配置在两次 RPC 之间切换时只会明确失败,不会绕过当前发放语义。
|
// 因此配置在两次 RPC 之间切换时只会明确失败,不会绕过当前发放语义。
|
||||||
if _, ok := h.requireSameCountryTarget(writer, request, targetUserID); !ok {
|
if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reason := strings.TrimSpace(body.Reason)
|
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")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// user-service 会再次读取双方用户并校验同国家,这里保留 gateway 早拒绝以减少无效封禁事务。
|
// user-service 会按同一 App 策略复验,gateway 先拒绝越界目标以减少无效封禁事务。
|
||||||
if _, ok := h.requireSameCountryTargetWithActor(writer, request, actor, targetUserID); !ok {
|
if _, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userProfileClient.CreateManagerUserBlock(request.Context(), &userv1.CreateManagerUserBlockRequest{
|
resp, err := h.userProfileClient.CreateManagerUserBlock(request.Context(), &userv1.CreateManagerUserBlockRequest{
|
||||||
@ -732,7 +723,7 @@ func (h *Handler) setManagerUserLevel(writer http.ResponseWriter, request *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 等级设置会把目标轨道 total_value 直接改到对应规则阈值,并由 activity-service 补齐 1..目标等级奖励。
|
// 等级设置会把目标轨道 total_value 直接改到对应规则阈值,并由 activity-service 补齐 1..目标等级奖励。
|
||||||
if _, ok := h.requireSameCountryTarget(writer, request, targetUserID); !ok {
|
if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.growthLevelClient.SetUserLevel(request.Context(), &activityv1.SetUserLevelRequest{
|
resp, err := h.growthLevelClient.SetUserLevel(request.Context(), &activityv1.SetUserLevelRequest{
|
||||||
@ -783,7 +774,7 @@ func (h *Handler) createManagerBDLeader(writer http.ResponseWriter, request *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 写入口重新解析策略并读取 active 目标,不能信任搜索结果或客户端自报范围。
|
// 写入口重新解析策略并读取 active 目标,不能信任搜索结果或客户端自报范围。
|
||||||
target, ok := h.requireManagerBDLeaderTarget(writer, request, actor, targetUserID)
|
target, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -847,7 +838,10 @@ func (h *Handler) transferManagerUserCountry(writer http.ResponseWriter, request
|
|||||||
if reason == "" {
|
if reason == "" {
|
||||||
reason = "manager_center_country_transfer"
|
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{
|
resp, err := h.userProfileClient.AdminChangeUserCountry(request.Context(), &userv1.AdminChangeUserCountryRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
TargetUserId: targetUserID,
|
TargetUserId: targetUserID,
|
||||||
@ -950,10 +944,10 @@ func (h *Handler) requireManagerCapability(writer http.ResponseWriter, request *
|
|||||||
return true
|
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{
|
resp, err := h.userHostClient.GetRoleScopePolicy(request.Context(), &userv1.GetRoleScopePolicyRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
Scene: managerAddBDLeaderCapability,
|
Scene: managerOperationScopeScene,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
@ -968,43 +962,40 @@ func (h *Handler) managerBDLeaderEffectiveScope(writer http.ResponseWriter, requ
|
|||||||
if !policy.GetRegionExpansionConfigurable() {
|
if !policy.GetRegionExpansionConfigurable() {
|
||||||
return scope, true
|
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{
|
capability, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||||
Capability: managerAddBDLeaderInRegionCapability,
|
Capability: managerCrossRegionCapability,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if capability.GetAllowed() {
|
if capability.GetAllowed() {
|
||||||
scope = managerRoleScopeRegion
|
scope = expandedScope
|
||||||
}
|
}
|
||||||
return scope, true
|
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)
|
actor, ok := h.requireManagerCenterActor(writer, request)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
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) {
|
func (h *Handler) requireManagerTargetWithActor(writer http.ResponseWriter, request *http.Request, actor *userv1.User, targetUserID int64) (*userv1.User, bool) {
|
||||||
target, ok := h.requireActiveTarget(writer, request, targetUserID)
|
scope, ok := h.managerEffectiveScope(writer, request)
|
||||||
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)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@ -1019,7 +1010,7 @@ func (h *Handler) requireManagerBDLeaderTarget(writer http.ResponseWriter, reque
|
|||||||
return target, true
|
return target, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// requireActiveTarget 是经理写入口共同的目标用户真实性门禁;区域策略只能决定后续是否比较国家,不能绕过 active 用户校验。
|
// requireActiveTarget 是经理写入口共同的目标用户真实性门禁;范围策略只能决定后续国家/区域匹配,不能绕过 active 用户校验。
|
||||||
func (h *Handler) requireActiveTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
|
func (h *Handler) requireActiveTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
|
||||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
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 {
|
func (h *Handler) managerPermissionOverview(request *http.Request) map[string]bool {
|
||||||
permissions := map[string]bool{
|
permissions := map[string]bool{
|
||||||
"can_grant_avatar_frame": false,
|
"can_grant_avatar_frame": false,
|
||||||
"can_grant_vehicle": false,
|
"can_grant_vehicle": false,
|
||||||
"can_grant_badge": false,
|
"can_grant_badge": false,
|
||||||
"can_grant_vip": false,
|
"can_grant_vip": false,
|
||||||
"can_update_user_level": false,
|
"can_update_user_level": false,
|
||||||
"can_add_bd_leader": false,
|
"can_add_bd_leader": false,
|
||||||
"can_add_bd_leader_in_region": false,
|
"can_manage_cross_region": false,
|
||||||
"can_add_admin": false,
|
"can_add_admin": false,
|
||||||
"can_add_superadmin": false,
|
"can_add_superadmin": false,
|
||||||
"can_block_user": false,
|
"can_block_user": false,
|
||||||
"can_transfer_user_country": false,
|
"can_transfer_user_country": false,
|
||||||
}
|
}
|
||||||
for key, capability := range map[string]string{
|
for key, capability := range map[string]string{
|
||||||
"can_grant_avatar_frame": managerGrantAvatarFrameCapability,
|
"can_grant_avatar_frame": managerGrantAvatarFrameCapability,
|
||||||
"can_grant_vehicle": managerGrantVehicleCapability,
|
"can_grant_vehicle": managerGrantVehicleCapability,
|
||||||
"can_grant_badge": managerGrantBadgeCapability,
|
"can_grant_badge": managerGrantBadgeCapability,
|
||||||
"can_grant_vip": managerGrantVIPCapability,
|
"can_grant_vip": managerGrantVIPCapability,
|
||||||
"can_update_user_level": managerUpdateUserLevelCapability,
|
"can_update_user_level": managerUpdateUserLevelCapability,
|
||||||
"can_add_bd_leader": managerAddBDLeaderCapability,
|
"can_add_bd_leader": managerAddBDLeaderCapability,
|
||||||
"can_add_bd_leader_in_region": managerAddBDLeaderInRegionCapability,
|
"can_manage_cross_region": managerCrossRegionCapability,
|
||||||
"can_add_admin": managerAddAdminCapability,
|
"can_add_admin": managerAddAdminCapability,
|
||||||
"can_add_superadmin": managerAddSuperadminCapability,
|
"can_add_superadmin": managerAddSuperadminCapability,
|
||||||
"can_block_user": managerBlockUserCapability,
|
"can_block_user": managerBlockUserCapability,
|
||||||
"can_transfer_user_country": managerTransferUserCountryCapability,
|
"can_transfer_user_country": managerTransferUserCountryCapability,
|
||||||
} {
|
} {
|
||||||
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
|||||||
@ -1979,6 +1979,9 @@ func (f *fakeUserHostClient) CheckBusinessCapability(_ context.Context, req *use
|
|||||||
if allowed, configured := f.capabilityAllowed[req.GetCapability()]; configured {
|
if allowed, configured := f.capabilityAllowed[req.GetCapability()]; configured {
|
||||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: allowed}, nil
|
return &userv1.CheckBusinessCapabilityResponse{Allowed: allowed}, nil
|
||||||
}
|
}
|
||||||
|
if req.GetCapability() == "manager_cross_region" {
|
||||||
|
return &userv1.CheckBusinessCapabilityResponse{Allowed: false}, nil
|
||||||
|
}
|
||||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: true}, 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 f.roleScopePolicyResp, nil
|
||||||
}
|
}
|
||||||
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||||
Scene: "manager_add_bd_leader",
|
Scene: "manager_operations",
|
||||||
BaseScope: "country",
|
BaseScope: "country",
|
||||||
|
ExpandedScope: "region",
|
||||||
RegionExpansionConfigurable: true,
|
RegionExpansionConfigurable: true,
|
||||||
}}, nil
|
}}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -887,6 +887,7 @@ CREATE TABLE IF NOT EXISTS manager_profiles (
|
|||||||
can_update_user_level TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心升级用户等级',
|
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 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_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_admin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Admin',
|
||||||
can_add_superadmin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Superadmin',
|
can_add_superadmin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Superadmin',
|
||||||
can_block_user TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心封禁用户',
|
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 '提供方',
|
provider VARCHAR(64) NULL COMMENT '提供方',
|
||||||
channel VARCHAR(32) NOT NULL DEFAULT '' COMMENT '渠道',
|
channel VARCHAR(32) NOT NULL DEFAULT '' COMMENT '渠道',
|
||||||
platform VARCHAR(16) 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 '结果',
|
result VARCHAR(32) NOT NULL COMMENT '结果',
|
||||||
failure_code VARCHAR(64) NULL COMMENT '失败编码',
|
failure_code VARCHAR(64) NULL COMMENT '失败编码',
|
||||||
client_ip VARCHAR(64) NULL COMMENT '客户端IP',
|
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_request_id (app_code, request_id),
|
||||||
KEY idx_login_audit_created_at (app_code, created_at_ms),
|
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_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_blocked (app_code, blocked, created_at_ms),
|
||||||
KEY idx_login_audit_ip_country (app_code, ip_country_code, 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='登录审计表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录审计表';
|
||||||
|
|||||||
@ -231,24 +231,24 @@ type BDProfile struct {
|
|||||||
// ManagerProfile 是经理身份事实。
|
// ManagerProfile 是经理身份事实。
|
||||||
// 经理是 BD Leader/Admin 的上级,和 Host/Agency/BD 链路没有父子关系。
|
// 经理是 BD Leader/Admin 的上级,和 Host/Agency/BD 链路没有父子关系。
|
||||||
type ManagerProfile struct {
|
type ManagerProfile struct {
|
||||||
UserID int64
|
UserID int64
|
||||||
RegionID int64
|
RegionID int64
|
||||||
Status string
|
Status string
|
||||||
Contact string
|
Contact string
|
||||||
CanGrantAvatarFrame bool
|
CanGrantAvatarFrame bool
|
||||||
CanGrantVehicle bool
|
CanGrantVehicle bool
|
||||||
CanGrantBadge bool
|
CanGrantBadge bool
|
||||||
CanGrantVIP bool
|
CanGrantVIP bool
|
||||||
CanUpdateUserLevel bool
|
CanUpdateUserLevel bool
|
||||||
CanAddBDLeader bool
|
CanAddBDLeader bool
|
||||||
CanAddBDLeaderInRegion bool
|
CanManageCrossRegion bool
|
||||||
CanAddAdmin bool
|
CanAddAdmin bool
|
||||||
CanAddSuperadmin bool
|
CanAddSuperadmin bool
|
||||||
CanBlockUser bool
|
CanBlockUser bool
|
||||||
CanTransferCountry bool
|
CanTransferCountry bool
|
||||||
CreatedByAdminID int64
|
CreatedByAdminID int64
|
||||||
CreatedAtMs int64
|
CreatedAtMs int64
|
||||||
UpdatedAtMs int64
|
UpdatedAtMs int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// CoinSellerProfile 是币商角色事实,余额不保存在 user-service。
|
// CoinSellerProfile 是币商角色事实,余额不保存在 user-service。
|
||||||
|
|||||||
@ -8,14 +8,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CapabilityManagerCenter = "manager_center"
|
CapabilityManagerCenter = "manager_center"
|
||||||
CapabilityManagerResourceGrant = "manager_resource_grant"
|
CapabilityManagerResourceGrant = "manager_resource_grant"
|
||||||
CapabilityManagerGrantAvatarFrame = "manager_grant_avatar_frame"
|
CapabilityManagerGrantAvatarFrame = "manager_grant_avatar_frame"
|
||||||
CapabilityManagerGrantVehicle = "manager_grant_vehicle"
|
CapabilityManagerGrantVehicle = "manager_grant_vehicle"
|
||||||
CapabilityManagerGrantBadge = "manager_grant_badge"
|
CapabilityManagerGrantBadge = "manager_grant_badge"
|
||||||
CapabilityManagerGrantVIP = "manager_grant_vip"
|
CapabilityManagerGrantVIP = "manager_grant_vip"
|
||||||
CapabilityManagerUpdateUserLevel = "manager_update_user_level"
|
CapabilityManagerUpdateUserLevel = "manager_update_user_level"
|
||||||
CapabilityManagerAddBDLeader = "manager_add_bd_leader"
|
CapabilityManagerAddBDLeader = "manager_add_bd_leader"
|
||||||
|
CapabilityManagerCrossRegion = "manager_cross_region"
|
||||||
|
// CapabilityManagerAddBDLeaderInRegion 兼容上一版 gateway,底层读取同一个通用跨区经理开关。
|
||||||
CapabilityManagerAddBDLeaderInRegion = "manager_add_bd_leader_in_region"
|
CapabilityManagerAddBDLeaderInRegion = "manager_add_bd_leader_in_region"
|
||||||
CapabilityManagerAddAdmin = "manager_add_admin"
|
CapabilityManagerAddAdmin = "manager_add_admin"
|
||||||
CapabilityManagerAddSuperadmin = "manager_add_superadmin"
|
CapabilityManagerAddSuperadmin = "manager_add_superadmin"
|
||||||
@ -50,6 +52,7 @@ func (s *Service) CheckBusinessCapability(ctx context.Context, actorUserID int64
|
|||||||
CapabilityManagerGrantVIP,
|
CapabilityManagerGrantVIP,
|
||||||
CapabilityManagerUpdateUserLevel,
|
CapabilityManagerUpdateUserLevel,
|
||||||
CapabilityManagerAddBDLeader,
|
CapabilityManagerAddBDLeader,
|
||||||
|
CapabilityManagerCrossRegion,
|
||||||
CapabilityManagerAddBDLeaderInRegion,
|
CapabilityManagerAddBDLeaderInRegion,
|
||||||
CapabilityManagerAddAdmin,
|
CapabilityManagerAddAdmin,
|
||||||
CapabilityManagerAddSuperadmin,
|
CapabilityManagerAddSuperadmin,
|
||||||
|
|||||||
@ -15,24 +15,29 @@ const (
|
|||||||
RoleScopeGlobal RoleScope = "global"
|
RoleScopeGlobal RoleScope = "global"
|
||||||
|
|
||||||
RoleScopeSceneOrganizationExpansion = "organization_role_expansion"
|
RoleScopeSceneOrganizationExpansion = "organization_role_expansion"
|
||||||
RoleScopeSceneManagerAddBDLeader = "manager_add_bd_leader"
|
RoleScopeSceneManagerOperations = "manager_operations"
|
||||||
|
// RoleScopeSceneManagerAddBDLeader 兼容上一版 gateway/admin 的策略查询,解析后统一返回 manager_operations。
|
||||||
|
RoleScopeSceneManagerAddBDLeader = "manager_add_bd_leader"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RoleScopePolicy 把 App 固有边界和可运营扩展能力分开;业务入口只根据解析结果判断目标范围。
|
// RoleScopePolicy 把 App 固有边界和可运营扩展能力分开;业务入口只根据解析结果判断目标范围。
|
||||||
type RoleScopePolicy struct {
|
type RoleScopePolicy struct {
|
||||||
Scene string
|
Scene string
|
||||||
BaseScope RoleScope
|
BaseScope RoleScope
|
||||||
|
ExpandedScope RoleScope
|
||||||
RegionExpansionConfigurable bool
|
RegionExpansionConfigurable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultRoleScopePolicies = map[string]RoleScopePolicy{
|
var defaultRoleScopePolicies = map[string]RoleScopePolicy{
|
||||||
RoleScopeSceneOrganizationExpansion: {
|
RoleScopeSceneOrganizationExpansion: {
|
||||||
Scene: RoleScopeSceneOrganizationExpansion,
|
Scene: RoleScopeSceneOrganizationExpansion,
|
||||||
BaseScope: RoleScopeRegion,
|
BaseScope: RoleScopeRegion,
|
||||||
|
ExpandedScope: RoleScopeRegion,
|
||||||
},
|
},
|
||||||
RoleScopeSceneManagerAddBDLeader: {
|
RoleScopeSceneManagerOperations: {
|
||||||
Scene: RoleScopeSceneManagerAddBDLeader,
|
Scene: RoleScopeSceneManagerOperations,
|
||||||
BaseScope: RoleScopeCountry,
|
BaseScope: RoleScopeCountry,
|
||||||
|
ExpandedScope: RoleScopeRegion,
|
||||||
RegionExpansionConfigurable: true,
|
RegionExpansionConfigurable: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -40,12 +45,14 @@ var defaultRoleScopePolicies = map[string]RoleScopePolicy{
|
|||||||
var appRoleScopePolicyOverrides = map[string]map[string]RoleScopePolicy{
|
var appRoleScopePolicyOverrides = map[string]map[string]RoleScopePolicy{
|
||||||
"huwaa": {
|
"huwaa": {
|
||||||
RoleScopeSceneOrganizationExpansion: {
|
RoleScopeSceneOrganizationExpansion: {
|
||||||
Scene: RoleScopeSceneOrganizationExpansion,
|
Scene: RoleScopeSceneOrganizationExpansion,
|
||||||
BaseScope: RoleScopeGlobal,
|
BaseScope: RoleScopeGlobal,
|
||||||
|
ExpandedScope: RoleScopeGlobal,
|
||||||
},
|
},
|
||||||
RoleScopeSceneManagerAddBDLeader: {
|
RoleScopeSceneManagerOperations: {
|
||||||
Scene: RoleScopeSceneManagerAddBDLeader,
|
Scene: RoleScopeSceneManagerOperations,
|
||||||
BaseScope: RoleScopeGlobal,
|
BaseScope: RoleScopeGlobal,
|
||||||
|
ExpandedScope: RoleScopeGlobal,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -53,6 +60,9 @@ var appRoleScopePolicyOverrides = map[string]map[string]RoleScopePolicy{
|
|||||||
// ResolveRoleScopePolicy 是 App 差异的唯一解析入口;新增 App 只扩展注册表,不修改搜索或写入 handler。
|
// ResolveRoleScopePolicy 是 App 差异的唯一解析入口;新增 App 只扩展注册表,不修改搜索或写入 handler。
|
||||||
func ResolveRoleScopePolicy(rawAppCode string, rawScene string) (RoleScopePolicy, error) {
|
func ResolveRoleScopePolicy(rawAppCode string, rawScene string) (RoleScopePolicy, error) {
|
||||||
scene := strings.ToLower(strings.TrimSpace(rawScene))
|
scene := strings.ToLower(strings.TrimSpace(rawScene))
|
||||||
|
if scene == RoleScopeSceneManagerAddBDLeader {
|
||||||
|
scene = RoleScopeSceneManagerOperations
|
||||||
|
}
|
||||||
policy, ok := defaultRoleScopePolicies[scene]
|
policy, ok := defaultRoleScopePolicies[scene]
|
||||||
if !ok {
|
if !ok {
|
||||||
return RoleScopePolicy{}, xerr.New(xerr.InvalidArgument, "role scope scene is invalid")
|
return RoleScopePolicy{}, xerr.New(xerr.InvalidArgument, "role scope scene is invalid")
|
||||||
@ -68,7 +78,7 @@ func ResolveRoleScopePolicy(rawAppCode string, rawScene string) (RoleScopePolicy
|
|||||||
// EffectiveScope 只允许声明为 configurable 的策略接受经理个人扩区开关。
|
// EffectiveScope 只允许声明为 configurable 的策略接受经理个人扩区开关。
|
||||||
func (p RoleScopePolicy) EffectiveScope(regionExpansionEnabled bool) RoleScope {
|
func (p RoleScopePolicy) EffectiveScope(regionExpansionEnabled bool) RoleScope {
|
||||||
if p.RegionExpansionConfigurable && regionExpansionEnabled {
|
if p.RegionExpansionConfigurable && regionExpansionEnabled {
|
||||||
return RoleScopeRegion
|
return p.ExpandedScope
|
||||||
}
|
}
|
||||||
return p.BaseScope
|
return p.BaseScope
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ func TestResolveRoleScopePolicyKeepsAppDifferencesInRegistry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, testCase := range tests {
|
for _, testCase := range tests {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
policy, err := ResolveRoleScopePolicy(testCase.appCode, RoleScopeSceneManagerAddBDLeader)
|
policy, err := ResolveRoleScopePolicy(testCase.appCode, RoleScopeSceneManagerOperations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("resolve policy: %v", err)
|
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)
|
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" {
|
if err != nil || allowed || reason != "manager_capability_required" {
|
||||||
t.Fatalf("region expansion must default closed: allowed=%v reason=%q err=%v", allowed, reason, err)
|
t.Fatalf("region expansion must default closed: allowed=%v reason=%q err=%v", allowed, reason, err)
|
||||||
}
|
}
|
||||||
repository.SetManagerAddBDLeaderInRegion(101, true)
|
repository.SetManagerCrossRegion(101, true)
|
||||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerAddBDLeaderInRegion)
|
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerCrossRegion)
|
||||||
if err != nil || !allowed || reason != "" {
|
if err != nil || !allowed || reason != "" {
|
||||||
t.Fatalf("enabled region expansion should be allowed: allowed=%v reason=%q err=%v", allowed, reason, err)
|
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/idgen"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
userdomain "hyapp/services/user-service/internal/domain/user"
|
userdomain "hyapp/services/user-service/internal/domain/user"
|
||||||
|
hostservice "hyapp/services/user-service/internal/service/host"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -160,7 +161,7 @@ func (s *Service) statusResultAfterPersistence(ctx context.Context, command User
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateManagerUserBlock 创建经理封禁记录并把目标用户状态置为 banned。
|
// CreateManagerUserBlock 创建经理封禁记录并把目标用户状态置为 banned。
|
||||||
// 国家和 active 状态在 service 层校验,保证任何 gateway/H5 入口都不能跨国家或重复封禁非 active 用户。
|
// 目标范围由 App 策略与经理通用跨区开关共同决定,user-service 必须复验,不能只依赖 gateway 早拒绝。
|
||||||
func (s *Service) CreateManagerUserBlock(ctx context.Context, command ManagerUserBlockCommand) (ManagerUserBlock, UserStatusResult, error) {
|
func (s *Service) CreateManagerUserBlock(ctx context.Context, command ManagerUserBlockCommand) (ManagerUserBlock, UserStatusResult, error) {
|
||||||
if s.userRepository == nil || s.moderationRepository == nil {
|
if s.userRepository == nil || s.moderationRepository == nil {
|
||||||
return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "user repository is not configured")
|
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 {
|
if err != nil {
|
||||||
return ManagerUserBlock{}, UserStatusResult{}, err
|
return ManagerUserBlock{}, UserStatusResult{}, err
|
||||||
}
|
}
|
||||||
if manager.Country == "" || target.Country == "" || !strings.EqualFold(manager.Country, target.Country) {
|
policy, err := hostservice.ResolveRoleScopePolicy(appcode.FromContext(ctx), hostservice.RoleScopeSceneManagerOperations)
|
||||||
return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.PermissionDenied, "target user is outside manager country")
|
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 {
|
if target.Status != userdomain.StatusActive {
|
||||||
return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "target user is not active")
|
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
|
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) {
|
func (s *Service) ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]ManagerUserBlock, error) {
|
||||||
if s.moderationRepository == nil {
|
if s.moderationRepository == nil {
|
||||||
return nil, xerr.New(xerr.Unavailable, "moderation repository is not configured")
|
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) {
|
func TestAdminBanUserAcceptsPermanentBanAndRunsRealtimeSideEffectsOnce(t *testing.T) {
|
||||||
repository := &fakeModerationRepository{
|
repository := &fakeModerationRepository{
|
||||||
user: userdomain.User{AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned},
|
user: userdomain.User{AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned},
|
||||||
@ -232,6 +269,7 @@ func TestBatchGetUserAdminProfilesDeduplicatesAndLimitsInput(t *testing.T) {
|
|||||||
|
|
||||||
type fakeModerationRepository struct {
|
type fakeModerationRepository struct {
|
||||||
user userdomain.User
|
user userdomain.User
|
||||||
|
users map[int64]userdomain.User
|
||||||
inviteReferrer userdomain.User
|
inviteReferrer userdomain.User
|
||||||
sessionIDs []string
|
sessionIDs []string
|
||||||
countrySessionIDs []string
|
countrySessionIDs []string
|
||||||
@ -250,6 +288,7 @@ type fakeModerationRepository struct {
|
|||||||
adminBanPersistence AdminUserBanPersistenceResult
|
adminBanPersistence AdminUserBanPersistenceResult
|
||||||
adminBanRelease AdminUserBanRelease
|
adminBanRelease AdminUserBanRelease
|
||||||
adminBanExpirations []AdminUserBanRelease
|
adminBanExpirations []AdminUserBanRelease
|
||||||
|
managerCrossRegionEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *fakeModerationRepository) SetUserStatus(_ context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error) {
|
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
|
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) {
|
func (r *fakeModerationRepository) GetInviteAttribution(context.Context, int64) (invitedomain.Attribution, error) {
|
||||||
return invitedomain.Attribution{}, nil
|
return invitedomain.Attribution{}, nil
|
||||||
}
|
}
|
||||||
@ -317,7 +360,10 @@ func (r *fakeModerationRepository) ExpireAdminUserBans(context.Context, int64, i
|
|||||||
return r.adminBanExpirations, nil
|
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
|
return r.user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -168,6 +168,7 @@ type DeviceRepository interface {
|
|||||||
// ModerationRepository 持有用户状态和认证 session 的同库事务。
|
// ModerationRepository 持有用户状态和认证 session 的同库事务。
|
||||||
type ModerationRepository interface {
|
type ModerationRepository interface {
|
||||||
SetUserStatus(ctx context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error)
|
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)
|
CreateManagerUserBlock(ctx context.Context, command ManagerUserBlockCommand, target userdomain.User) (ManagerUserBlock, error)
|
||||||
ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]ManagerUserBlock, error)
|
ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]ManagerUserBlock, error)
|
||||||
ReleaseManagerUserBlock(ctx context.Context, command ReleaseManagerUserBlockCommand) (ManagerUserBlockRelease, 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"
|
columnSQL = "can_update_user_level = 1"
|
||||||
case hostservice.CapabilityManagerAddBDLeader:
|
case hostservice.CapabilityManagerAddBDLeader:
|
||||||
columnSQL = "can_add_bd_leader = 1"
|
columnSQL = "can_add_bd_leader = 1"
|
||||||
case hostservice.CapabilityManagerAddBDLeaderInRegion:
|
case hostservice.CapabilityManagerCrossRegion, hostservice.CapabilityManagerAddBDLeaderInRegion:
|
||||||
columnSQL = "can_add_bd_leader_in_region = 1"
|
columnSQL = "can_manage_cross_region = 1"
|
||||||
case hostservice.CapabilityManagerAddAdmin:
|
case hostservice.CapabilityManagerAddAdmin:
|
||||||
columnSQL = "can_add_admin = 1"
|
columnSQL = "can_add_admin = 1"
|
||||||
case hostservice.CapabilityManagerAddSuperadmin:
|
case hostservice.CapabilityManagerAddSuperadmin:
|
||||||
|
|||||||
@ -13,6 +13,21 @@ import (
|
|||||||
userservice "hyapp/services/user-service/internal/service/user"
|
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。
|
// SetUserStatus 原子更新用户主状态,并在非 active 状态下吊销该用户所有 active refresh session。
|
||||||
func (r *Repository) SetUserStatus(ctx context.Context, command userservice.UserStatusCommand) (userservice.UserStatusPersistenceResult, error) {
|
func (r *Repository) SetUserStatus(ctx context.Context, command userservice.UserStatusCommand) (userservice.UserStatusPersistenceResult, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
|
|||||||
@ -829,7 +829,7 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
|||||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
INSERT INTO manager_profiles (
|
INSERT INTO manager_profiles (
|
||||||
user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
|
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
|
created_by_admin_id, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
@ -841,7 +841,7 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
|||||||
can_grant_vip = VALUES(can_grant_vip),
|
can_grant_vip = VALUES(can_grant_vip),
|
||||||
can_update_user_level = VALUES(can_update_user_level),
|
can_update_user_level = VALUES(can_update_user_level),
|
||||||
can_add_bd_leader = VALUES(can_add_bd_leader),
|
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_admin = VALUES(can_add_admin),
|
||||||
can_add_superadmin = VALUES(can_add_superadmin),
|
can_add_superadmin = VALUES(can_add_superadmin),
|
||||||
can_block_user = VALUES(can_block_user),
|
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)
|
updated_at_ms = VALUES(updated_at_ms)
|
||||||
`, profile.UserID, profile.Status, profile.Contact, defaultTrue(profile.CanGrantAvatarFrame),
|
`, profile.UserID, profile.Status, profile.Contact, defaultTrue(profile.CanGrantAvatarFrame),
|
||||||
defaultTrue(profile.CanGrantVehicle), defaultTrue(profile.CanGrantBadge), defaultTrue(profile.CanGrantVIP),
|
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),
|
defaultTrue(profile.CanAddSuperadmin), defaultTrue(profile.CanBlockUser), defaultTrue(profile.CanTransferCountry),
|
||||||
profile.CreatedByAdminID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
profile.CreatedByAdminID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||||||
if err != nil {
|
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()
|
r.t.Helper()
|
||||||
|
|
||||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
UPDATE manager_profiles
|
UPDATE manager_profiles
|
||||||
SET can_add_bd_leader_in_region = ?
|
SET can_manage_cross_region = ?
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
`, enabled, userID)
|
`, enabled, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -550,6 +550,7 @@ func (s *Server) GetRoleScopePolicy(ctx context.Context, req *userv1.GetRoleScop
|
|||||||
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||||
Scene: policy.Scene,
|
Scene: policy.Scene,
|
||||||
BaseScope: string(policy.BaseScope),
|
BaseScope: string(policy.BaseScope),
|
||||||
|
ExpandedScope: string(policy.ExpandedScope),
|
||||||
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
||||||
}}, nil
|
}}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user