feat: add manager BD Leader scope policy
This commit is contained in:
parent
1fe23326c6
commit
2444a7a108
File diff suppressed because it is too large
Load Diff
@ -482,6 +482,22 @@ message CheckBusinessCapabilityResponse {
|
||||
string reason = 2;
|
||||
}
|
||||
|
||||
// RoleScopePolicy 是 App 级角色拓展边界;调用方只消费 scope,不直接判断具体 app_code。
|
||||
message RoleScopePolicy {
|
||||
string scene = 1;
|
||||
string base_scope = 2;
|
||||
bool region_expansion_configurable = 3;
|
||||
}
|
||||
|
||||
message GetRoleScopePolicyRequest {
|
||||
RequestMeta meta = 1;
|
||||
string scene = 2;
|
||||
}
|
||||
|
||||
message GetRoleScopePolicyResponse {
|
||||
RoleScopePolicy policy = 1;
|
||||
}
|
||||
|
||||
message GetAgencyMembersRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 agency_id = 2;
|
||||
@ -709,6 +725,7 @@ service UserHostService {
|
||||
rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse);
|
||||
rpc GetAgency(GetAgencyRequest) returns (GetAgencyResponse);
|
||||
rpc CheckBusinessCapability(CheckBusinessCapabilityRequest) returns (CheckBusinessCapabilityResponse);
|
||||
rpc GetRoleScopePolicy(GetRoleScopePolicyRequest) returns (GetRoleScopePolicyResponse);
|
||||
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
|
||||
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
|
||||
rpc UpdateAgencyProfile(UpdateAgencyProfileRequest) returns (UpdateAgencyProfileResponse);
|
||||
|
||||
@ -45,6 +45,7 @@ const (
|
||||
UserHostService_GetUserRoleSummary_FullMethodName = "/hyapp.user.v1.UserHostService/GetUserRoleSummary"
|
||||
UserHostService_GetAgency_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgency"
|
||||
UserHostService_CheckBusinessCapability_FullMethodName = "/hyapp.user.v1.UserHostService/CheckBusinessCapability"
|
||||
UserHostService_GetRoleScopePolicy_FullMethodName = "/hyapp.user.v1.UserHostService/GetRoleScopePolicy"
|
||||
UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers"
|
||||
UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications"
|
||||
UserHostService_UpdateAgencyProfile_FullMethodName = "/hyapp.user.v1.UserHostService/UpdateAgencyProfile"
|
||||
@ -83,6 +84,7 @@ type UserHostServiceClient interface {
|
||||
GetUserRoleSummary(ctx context.Context, in *GetUserRoleSummaryRequest, opts ...grpc.CallOption) (*GetUserRoleSummaryResponse, error)
|
||||
GetAgency(ctx context.Context, in *GetAgencyRequest, opts ...grpc.CallOption) (*GetAgencyResponse, error)
|
||||
CheckBusinessCapability(ctx context.Context, in *CheckBusinessCapabilityRequest, opts ...grpc.CallOption) (*CheckBusinessCapabilityResponse, error)
|
||||
GetRoleScopePolicy(ctx context.Context, in *GetRoleScopePolicyRequest, opts ...grpc.CallOption) (*GetRoleScopePolicyResponse, error)
|
||||
GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error)
|
||||
GetAgencyApplications(ctx context.Context, in *GetAgencyApplicationsRequest, opts ...grpc.CallOption) (*GetAgencyApplicationsResponse, error)
|
||||
UpdateAgencyProfile(ctx context.Context, in *UpdateAgencyProfileRequest, opts ...grpc.CallOption) (*UpdateAgencyProfileResponse, error)
|
||||
@ -356,6 +358,16 @@ func (c *userHostServiceClient) CheckBusinessCapability(ctx context.Context, in
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) GetRoleScopePolicy(ctx context.Context, in *GetRoleScopePolicyRequest, opts ...grpc.CallOption) (*GetRoleScopePolicyResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetRoleScopePolicyResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_GetRoleScopePolicy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetAgencyMembersResponse)
|
||||
@ -419,6 +431,7 @@ type UserHostServiceServer interface {
|
||||
GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error)
|
||||
GetAgency(context.Context, *GetAgencyRequest) (*GetAgencyResponse, error)
|
||||
CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error)
|
||||
GetRoleScopePolicy(context.Context, *GetRoleScopePolicyRequest) (*GetRoleScopePolicyResponse, error)
|
||||
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
|
||||
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
|
||||
UpdateAgencyProfile(context.Context, *UpdateAgencyProfileRequest) (*UpdateAgencyProfileResponse, error)
|
||||
@ -510,6 +523,9 @@ func (UnimplementedUserHostServiceServer) GetAgency(context.Context, *GetAgencyR
|
||||
func (UnimplementedUserHostServiceServer) CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckBusinessCapability not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetRoleScopePolicy(context.Context, *GetRoleScopePolicyRequest) (*GetRoleScopePolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoleScopePolicy not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyMembers not implemented")
|
||||
}
|
||||
@ -1008,6 +1024,24 @@ func _UserHostService_CheckBusinessCapability_Handler(srv interface{}, ctx conte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_GetRoleScopePolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetRoleScopePolicyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).GetRoleScopePolicy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_GetRoleScopePolicy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).GetRoleScopePolicy(ctx, req.(*GetRoleScopePolicyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_GetAgencyMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetAgencyMembersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1173,6 +1207,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "CheckBusinessCapability",
|
||||
Handler: _UserHostService_CheckBusinessCapability_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRoleScopePolicy",
|
||||
Handler: _UserHostService_GetRoleScopePolicy_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetAgencyMembers",
|
||||
Handler: _UserHostService_GetAgencyMembers_Handler,
|
||||
|
||||
@ -79,7 +79,7 @@ graph LR
|
||||
|
||||
当前 `user_id` 是内部稳定主键,后台仍必须在所有 host/Agency/BD 查询、幂等和审计 detail 中带上 `app_code`。如果某些现有表还使用全局 `PRIMARY KEY(user_id)`、`PRIMARY KEY(command_id)` 或 `PRIMARY KEY(event_id)`,这只能说明当前实现暂时要求这些 ID 全局唯一;不能据此省略 `app_code`,也不能在多 App 写入前声称同一个 `command_id` 可以跨 App 复用。
|
||||
|
||||
经理中心创建 BD Leader 时,`app_code` 必须来自已验证的 App token,不接受客户端自报租户。Huwaa 的 `manager_add_bd_leader` 搜索和创建可跨国家/区域;其他 App 仍要求经理和目标用户同国家。两类路径都必须保留 active Manager 和 `manager_add_bd_leader` 能力校验。
|
||||
经理中心创建 BD Leader 时,`app_code` 必须来自已验证的 App token,不接受客户端自报租户。App 差异统一由 user-service 的角色范围策略解析:Huwaa 的基础范围是 `global`;其他 App 的基础范围是 `country`,且只有策略声明可配置时,经理的区域邀请开关才能把有效范围扩展为 `region`。搜索和创建入口只消费 `country|region|global` 策略结果,不直接判断具体 App;两类路径都必须保留 active Manager 和 `manager_add_bd_leader` 能力校验。
|
||||
|
||||
```sql
|
||||
admin_operation_logs(
|
||||
|
||||
13
scripts/mysql/064_manager_bd_leader_region_scope.sql
Normal file
13
scripts/mysql/064_manager_bd_leader_region_scope.sql
Normal file
@ -0,0 +1,13 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE hyapp_user;
|
||||
|
||||
-- 区域邀请是经理个人的显式扩权;默认 0 保证现有和新建经理继续使用 App 基础范围。
|
||||
SET @ddl := 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,
|
||||
'ALTER TABLE manager_profiles ADD COLUMN can_add_bd_leader_in_region TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否允许把 BD Leader 邀请范围从本国家扩展到经理所属区域'' AFTER can_add_bd_leader',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -2,6 +2,7 @@ package userclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
@ -41,6 +42,7 @@ type Client interface {
|
||||
ReviewCoinSellerSubApplication(ctx context.Context, req ReviewCoinSellerSubApplicationRequest) (*CoinSellerSubApplicationReviewResult, error)
|
||||
GetCoinSellerProfile(ctx context.Context, req GetCoinSellerProfileRequest) (*CoinSellerProfile, error)
|
||||
GetUserRoleSummary(ctx context.Context, req GetUserRoleSummaryRequest) (*UserRoleSummary, error)
|
||||
GetRoleScopePolicy(ctx context.Context, req GetRoleScopePolicyRequest) (*RoleScopePolicy, error)
|
||||
CreateAgency(ctx context.Context, req CreateAgencyRequest) (*CreateAgencyResult, error)
|
||||
AdminAddAgencyHost(ctx context.Context, req AdminAddAgencyHostRequest) (*CreateAgencyResult, error)
|
||||
CloseAgency(ctx context.Context, req CloseAgencyRequest) (*Agency, error)
|
||||
@ -111,6 +113,18 @@ type ChangeUserCountryResult struct {
|
||||
RegionChanged bool `json:"regionChanged"`
|
||||
}
|
||||
|
||||
type GetRoleScopePolicyRequest struct {
|
||||
RequestID string
|
||||
Caller string
|
||||
Scene string
|
||||
}
|
||||
|
||||
type RoleScopePolicy struct {
|
||||
Scene string `json:"scene"`
|
||||
BaseScope string `json:"baseScope"`
|
||||
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
||||
}
|
||||
|
||||
type SetUserStatusResult struct {
|
||||
User *User `json:"user"`
|
||||
RevokedSessionCount int64 `json:"revokedSessionCount"`
|
||||
@ -817,6 +831,25 @@ func (c *GRPCClient) CreateBDLeader(ctx context.Context, req CreateBDLeaderReque
|
||||
return fromProtoBDProfile(resp.GetBdProfile()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetRoleScopePolicy(ctx context.Context, req GetRoleScopePolicyRequest) (*RoleScopePolicy, error) {
|
||||
resp, err := c.hostClient.GetRoleScopePolicy(ctx, &userv1.GetRoleScopePolicyRequest{
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
Scene: req.Scene,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy := resp.GetPolicy()
|
||||
if policy == nil {
|
||||
return nil, fmt.Errorf("role scope policy missing")
|
||||
}
|
||||
return &RoleScopePolicy{
|
||||
Scene: policy.GetScene(),
|
||||
BaseScope: policy.GetBaseScope(),
|
||||
RegionExpansionConfigurable: policy.GetRegionExpansionConfigurable(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) CreateBD(ctx context.Context, req CreateBDRequest) (*BDProfile, error) {
|
||||
resp, err := c.hostAdminClient.CreateBD(ctx, &userv1.CreateBDRequest{
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
|
||||
@ -68,12 +68,15 @@ func (h *Handler) ListManagers(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListManagers(c.Request.Context(), query)
|
||||
items, total, policy, err := h.service.ListManagers(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取经理列表失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
|
||||
response.OK(c, ManagerPageResponse{
|
||||
Items: items, Page: query.Page, PageSize: query.PageSize, Total: total,
|
||||
BDLeaderInviteScopePolicy: policy,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateManager(c *gin.Context) {
|
||||
|
||||
@ -68,28 +68,45 @@ type UserIdentity struct {
|
||||
}
|
||||
|
||||
type ManagerListItem struct {
|
||||
UserID int64 `json:"userId,string"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
RegionName string `json:"regionName"`
|
||||
Status string `json:"status"`
|
||||
Contact string `json:"contact"`
|
||||
CanGrantAvatarFrame bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge bool `json:"canGrantBadge"`
|
||||
CanGrantVIP bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader bool `json:"canAddBdLeader"`
|
||||
CanAddAdmin bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser bool `json:"canBlockUser"`
|
||||
CanTransferCountry bool `json:"canTransferUserCountry"`
|
||||
BDLeaderCount int64 `json:"bdLeaderCount"`
|
||||
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
UserID int64 `json:"userId,string"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
RegionName string `json:"regionName"`
|
||||
Status string `json:"status"`
|
||||
Contact string `json:"contact"`
|
||||
CanGrantAvatarFrame bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge bool `json:"canGrantBadge"`
|
||||
CanGrantVIP bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader bool `json:"canAddBdLeader"`
|
||||
CanAddBDLeaderInRegion bool `json:"canAddBdLeaderInRegion"`
|
||||
BDLeaderInviteBaseScope string `json:"bdLeaderInviteBaseScope"`
|
||||
BDLeaderInviteEffectiveScope string `json:"bdLeaderInviteEffectiveScope"`
|
||||
BDLeaderRegionExpansionConfigurable bool `json:"bdLeaderRegionExpansionConfigurable"`
|
||||
CanAddAdmin bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser bool `json:"canBlockUser"`
|
||||
CanTransferCountry bool `json:"canTransferUserCountry"`
|
||||
BDLeaderCount int64 `json:"bdLeaderCount"`
|
||||
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
type ManagerRoleScopePolicy struct {
|
||||
BaseScope string `json:"baseScope"`
|
||||
RegionExpansionConfigurable bool `json:"regionExpansionConfigurable"`
|
||||
}
|
||||
|
||||
type ManagerPageResponse struct {
|
||||
Items []*ManagerListItem `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int64 `json:"total"`
|
||||
BDLeaderInviteScopePolicy ManagerRoleScopePolicy `json:"bdLeaderInviteScopePolicy"`
|
||||
}
|
||||
|
||||
type CoinSellerSalaryRateTier struct {
|
||||
@ -645,9 +662,9 @@ func (r *Reader) CreateManagerProfile(ctx context.Context, userID int64, actorID
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO manager_profiles (
|
||||
app_code, user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
|
||||
can_grant_vip, can_update_user_level, can_add_bd_leader, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
|
||||
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,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = 'active',
|
||||
contact_info = VALUES(contact_info),
|
||||
@ -657,13 +674,14 @@ func (r *Reader) CreateManagerProfile(ctx context.Context, userID int64, actorID
|
||||
can_grant_vip = VALUES(can_grant_vip),
|
||||
can_update_user_level = VALUES(can_update_user_level),
|
||||
can_add_bd_leader = VALUES(can_add_bd_leader),
|
||||
can_add_bd_leader_in_region = VALUES(can_add_bd_leader_in_region),
|
||||
can_add_admin = VALUES(can_add_admin),
|
||||
can_add_superadmin = VALUES(can_add_superadmin),
|
||||
can_block_user = VALUES(can_block_user),
|
||||
can_transfer_user_country = VALUES(can_transfer_user_country),
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, appCode, userID, contact, permissions.CanGrantAvatarFrame, permissions.CanGrantVehicle,
|
||||
permissions.CanGrantBadge, permissions.CanGrantVIP, permissions.CanUpdateUserLevel, permissions.CanAddBDLeader, permissions.CanAddAdmin,
|
||||
permissions.CanGrantBadge, permissions.CanGrantVIP, permissions.CanUpdateUserLevel, permissions.CanAddBDLeader, permissions.CanAddBDLeaderInRegion, permissions.CanAddAdmin,
|
||||
permissions.CanAddSuperadmin, permissions.CanBlockUser, permissions.CanTransferCountry, actorID, nowMs, nowMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -771,14 +789,14 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
|
||||
COALESCE(manager.avatar, ''), `+userCountryRegionIDSQL("manager_region")+`, `+userCountryRegionNameSQL("manager_region")+`,
|
||||
mp.status, COALESCE(mp.contact_info, ''),
|
||||
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_grant_badge, mp.can_grant_vip, mp.can_update_user_level,
|
||||
mp.can_add_bd_leader, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
|
||||
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,
|
||||
COUNT(DISTINCT bl.user_id), COALESCE(MAX(bl.created_at_ms), 0),
|
||||
mp.created_at_ms, mp.updated_at_ms
|
||||
%s
|
||||
GROUP BY mp.user_id, manager.current_display_user_id, manager.username, manager.avatar,
|
||||
manager_region.region_id, manager_region.name, mp.status, mp.contact_info,
|
||||
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_grant_badge, mp.can_grant_vip, mp.can_update_user_level,
|
||||
mp.can_add_bd_leader, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
|
||||
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.created_at_ms, mp.updated_at_ms
|
||||
ORDER BY mp.updated_at_ms DESC, mp.user_id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
@ -806,6 +824,7 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
|
||||
&item.CanGrantVIP,
|
||||
&item.CanUpdateUserLevel,
|
||||
&item.CanAddBDLeader,
|
||||
&item.CanAddBDLeaderInRegion,
|
||||
&item.CanAddAdmin,
|
||||
&item.CanAddSuperadmin,
|
||||
&item.CanBlockUser,
|
||||
|
||||
@ -29,7 +29,7 @@ func TestUpdateManagerProfilePersistsStatus(t *testing.T) {
|
||||
mock.ExpectQuery(`(?s)SELECT mp\.user_id, COALESCE\(manager\.current_display_user_id, ''\).*FROM manager_profiles mp`).
|
||||
WillReturnRows(managerListRows().
|
||||
AddRow(int64(1003), "1003", "manager", "avatar.png", int64(7), "西亚区", "disabled", "+63",
|
||||
true, true, true, true, true, true, true, true, true, true,
|
||||
true, true, true, true, true, true, false, true, true, true, true,
|
||||
int64(0), int64(0), int64(1710000000000), int64(1710000001000)))
|
||||
|
||||
item, err := reader.UpdateManagerProfile(ctx, 1003, 7, updateManagerRequest{Status: &status})
|
||||
@ -75,6 +75,7 @@ func managerListRows() *sqlmock.Rows {
|
||||
"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",
|
||||
|
||||
@ -69,33 +69,35 @@ type createBDLeaderRequest struct {
|
||||
}
|
||||
|
||||
type createManagerRequest struct {
|
||||
Contact string `json:"contact"`
|
||||
TargetUserID flexibleInt64 `json:"targetUserId" binding:"required"`
|
||||
CanGrantAvatarFrame *bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle *bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge *bool `json:"canGrantBadge"`
|
||||
CanGrantVIP *bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser *bool `json:"canBlockUser"`
|
||||
CanTransferCountry *bool `json:"canTransferUserCountry"`
|
||||
Contact string `json:"contact"`
|
||||
TargetUserID flexibleInt64 `json:"targetUserId" binding:"required"`
|
||||
CanGrantAvatarFrame *bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle *bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge *bool `json:"canGrantBadge"`
|
||||
CanGrantVIP *bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||
CanAddBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser *bool `json:"canBlockUser"`
|
||||
CanTransferCountry *bool `json:"canTransferUserCountry"`
|
||||
}
|
||||
|
||||
type updateManagerRequest struct {
|
||||
Contact *string `json:"contact"`
|
||||
Status *string `json:"status"`
|
||||
CanGrantAvatarFrame *bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle *bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge *bool `json:"canGrantBadge"`
|
||||
CanGrantVIP *bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser *bool `json:"canBlockUser"`
|
||||
CanTransferCountry *bool `json:"canTransferUserCountry"`
|
||||
Contact *string `json:"contact"`
|
||||
Status *string `json:"status"`
|
||||
CanGrantAvatarFrame *bool `json:"canGrantAvatarFrame"`
|
||||
CanGrantVehicle *bool `json:"canGrantVehicle"`
|
||||
CanGrantBadge *bool `json:"canGrantBadge"`
|
||||
CanGrantVIP *bool `json:"canGrantVip"`
|
||||
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
|
||||
CanAddBDLeader *bool `json:"canAddBdLeader"`
|
||||
CanAddBDLeaderInRegion *bool `json:"canAddBdLeaderInRegion"`
|
||||
CanAddAdmin *bool `json:"canAddAdmin"`
|
||||
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
|
||||
CanBlockUser *bool `json:"canBlockUser"`
|
||||
CanTransferCountry *bool `json:"canTransferUserCountry"`
|
||||
}
|
||||
|
||||
func (r updateManagerRequest) appendStatusAssignment(assignments *[]string, args *[]any) error {
|
||||
@ -116,30 +118,32 @@ func (r updateManagerRequest) appendStatusAssignment(assignments *[]string, args
|
||||
}
|
||||
|
||||
type managerPermissions struct {
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanAddBDLeaderInRegion bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
}
|
||||
|
||||
func (r createManagerRequest) permissionsWithDefault() managerPermissions {
|
||||
return managerPermissions{
|
||||
CanGrantAvatarFrame: boolDefaultTrue(r.CanGrantAvatarFrame),
|
||||
CanGrantVehicle: boolDefaultTrue(r.CanGrantVehicle),
|
||||
CanGrantBadge: boolDefaultTrue(r.CanGrantBadge),
|
||||
CanGrantVIP: boolDefaultTrue(r.CanGrantVIP),
|
||||
CanUpdateUserLevel: boolDefaultTrue(r.CanUpdateUserLevel),
|
||||
CanAddBDLeader: boolDefaultTrue(r.CanAddBDLeader),
|
||||
CanAddAdmin: boolDefaultTrue(r.CanAddAdmin),
|
||||
CanAddSuperadmin: boolDefaultTrue(r.CanAddSuperadmin),
|
||||
CanBlockUser: boolDefaultTrue(r.CanBlockUser),
|
||||
CanTransferCountry: boolDefaultTrue(r.CanTransferCountry),
|
||||
CanGrantAvatarFrame: boolDefaultTrue(r.CanGrantAvatarFrame),
|
||||
CanGrantVehicle: boolDefaultTrue(r.CanGrantVehicle),
|
||||
CanGrantBadge: boolDefaultTrue(r.CanGrantBadge),
|
||||
CanGrantVIP: boolDefaultTrue(r.CanGrantVIP),
|
||||
CanUpdateUserLevel: boolDefaultTrue(r.CanUpdateUserLevel),
|
||||
CanAddBDLeader: boolDefaultTrue(r.CanAddBDLeader),
|
||||
CanAddBDLeaderInRegion: boolDefaultFalse(r.CanAddBDLeaderInRegion),
|
||||
CanAddAdmin: boolDefaultTrue(r.CanAddAdmin),
|
||||
CanAddSuperadmin: boolDefaultTrue(r.CanAddSuperadmin),
|
||||
CanBlockUser: boolDefaultTrue(r.CanBlockUser),
|
||||
CanTransferCountry: boolDefaultTrue(r.CanTransferCountry),
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,6 +154,7 @@ func (r updateManagerRequest) appendPermissionAssignments(assignments *[]string,
|
||||
appendBoolAssignment(assignments, args, "can_grant_vip", r.CanGrantVIP)
|
||||
appendBoolAssignment(assignments, args, "can_update_user_level", r.CanUpdateUserLevel)
|
||||
appendBoolAssignment(assignments, args, "can_add_bd_leader", r.CanAddBDLeader)
|
||||
appendBoolAssignment(assignments, args, "can_add_bd_leader_in_region", r.CanAddBDLeaderInRegion)
|
||||
appendBoolAssignment(assignments, args, "can_add_admin", r.CanAddAdmin)
|
||||
appendBoolAssignment(assignments, args, "can_add_superadmin", r.CanAddSuperadmin)
|
||||
appendBoolAssignment(assignments, args, "can_block_user", r.CanBlockUser)
|
||||
@ -168,6 +173,10 @@ func boolDefaultTrue(value *bool) bool {
|
||||
return value == nil || *value
|
||||
}
|
||||
|
||||
func boolDefaultFalse(value *bool) bool {
|
||||
return value != nil && *value
|
||||
}
|
||||
|
||||
type createBDRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
TargetUserID flexibleInt64 `json:"targetUserId" binding:"required"`
|
||||
|
||||
@ -29,3 +29,21 @@ func TestFlexibleInt64AcceptsStringAndNumber(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerRegionExpansionDefaultsClosedAndPreservesExplicitEnable(t *testing.T) {
|
||||
var omitted createManagerRequest
|
||||
if err := json.Unmarshal([]byte(`{"targetUserId":"1003"}`), &omitted); err != nil {
|
||||
t.Fatalf("unmarshal omitted permission: %v", err)
|
||||
}
|
||||
if omitted.permissionsWithDefault().CanAddBDLeaderInRegion {
|
||||
t.Fatalf("region expansion must default closed")
|
||||
}
|
||||
|
||||
var enabled createManagerRequest
|
||||
if err := json.Unmarshal([]byte(`{"targetUserId":"1003","canAddBdLeaderInRegion":true}`), &enabled); err != nil {
|
||||
t.Fatalf("unmarshal enabled permission: %v", err)
|
||||
}
|
||||
if !enabled.permissionsWithDefault().CanAddBDLeaderInRegion {
|
||||
t.Fatalf("explicit region expansion must be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ const (
|
||||
sortByTotalRechargeUSDT = "total_recharge_usdt"
|
||||
bdLeaderPositionAliasMaxRunes = 64
|
||||
hostOrgInternalUserIDMinDigits = 15
|
||||
managerAddBDLeaderScopeScene = "manager_add_bd_leader"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@ -56,22 +57,87 @@ func (s *Service) ListAgencies(ctx context.Context, query listQuery) ([]*usercli
|
||||
return s.reader.ListAgencies(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListManagers(ctx context.Context, query listQuery) ([]*ManagerListItem, int64, error) {
|
||||
func (s *Service) ListManagers(ctx context.Context, query listQuery) ([]*ManagerListItem, int64, ManagerRoleScopePolicy, error) {
|
||||
query = normalizeListQuery(query)
|
||||
return s.reader.ListManagers(ctx, query)
|
||||
items, total, err := s.reader.ListManagers(ctx, query)
|
||||
if err != nil {
|
||||
return nil, 0, ManagerRoleScopePolicy{}, err
|
||||
}
|
||||
policy, err := s.managerRoleScopePolicy(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, ManagerRoleScopePolicy{}, err
|
||||
}
|
||||
decorateManagerScopePolicy(items, policy)
|
||||
return items, total, ManagerRoleScopePolicy{
|
||||
BaseScope: policy.BaseScope,
|
||||
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateManager(ctx context.Context, actorID int64, requestID string, req createManagerRequest) (*ManagerListItem, error) {
|
||||
policy, err := s.managerRoleScopePolicy(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.CanAddBDLeaderInRegion = normalizedManagerRegionPermission(req.CanAddBDLeaderInRegion, policy.RegionExpansionConfigurable)
|
||||
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 经理身份只落 manager_profiles 表;权限开关同时写入 manager_profiles,供 H5 写入口逐项校验。
|
||||
return s.reader.CreateManagerProfile(ctx, targetUserID, actorID, req)
|
||||
item, err := s.reader.CreateManagerProfile(ctx, targetUserID, actorID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decorateManagerScopePolicy([]*ManagerListItem{item}, policy)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateManager(ctx context.Context, actorID int64, userID int64, req updateManagerRequest) (*ManagerListItem, error) {
|
||||
return s.reader.UpdateManagerProfile(ctx, userID, actorID, req)
|
||||
policy, err := s.managerRoleScopePolicy(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.CanAddBDLeaderInRegion = normalizedManagerRegionPermission(req.CanAddBDLeaderInRegion, policy.RegionExpansionConfigurable)
|
||||
item, err := s.reader.UpdateManagerProfile(ctx, userID, actorID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decorateManagerScopePolicy([]*ManagerListItem{item}, policy)
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Service) managerRoleScopePolicy(ctx context.Context) (*userclient.RoleScopePolicy, error) {
|
||||
return s.userClient.GetRoleScopePolicy(ctx, userclient.GetRoleScopePolicyRequest{
|
||||
Caller: "hyapp-admin-server",
|
||||
Scene: managerAddBDLeaderScopeScene,
|
||||
})
|
||||
}
|
||||
|
||||
// normalizedManagerRegionPermission 让 App 基础策略决定开关是否可配置;global App 即使收到篡改请求也只保存 false。
|
||||
func normalizedManagerRegionPermission(value *bool, configurable bool) *bool {
|
||||
if configurable {
|
||||
return value
|
||||
}
|
||||
disabled := false
|
||||
return &disabled
|
||||
}
|
||||
|
||||
func decorateManagerScopePolicy(items []*ManagerListItem, policy *userclient.RoleScopePolicy) {
|
||||
if policy == nil {
|
||||
return
|
||||
}
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
item.BDLeaderInviteBaseScope = policy.BaseScope
|
||||
item.BDLeaderRegionExpansionConfigurable = policy.RegionExpansionConfigurable
|
||||
item.BDLeaderInviteEffectiveScope = policy.BaseScope
|
||||
if policy.RegionExpansionConfigurable && item.CanAddBDLeaderInRegion {
|
||||
item.BDLeaderInviteEffectiveScope = "region"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListHosts(ctx context.Context, query listQuery) ([]*userclient.HostProfile, int64, error) {
|
||||
|
||||
@ -129,6 +129,33 @@ func TestManagerListItemJSONHasNoAgencyOrParentBD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecorateManagerScopePolicyUsesPolicyMetadataWithoutAppBranches(t *testing.T) {
|
||||
items := []*ManagerListItem{{CanAddBDLeaderInRegion: true}}
|
||||
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
||||
BaseScope: "country",
|
||||
RegionExpansionConfigurable: true,
|
||||
})
|
||||
if items[0].BDLeaderInviteEffectiveScope != "region" || !items[0].BDLeaderRegionExpansionConfigurable {
|
||||
t.Fatalf("configurable policy decoration mismatch: %+v", items[0])
|
||||
}
|
||||
|
||||
items[0].CanAddBDLeaderInRegion = true
|
||||
decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
|
||||
BaseScope: "global",
|
||||
RegionExpansionConfigurable: false,
|
||||
})
|
||||
if items[0].BDLeaderInviteEffectiveScope != "global" || items[0].BDLeaderRegionExpansionConfigurable {
|
||||
t.Fatalf("global policy decoration mismatch: %+v", items[0])
|
||||
}
|
||||
if got := normalizedManagerRegionPermission(pointerBool(true), false); got == nil || *got {
|
||||
t.Fatalf("non-configurable app must persist region expansion as false")
|
||||
}
|
||||
}
|
||||
|
||||
func pointerBool(value bool) *bool {
|
||||
return &value
|
||||
}
|
||||
|
||||
// TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix 锁定线上真实 FullPath 带 /api/v1 前缀时,BD Leader 状态仍然写独立负责人表。
|
||||
func TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix(t *testing.T) {
|
||||
role, target := bdRoleFromStatusPath("/api/v1/admin/bd-leaders/:user_id/status")
|
||||
|
||||
@ -130,6 +130,7 @@ type UserHostClient interface {
|
||||
GetAgency(ctx context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error)
|
||||
UpdateAgencyProfile(ctx context.Context, req *userv1.UpdateAgencyProfileRequest) (*userv1.UpdateAgencyProfileResponse, error)
|
||||
CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error)
|
||||
GetRoleScopePolicy(ctx context.Context, req *userv1.GetRoleScopePolicyRequest) (*userv1.GetRoleScopePolicyResponse, error)
|
||||
GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error)
|
||||
GetAgencyApplications(ctx context.Context, req *userv1.GetAgencyApplicationsRequest) (*userv1.GetAgencyApplicationsResponse, error)
|
||||
ReviewAgencyApplication(ctx context.Context, req *userv1.ReviewAgencyApplicationRequest) (*userv1.ReviewAgencyApplicationResponse, error)
|
||||
@ -614,6 +615,10 @@ func (c *grpcUserHostClient) CheckBusinessCapability(ctx context.Context, req *u
|
||||
return c.client.CheckBusinessCapability(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) GetRoleScopePolicy(ctx context.Context, req *userv1.GetRoleScopePolicyRequest) (*userv1.GetRoleScopePolicyResponse, error) {
|
||||
return c.client.GetRoleScopePolicy(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error) {
|
||||
return c.client.GetAgencyMembers(ctx, req)
|
||||
}
|
||||
|
||||
@ -306,7 +306,7 @@ func TestSearchManagerUsersDefaultsSameCountryAndAllowsAllForCountryTransfer(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchManagerUsersAllowsCrossCountryOnlyForHuwaaBDLeaderAction(t *testing.T) {
|
||||
func TestSearchManagerUsersUsesResolvedGlobalPolicyForHuwaaBDLeaderAction(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
||||
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||
@ -318,7 +318,10 @@ func TestSearchManagerUsersAllowsCrossCountryOnlyForHuwaaBDLeaderAction(t *testi
|
||||
900002: {UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA"},
|
||||
},
|
||||
}
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, &fakeUserHostClient{}, profileClient)
|
||||
hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: "manager_add_bd_leader", BaseScope: "global", RegionExpansionConfigurable: false,
|
||||
}}}
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
||||
|
||||
bdLeaderRequest := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&action=add-bd-leader", nil)
|
||||
bdLeaderRequest.Header.Set("Authorization", "Bearer "+signGatewayTokenWithAppCode(t, "secret", 42, "huwaa"))
|
||||
@ -365,6 +368,59 @@ func TestSearchManagerUsersAllowsCrossCountryOnlyForHuwaaBDLeaderAction(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchManagerUsersAppliesManagerRegionExpansionWithoutCrossingRegion(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
|
||||
{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||
{UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||
{UserId: 900003, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
|
||||
}},
|
||||
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: "US", RegionId: 1001},
|
||||
900002: {UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
|
||||
900003: {UserId: 900003, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "MX", RegionId: 2002},
|
||||
},
|
||||
}
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
regionOpen bool
|
||||
wantVisible int
|
||||
}{
|
||||
{name: "closed keeps country", regionOpen: false, wantVisible: 1},
|
||||
{name: "open expands within region", regionOpen: true, wantVisible: 2},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{
|
||||
"manager_add_bd_leader_in_region": testCase.regionOpen,
|
||||
}}
|
||||
router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&scope=all&action=add-bd-leader", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-manager-region-search")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Data struct {
|
||||
Total int `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if envelope.Data.Total != testCase.wantVisible {
|
||||
t.Fatalf("visible total = %d, want %d", envelope.Data.Total, testCase.wantVisible)
|
||||
}
|
||||
if hostClient.sawCapability("manager_transfer_user_country") {
|
||||
t.Fatalf("scope=all must not override add-bd-leader policy")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantManagerResourceUsesServerControlledGrantShape(t *testing.T) {
|
||||
walletClient := &fakeWalletClient{}
|
||||
hostClient := &fakeUserHostClient{}
|
||||
@ -751,9 +807,10 @@ func TestCreateManagerBDLeaderRejectsCrossCountryTarget(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},
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
|
||||
}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, &fakeUserHostClient{}, hostAdminClient, profileClient)
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_add_bd_leader_in_region": false}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||
body := []byte(`{"command_id":"mgr-bdleader-cross","target_user_id":"900001"}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader(body))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
@ -768,13 +825,39 @@ func TestCreateManagerBDLeaderRejectsCrossCountryTarget(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateManagerBDLeaderAllowsCrossCountryTargetWhenRegionExpansionEnabled(t *testing.T) {
|
||||
hostAdminClient := &fakeUserHostAdminClient{}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
42: {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
|
||||
900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
|
||||
}}
|
||||
hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_add_bd_leader_in_region": true}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader([]byte(`{"command_id":"mgr-bdleader-region","target_user_id":"900001"}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-manager-bdleader-region")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("same-region BD Leader status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if req := hostAdminClient.lastCreateBDLeader; req == nil || req.GetTargetUserId() != 900001 {
|
||||
t.Fatalf("same-region target must reach CreateBDLeader: %+v", req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateManagerBDLeaderAllowsCrossCountryTargetForHuwaa(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, DisplayUserId: "163006", Country: "CA", RegionId: 2002},
|
||||
}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, &fakeUserHostClient{}, hostAdminClient, profileClient)
|
||||
hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: "manager_add_bd_leader", BaseScope: "global", RegionExpansionConfigurable: false,
|
||||
}}}
|
||||
router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
|
||||
body := []byte(`{"command_id":"huwaa-mgr-bdleader-cross","target_user_id":"900001"}`)
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader(body))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayTokenWithAppCode(t, "secret", 42, "huwaa"))
|
||||
|
||||
@ -24,6 +24,7 @@ const managerGrantBadgeCapability = "manager_grant_badge"
|
||||
const managerGrantVIPCapability = "manager_grant_vip"
|
||||
const managerUpdateUserLevelCapability = "manager_update_user_level"
|
||||
const managerAddBDLeaderCapability = "manager_add_bd_leader"
|
||||
const managerAddBDLeaderInRegionCapability = "manager_add_bd_leader_in_region"
|
||||
const managerAddAdminCapability = "manager_add_admin"
|
||||
const managerAddSuperadminCapability = "manager_add_superadmin"
|
||||
const managerBlockUserCapability = "manager_block_user"
|
||||
@ -33,7 +34,10 @@ const managerGrantDefaultDurationMS int64 = 7 * managerGrantDayMS
|
||||
const managerGrantVIPDurationMS int64 = 30 * managerGrantDayMS
|
||||
const managerGrantMaxResourceIDs = 20
|
||||
const managerGrantMaxVIPLevel int32 = 5
|
||||
const huwaaAppCode = "huwaa"
|
||||
|
||||
const managerRoleScopeCountry = "country"
|
||||
const managerRoleScopeRegion = "region"
|
||||
const managerRoleScopeGlobal = "global"
|
||||
|
||||
type managerGrantResourceData struct {
|
||||
ResourceID string `json:"resource_id"`
|
||||
@ -168,19 +172,24 @@ func (h *Handler) searchManagerUsers(writer http.ResponseWriter, request *http.R
|
||||
return
|
||||
}
|
||||
// 搜索先走已有 business lookup 保持短号/昵称解析一致,再用 BatchGetUsers 拿国家和等级投影做最终过滤。
|
||||
// 国家转移依赖显式 scope=all 和权限;Huwaa 的 add-bd-leader 是唯一无需该权限即可跨国家选人的角色例外。
|
||||
// BD Leader 的 App 差异来自 user-service 策略,handler 不直接判断 app_code。
|
||||
actor, ok := h.requireManagerCenterActor(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
scope := strings.ToLower(strings.TrimSpace(request.URL.Query().Get("scope")))
|
||||
requestedAllCountries := scope == "all"
|
||||
// Huwaa 的 BD Leader 拓展不受区域/国家边界限制;例外只绑定 add-bd-leader 场景,资源发放等高风险动作仍保持同国家。
|
||||
bdLeaderCrossRegion := huwaaBDLeaderSearchRegionUnrestricted(request)
|
||||
if requestedAllCountries && !bdLeaderCrossRegion && !h.requireManagerCapability(writer, request, managerTransferUserCountryCapability) {
|
||||
bdLeaderSearch := managerSearchScene(request) == managerAddBDLeaderCapability
|
||||
requestedAllCountries := scope == "all" && !bdLeaderSearch
|
||||
if requestedAllCountries && !h.requireManagerCapability(writer, request, managerTransferUserCountryCapability) {
|
||||
return
|
||||
}
|
||||
allCountries := requestedAllCountries || bdLeaderCrossRegion
|
||||
managerTargetScope := managerRoleScopeCountry
|
||||
if bdLeaderSearch {
|
||||
managerTargetScope, ok = h.managerBDLeaderEffectiveScope(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
@ -225,8 +234,9 @@ func (h *Handler) searchManagerUsers(writer http.ResponseWriter, request *http.R
|
||||
visibleUserIDs := make([]int64, 0, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
user := usersResp.GetUsers()[userID]
|
||||
// 默认搜索必须和写入口一样收敛到同国家;国家转移已单独校验权限,允许在这里跨国家挑选目标。
|
||||
if user == nil || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE || (!allCountries && !sameCountry(actor.GetCountry(), user.GetCountry())) {
|
||||
// 搜索与最终创建共用 scope evaluator,避免同一 App 的规则在两个 handler 分叉。
|
||||
if user == nil || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE ||
|
||||
(!requestedAllCountries && !managerTargetMatchesScope(managerTargetScope, actor, user)) {
|
||||
continue
|
||||
}
|
||||
item := managerUserFromProto(user, levels[userID])
|
||||
@ -772,13 +782,8 @@ func (h *Handler) createManagerBDLeader(writer http.ResponseWriter, request *htt
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
// 写入口必须重新读取 active 目标用户;Huwaa 只取消区域/国家匹配,经理身份和 add-bd-leader 能力仍在上方强制校验。
|
||||
var target *userv1.User
|
||||
if huwaaRoleRegionUnrestricted(request) {
|
||||
target, ok = h.requireActiveTarget(writer, request, targetUserID)
|
||||
} else {
|
||||
target, ok = h.requireSameCountryTargetWithActor(writer, request, actor, targetUserID)
|
||||
}
|
||||
// 写入口重新解析策略并读取 active 目标,不能信任搜索结果或客户端自报范围。
|
||||
target, ok := h.requireManagerBDLeaderTarget(writer, request, actor, targetUserID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -945,6 +950,39 @@ func (h *Handler) requireManagerCapability(writer http.ResponseWriter, request *
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Handler) managerBDLeaderEffectiveScope(writer http.ResponseWriter, request *http.Request) (string, bool) {
|
||||
resp, err := h.userHostClient.GetRoleScopePolicy(request.Context(), &userv1.GetRoleScopePolicyRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
Scene: managerAddBDLeaderCapability,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return "", false
|
||||
}
|
||||
policy := resp.GetPolicy()
|
||||
if policy == nil || !validManagerRoleScope(policy.GetBaseScope()) {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return "", false
|
||||
}
|
||||
scope := policy.GetBaseScope()
|
||||
if !policy.GetRegionExpansionConfigurable() {
|
||||
return scope, true
|
||||
}
|
||||
capability, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||
Capability: managerAddBDLeaderInRegionCapability,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return "", false
|
||||
}
|
||||
if capability.GetAllowed() {
|
||||
scope = managerRoleScopeRegion
|
||||
}
|
||||
return scope, true
|
||||
}
|
||||
|
||||
func (h *Handler) requireSameCountryTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
|
||||
actor, ok := h.requireManagerCenterActor(writer, request)
|
||||
if !ok {
|
||||
@ -965,6 +1003,22 @@ func (h *Handler) requireSameCountryTargetWithActor(writer http.ResponseWriter,
|
||||
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 {
|
||||
return nil, false
|
||||
}
|
||||
target, ok := h.requireActiveTarget(writer, request, targetUserID)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if !managerTargetMatchesScope(scope, actor, target) {
|
||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||
return nil, false
|
||||
}
|
||||
return target, true
|
||||
}
|
||||
|
||||
// requireActiveTarget 是经理写入口共同的目标用户真实性门禁;区域策略只能决定后续是否比较国家,不能绕过 active 用户校验。
|
||||
func (h *Handler) requireActiveTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
|
||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
@ -983,38 +1037,59 @@ func (h *Handler) requireActiveTarget(writer http.ResponseWriter, request *http.
|
||||
return target, true
|
||||
}
|
||||
|
||||
func huwaaRoleRegionUnrestricted(request *http.Request) bool {
|
||||
return request != nil && appcode.FromContext(request.Context()) == huwaaAppCode
|
||||
func validManagerRoleScope(scope string) bool {
|
||||
switch scope {
|
||||
case managerRoleScopeCountry, managerRoleScopeRegion, managerRoleScopeGlobal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func huwaaBDLeaderSearchRegionUnrestricted(request *http.Request) bool {
|
||||
return huwaaRoleRegionUnrestricted(request) && managerSearchScene(request) == managerAddBDLeaderCapability
|
||||
// managerTargetMatchesScope 是搜索和写入口共享的唯一匹配器。region 表示在同国家基础上扩到同区域,
|
||||
// 因而国家映射短暂异常时,打开扩区开关也不会反而失去原本可邀请的同国家用户。
|
||||
func managerTargetMatchesScope(scope string, actor *userv1.User, target *userv1.User) bool {
|
||||
if actor == nil || target == nil {
|
||||
return false
|
||||
}
|
||||
switch scope {
|
||||
case managerRoleScopeGlobal:
|
||||
return true
|
||||
case managerRoleScopeRegion:
|
||||
return sameCountry(actor.GetCountry(), target.GetCountry()) || sameRegion(actor.GetRegionId(), target.GetRegionId())
|
||||
case managerRoleScopeCountry:
|
||||
return sameCountry(actor.GetCountry(), target.GetCountry())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) managerPermissionOverview(request *http.Request) map[string]bool {
|
||||
permissions := map[string]bool{
|
||||
"can_grant_avatar_frame": false,
|
||||
"can_grant_vehicle": false,
|
||||
"can_grant_badge": false,
|
||||
"can_grant_vip": false,
|
||||
"can_update_user_level": false,
|
||||
"can_add_bd_leader": false,
|
||||
"can_add_admin": false,
|
||||
"can_add_superadmin": false,
|
||||
"can_block_user": false,
|
||||
"can_transfer_user_country": false,
|
||||
"can_grant_avatar_frame": false,
|
||||
"can_grant_vehicle": false,
|
||||
"can_grant_badge": false,
|
||||
"can_grant_vip": false,
|
||||
"can_update_user_level": false,
|
||||
"can_add_bd_leader": false,
|
||||
"can_add_bd_leader_in_region": false,
|
||||
"can_add_admin": false,
|
||||
"can_add_superadmin": false,
|
||||
"can_block_user": false,
|
||||
"can_transfer_user_country": false,
|
||||
}
|
||||
for key, capability := range map[string]string{
|
||||
"can_grant_avatar_frame": managerGrantAvatarFrameCapability,
|
||||
"can_grant_vehicle": managerGrantVehicleCapability,
|
||||
"can_grant_badge": managerGrantBadgeCapability,
|
||||
"can_grant_vip": managerGrantVIPCapability,
|
||||
"can_update_user_level": managerUpdateUserLevelCapability,
|
||||
"can_add_bd_leader": managerAddBDLeaderCapability,
|
||||
"can_add_admin": managerAddAdminCapability,
|
||||
"can_add_superadmin": managerAddSuperadminCapability,
|
||||
"can_block_user": managerBlockUserCapability,
|
||||
"can_transfer_user_country": managerTransferUserCountryCapability,
|
||||
"can_grant_avatar_frame": managerGrantAvatarFrameCapability,
|
||||
"can_grant_vehicle": managerGrantVehicleCapability,
|
||||
"can_grant_badge": managerGrantBadgeCapability,
|
||||
"can_grant_vip": managerGrantVIPCapability,
|
||||
"can_update_user_level": managerUpdateUserLevelCapability,
|
||||
"can_add_bd_leader": managerAddBDLeaderCapability,
|
||||
"can_add_bd_leader_in_region": managerAddBDLeaderInRegionCapability,
|
||||
"can_add_admin": managerAddAdminCapability,
|
||||
"can_add_superadmin": managerAddSuperadminCapability,
|
||||
"can_block_user": managerBlockUserCapability,
|
||||
"can_transfer_user_country": managerTransferUserCountryCapability,
|
||||
} {
|
||||
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
@ -1048,6 +1123,10 @@ func sameCountry(left string, right string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(left), strings.TrimSpace(right)) && strings.TrimSpace(left) != ""
|
||||
}
|
||||
|
||||
func sameRegion(left int64, right int64) bool {
|
||||
return left > 0 && left == right
|
||||
}
|
||||
|
||||
func managerGrantResourceFromProto(resource *walletv1.Resource) managerGrantResourceData {
|
||||
if resource == nil {
|
||||
return managerGrantResourceData{}
|
||||
|
||||
@ -552,8 +552,12 @@ type fakeUserHostClient struct {
|
||||
kickErr error
|
||||
lastCapability *userv1.CheckBusinessCapabilityRequest
|
||||
capabilities []string
|
||||
capabilityAllowed map[string]bool
|
||||
capabilityResp *userv1.CheckBusinessCapabilityResponse
|
||||
capabilityErr error
|
||||
lastRoleScopePolicy *userv1.GetRoleScopePolicyRequest
|
||||
roleScopePolicyResp *userv1.GetRoleScopePolicyResponse
|
||||
roleScopePolicyErr error
|
||||
}
|
||||
|
||||
type fakeUserHostAdminClient struct {
|
||||
@ -1972,9 +1976,27 @@ func (f *fakeUserHostClient) CheckBusinessCapability(_ context.Context, req *use
|
||||
if f.capabilityResp != nil {
|
||||
return f.capabilityResp, nil
|
||||
}
|
||||
if allowed, configured := f.capabilityAllowed[req.GetCapability()]; configured {
|
||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: allowed}, nil
|
||||
}
|
||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: true}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) GetRoleScopePolicy(_ context.Context, req *userv1.GetRoleScopePolicyRequest) (*userv1.GetRoleScopePolicyResponse, error) {
|
||||
f.lastRoleScopePolicy = req
|
||||
if f.roleScopePolicyErr != nil {
|
||||
return nil, f.roleScopePolicyErr
|
||||
}
|
||||
if f.roleScopePolicyResp != nil {
|
||||
return f.roleScopePolicyResp, nil
|
||||
}
|
||||
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: "manager_add_bd_leader",
|
||||
BaseScope: "country",
|
||||
RegionExpansionConfigurable: true,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) sawCapability(capability string) bool {
|
||||
for _, item := range f.capabilities {
|
||||
if item == capability {
|
||||
|
||||
@ -886,6 +886,7 @@ CREATE TABLE IF NOT EXISTS manager_profiles (
|
||||
can_grant_vip TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心赠送 VIP',
|
||||
can_update_user_level TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心升级用户等级',
|
||||
can_add_bd_leader TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 BD Leader',
|
||||
can_add_bd_leader_in_region TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许把 BD Leader 邀请范围从本国家扩展到经理所属区域',
|
||||
can_add_admin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Admin',
|
||||
can_add_superadmin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Superadmin',
|
||||
can_block_user TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心封禁用户',
|
||||
|
||||
@ -231,23 +231,24 @@ type BDProfile struct {
|
||||
// ManagerProfile 是经理身份事实。
|
||||
// 经理是 BD Leader/Admin 的上级,和 Host/Agency/BD 链路没有父子关系。
|
||||
type ManagerProfile struct {
|
||||
UserID int64
|
||||
RegionID int64
|
||||
Status string
|
||||
Contact string
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
CreatedByAdminID int64
|
||||
CreatedAtMs int64
|
||||
UpdatedAtMs int64
|
||||
UserID int64
|
||||
RegionID int64
|
||||
Status string
|
||||
Contact string
|
||||
CanGrantAvatarFrame bool
|
||||
CanGrantVehicle bool
|
||||
CanGrantBadge bool
|
||||
CanGrantVIP bool
|
||||
CanUpdateUserLevel bool
|
||||
CanAddBDLeader bool
|
||||
CanAddBDLeaderInRegion bool
|
||||
CanAddAdmin bool
|
||||
CanAddSuperadmin bool
|
||||
CanBlockUser bool
|
||||
CanTransferCountry bool
|
||||
CreatedByAdminID int64
|
||||
CreatedAtMs int64
|
||||
UpdatedAtMs int64
|
||||
}
|
||||
|
||||
// CoinSellerProfile 是币商角色事实,余额不保存在 user-service。
|
||||
|
||||
@ -8,18 +8,19 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CapabilityManagerCenter = "manager_center"
|
||||
CapabilityManagerResourceGrant = "manager_resource_grant"
|
||||
CapabilityManagerGrantAvatarFrame = "manager_grant_avatar_frame"
|
||||
CapabilityManagerGrantVehicle = "manager_grant_vehicle"
|
||||
CapabilityManagerGrantBadge = "manager_grant_badge"
|
||||
CapabilityManagerGrantVIP = "manager_grant_vip"
|
||||
CapabilityManagerUpdateUserLevel = "manager_update_user_level"
|
||||
CapabilityManagerAddBDLeader = "manager_add_bd_leader"
|
||||
CapabilityManagerAddAdmin = "manager_add_admin"
|
||||
CapabilityManagerAddSuperadmin = "manager_add_superadmin"
|
||||
CapabilityManagerBlockUser = "manager_block_user"
|
||||
CapabilityManagerTransferCountry = "manager_transfer_user_country"
|
||||
CapabilityManagerCenter = "manager_center"
|
||||
CapabilityManagerResourceGrant = "manager_resource_grant"
|
||||
CapabilityManagerGrantAvatarFrame = "manager_grant_avatar_frame"
|
||||
CapabilityManagerGrantVehicle = "manager_grant_vehicle"
|
||||
CapabilityManagerGrantBadge = "manager_grant_badge"
|
||||
CapabilityManagerGrantVIP = "manager_grant_vip"
|
||||
CapabilityManagerUpdateUserLevel = "manager_update_user_level"
|
||||
CapabilityManagerAddBDLeader = "manager_add_bd_leader"
|
||||
CapabilityManagerAddBDLeaderInRegion = "manager_add_bd_leader_in_region"
|
||||
CapabilityManagerAddAdmin = "manager_add_admin"
|
||||
CapabilityManagerAddSuperadmin = "manager_add_superadmin"
|
||||
CapabilityManagerBlockUser = "manager_block_user"
|
||||
CapabilityManagerTransferCountry = "manager_transfer_user_country"
|
||||
)
|
||||
|
||||
// CheckBusinessCapability 是 App/H5 业务入口的服务端身份边界。
|
||||
@ -49,6 +50,7 @@ func (s *Service) CheckBusinessCapability(ctx context.Context, actorUserID int64
|
||||
CapabilityManagerGrantVIP,
|
||||
CapabilityManagerUpdateUserLevel,
|
||||
CapabilityManagerAddBDLeader,
|
||||
CapabilityManagerAddBDLeaderInRegion,
|
||||
CapabilityManagerAddAdmin,
|
||||
CapabilityManagerAddSuperadmin,
|
||||
CapabilityManagerBlockUser,
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
package host
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
)
|
||||
|
||||
type RoleScope string
|
||||
|
||||
const (
|
||||
RoleScopeCountry RoleScope = "country"
|
||||
RoleScopeRegion RoleScope = "region"
|
||||
RoleScopeGlobal RoleScope = "global"
|
||||
|
||||
RoleScopeSceneOrganizationExpansion = "organization_role_expansion"
|
||||
RoleScopeSceneManagerAddBDLeader = "manager_add_bd_leader"
|
||||
)
|
||||
|
||||
// RoleScopePolicy 把 App 固有边界和可运营扩展能力分开;业务入口只根据解析结果判断目标范围。
|
||||
type RoleScopePolicy struct {
|
||||
Scene string
|
||||
BaseScope RoleScope
|
||||
RegionExpansionConfigurable bool
|
||||
}
|
||||
|
||||
var defaultRoleScopePolicies = map[string]RoleScopePolicy{
|
||||
RoleScopeSceneOrganizationExpansion: {
|
||||
Scene: RoleScopeSceneOrganizationExpansion,
|
||||
BaseScope: RoleScopeRegion,
|
||||
},
|
||||
RoleScopeSceneManagerAddBDLeader: {
|
||||
Scene: RoleScopeSceneManagerAddBDLeader,
|
||||
BaseScope: RoleScopeCountry,
|
||||
RegionExpansionConfigurable: true,
|
||||
},
|
||||
}
|
||||
|
||||
var appRoleScopePolicyOverrides = map[string]map[string]RoleScopePolicy{
|
||||
"huwaa": {
|
||||
RoleScopeSceneOrganizationExpansion: {
|
||||
Scene: RoleScopeSceneOrganizationExpansion,
|
||||
BaseScope: RoleScopeGlobal,
|
||||
},
|
||||
RoleScopeSceneManagerAddBDLeader: {
|
||||
Scene: RoleScopeSceneManagerAddBDLeader,
|
||||
BaseScope: RoleScopeGlobal,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ResolveRoleScopePolicy 是 App 差异的唯一解析入口;新增 App 只扩展注册表,不修改搜索或写入 handler。
|
||||
func ResolveRoleScopePolicy(rawAppCode string, rawScene string) (RoleScopePolicy, error) {
|
||||
scene := strings.ToLower(strings.TrimSpace(rawScene))
|
||||
policy, ok := defaultRoleScopePolicies[scene]
|
||||
if !ok {
|
||||
return RoleScopePolicy{}, xerr.New(xerr.InvalidArgument, "role scope scene is invalid")
|
||||
}
|
||||
if overrides := appRoleScopePolicyOverrides[appcode.Normalize(rawAppCode)]; overrides != nil {
|
||||
if override, exists := overrides[scene]; exists {
|
||||
policy = override
|
||||
}
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// EffectiveScope 只允许声明为 configurable 的策略接受经理个人扩区开关。
|
||||
func (p RoleScopePolicy) EffectiveScope(regionExpansionEnabled bool) RoleScope {
|
||||
if p.RegionExpansionConfigurable && regionExpansionEnabled {
|
||||
return RoleScopeRegion
|
||||
}
|
||||
return p.BaseScope
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package host
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveRoleScopePolicyKeepsAppDifferencesInRegistry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
appCode string
|
||||
baseScope RoleScope
|
||||
configurable bool
|
||||
expanded RoleScope
|
||||
}{
|
||||
{name: "default app", appCode: "lalu", baseScope: RoleScopeCountry, configurable: true, expanded: RoleScopeRegion},
|
||||
{name: "normalized huwaa", appCode: " HUWAa ", baseScope: RoleScopeGlobal, configurable: false, expanded: RoleScopeGlobal},
|
||||
}
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
policy, err := ResolveRoleScopePolicy(testCase.appCode, RoleScopeSceneManagerAddBDLeader)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve policy: %v", err)
|
||||
}
|
||||
if policy.BaseScope != testCase.baseScope || policy.RegionExpansionConfigurable != testCase.configurable {
|
||||
t.Fatalf("policy mismatch: %+v", policy)
|
||||
}
|
||||
if got := policy.EffectiveScope(true); got != testCase.expanded {
|
||||
t.Fatalf("expanded scope = %q, want %q", got, testCase.expanded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -135,6 +135,16 @@ func TestCheckBusinessCapabilityRequiresActiveManagerProfile(t *testing.T) {
|
||||
t.Fatalf("active manager should be allowed: allowed=%v reason=%q", allowed, reason)
|
||||
}
|
||||
|
||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerAddBDLeaderInRegion)
|
||||
if err != nil || allowed || reason != "manager_capability_required" {
|
||||
t.Fatalf("region expansion must default closed: allowed=%v reason=%q err=%v", allowed, reason, err)
|
||||
}
|
||||
repository.SetManagerAddBDLeaderInRegion(101, true)
|
||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerAddBDLeaderInRegion)
|
||||
if err != nil || !allowed || reason != "" {
|
||||
t.Fatalf("enabled region expansion should be allowed: allowed=%v reason=%q err=%v", allowed, reason, err)
|
||||
}
|
||||
|
||||
repository.SetManagerGrantVIP(101, false)
|
||||
allowed, reason, err = svc.CheckBusinessCapability(ctx, 101, hostservice.CapabilityManagerGrantVIP)
|
||||
if err != nil {
|
||||
|
||||
@ -373,6 +373,8 @@ func (r *Repository) HasActiveManagerCapability(ctx context.Context, userID int6
|
||||
columnSQL = "can_update_user_level = 1"
|
||||
case hostservice.CapabilityManagerAddBDLeader:
|
||||
columnSQL = "can_add_bd_leader = 1"
|
||||
case hostservice.CapabilityManagerAddBDLeaderInRegion:
|
||||
columnSQL = "can_add_bd_leader_in_region = 1"
|
||||
case hostservice.CapabilityManagerAddAdmin:
|
||||
columnSQL = "can_add_admin = 1"
|
||||
case hostservice.CapabilityManagerAddSuperadmin:
|
||||
|
||||
@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
hostservice "hyapp/services/user-service/internal/service/host"
|
||||
)
|
||||
|
||||
const huwaaAppCode = "huwaa"
|
||||
|
||||
// roleRegionMatchRequired 只控制 Agency 角色邀请、Host 主动申请和 BD 邀请的区域匹配。
|
||||
// Huwaa 的这些组织拓展允许跨区域,但角色事实仍归属目标用户当前区域;Agency 主动邀请 Host、币商列表和子币商关系不得复用该例外。
|
||||
// App 差异由统一策略注册表解析;Agency 主动邀请 Host、币商列表和子币商关系不得复用该例外。
|
||||
func roleRegionMatchRequired(ctx context.Context) bool {
|
||||
return appcode.FromContext(ctx) != huwaaAppCode
|
||||
policy, err := hostservice.ResolveRoleScopePolicy(appcode.FromContext(ctx), hostservice.RoleScopeSceneOrganizationExpansion)
|
||||
return err != nil || policy.BaseScope != hostservice.RoleScopeGlobal
|
||||
}
|
||||
|
||||
@ -829,9 +829,9 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||
INSERT INTO manager_profiles (
|
||||
user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
|
||||
can_grant_vip, can_update_user_level, can_add_bd_leader, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
|
||||
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,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
contact_info = VALUES(contact_info),
|
||||
@ -841,6 +841,7 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
can_grant_vip = VALUES(can_grant_vip),
|
||||
can_update_user_level = VALUES(can_update_user_level),
|
||||
can_add_bd_leader = VALUES(can_add_bd_leader),
|
||||
can_add_bd_leader_in_region = VALUES(can_add_bd_leader_in_region),
|
||||
can_add_admin = VALUES(can_add_admin),
|
||||
can_add_superadmin = VALUES(can_add_superadmin),
|
||||
can_block_user = VALUES(can_block_user),
|
||||
@ -848,7 +849,7 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, profile.UserID, profile.Status, profile.Contact, defaultTrue(profile.CanGrantAvatarFrame),
|
||||
defaultTrue(profile.CanGrantVehicle), defaultTrue(profile.CanGrantBadge), defaultTrue(profile.CanGrantVIP),
|
||||
defaultTrue(profile.CanUpdateUserLevel), defaultTrue(profile.CanAddBDLeader), defaultTrue(profile.CanAddAdmin),
|
||||
defaultTrue(profile.CanUpdateUserLevel), defaultTrue(profile.CanAddBDLeader), profile.CanAddBDLeaderInRegion, defaultTrue(profile.CanAddAdmin),
|
||||
defaultTrue(profile.CanAddSuperadmin), defaultTrue(profile.CanBlockUser), defaultTrue(profile.CanTransferCountry),
|
||||
profile.CreatedByAdminID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||||
if err != nil {
|
||||
@ -856,6 +857,19 @@ func (r *Repository) PutManagerProfile(profile hostdomain.ManagerProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) SetManagerAddBDLeaderInRegion(userID int64, enabled bool) {
|
||||
r.t.Helper()
|
||||
|
||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||
UPDATE manager_profiles
|
||||
SET can_add_bd_leader_in_region = ?
|
||||
WHERE user_id = ?
|
||||
`, enabled, userID)
|
||||
if err != nil {
|
||||
r.t.Fatalf("set manager region BD Leader permission %d failed: %v", userID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) SetManagerGrantVIP(userID int64, enabled bool) {
|
||||
r.t.Helper()
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package grpc
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
hostservice "hyapp/services/user-service/internal/service/host"
|
||||
|
||||
@ -539,6 +540,20 @@ func (s *Server) CheckBusinessCapability(ctx context.Context, req *userv1.CheckB
|
||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: allowed, Reason: reason}, nil
|
||||
}
|
||||
|
||||
// GetRoleScopePolicy 暴露 App 级角色范围元数据,让 gateway/admin 只消费策略结果而不复制 app_code 分支。
|
||||
func (s *Server) GetRoleScopePolicy(ctx context.Context, req *userv1.GetRoleScopePolicyRequest) (*userv1.GetRoleScopePolicyResponse, error) {
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
policy, err := hostservice.ResolveRoleScopePolicy(appcode.FromContext(ctx), req.GetScene())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
|
||||
Scene: policy.Scene,
|
||||
BaseScope: string(policy.BaseScope),
|
||||
RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// GetAgencyMembers 返回 Agency 成员关系事实列表。
|
||||
func (s *Server) GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user