feat(round-13): 潜客企业域名富化/雷达CSV报表导出/商机Webhook签名分发/销售管辖权限隔离 (91套件1033测试全绿)
这个提交包含在:
@@ -0,0 +1,107 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import { BaseService } from '@cool-midway/core';
|
||||
|
||||
export type TeamUserRole = 'owner' | 'admin' | 'sales_manager' | 'sales_rep' | 'member';
|
||||
|
||||
export interface UserDataContext {
|
||||
userId: number;
|
||||
tenantId: string;
|
||||
role: TeamUserRole;
|
||||
departmentId?: string;
|
||||
}
|
||||
|
||||
export interface LeadDataScopeFilter {
|
||||
tenantId: string;
|
||||
assignedUserId?: number;
|
||||
departmentId?: string;
|
||||
}
|
||||
|
||||
export interface LeadEntityRecord {
|
||||
id: number;
|
||||
tenantId: string;
|
||||
assignedUserId?: number;
|
||||
departmentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户当前身份构建线索数据列表的过滤条件 (SQL WHERE 映射)
|
||||
*/
|
||||
export function buildLeadDataScopeFilter(ctx: UserDataContext): LeadDataScopeFilter {
|
||||
const filter: LeadDataScopeFilter = {
|
||||
tenantId: ctx.tenantId,
|
||||
};
|
||||
|
||||
// 租户所有者与管理员具备全量租户数据权限
|
||||
if (ctx.role === 'owner' || ctx.role === 'admin') {
|
||||
return filter;
|
||||
}
|
||||
|
||||
// 销售经理管辖本部门内全部销售
|
||||
if (ctx.role === 'sales_manager') {
|
||||
if (ctx.departmentId) {
|
||||
filter.departmentId = ctx.departmentId;
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
// 一线销售代表或普通成员仅能查看分配给自己的线索
|
||||
filter.assignedUserId = ctx.userId;
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条线索访问鉴权校验,防止跨租户与同租户横向越权访问
|
||||
*/
|
||||
export function assertLeadAccessPermission(
|
||||
ctx: UserDataContext,
|
||||
lead: LeadEntityRecord
|
||||
): void {
|
||||
// 1. 租户隔离校验
|
||||
if (ctx.tenantId !== lead.tenantId) {
|
||||
throw new Error('无权访问该客户线索 (Tenant mismatch / Forbidden)');
|
||||
}
|
||||
|
||||
// 2. 超级管理员与 Owner 拥有全权
|
||||
if (ctx.role === 'owner' || ctx.role === 'admin') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 销售组长:同部门可看
|
||||
if (ctx.role === 'sales_manager') {
|
||||
if (ctx.departmentId && ctx.departmentId === lead.departmentId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 一线销售:必须是线索负责人
|
||||
if (lead.assignedUserId === ctx.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('无权访问该客户线索,权限不足 (Forbidden: Insufficient permissions)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 出海多租户团队成员细粒度数据隔离服务
|
||||
*/
|
||||
@Provide()
|
||||
export class TeamDataScopeService extends BaseService {
|
||||
/**
|
||||
* 获取线索查询范围
|
||||
*/
|
||||
getLeadScope(ctx: UserDataContext): LeadDataScopeFilter {
|
||||
return buildLeadDataScopeFilter(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检验单条线索是否具备读取权限
|
||||
*/
|
||||
checkAccess(ctx: UserDataContext, lead: LeadEntityRecord): boolean {
|
||||
try {
|
||||
assertLeadAccessPermission(ctx, lead);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户