feat(round-13): 潜客企业域名富化/雷达CSV报表导出/商机Webhook签名分发/销售管辖权限隔离 (91套件1033测试全绿)

这个提交包含在:
wikiboss
2026-09-21 02:09:13 +08:00
父节点 3cfbed9e6a
当前提交 749ccf2e01
共有 8 个文件被更改,包括 906 次插入 和 0 次删除

查看文件

@@ -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;
}
}
}