线联ERP数据权限配置操作手册

作者:jonny  发布日期:2026/10/28 22:11:50

线联ERP数据权限配置操作手册|C/S开发框架

ERP 系统中,成本、卖价、金额是典型的敏感数据:业务不能看成本、采购不能看卖价、工程部不能看卖价和成本,HR/车间/仓库则要求屏蔽所有单价金额。传统的"每个窗体写死 if"做法维护成本极高。本文基于 CSFrameworkV6 快速开发框架(.NET 8 + WinForms + SQL Server)实现一套集中配置、字段字典驱动的数据权限方案,并交付数据权限配置窗体,管理员勾选即可即刻生效。

一、方案概览:三重生效判定

整套方案只有一条核心判定规则,三个条件缺一不可:

某项权限对当前用户生效 = 总开关启用(UseDataPermission=启用)
                       AND 该权限项勾选(Enabled=启用)
                       AND 当前用户命中该权限项的适用范围

配套三条约定:

1、适用范围为空 = 对所有用户生效(含管理员自己,不做豁免);
2、读取配置失败视为"无命中",宁可放开也不阻塞界面;
3、隐藏方式为整列隐藏(列 Visible=false),不采用打码/掩码方式。

配置存储在系统库三张表:sys_DataPermissionItem(权限项)、sys_DataPermissionItemScope(适用范围)、sys_DataRangePermission(行级预留),总开关存 sys_SystemSettings(ParamCode=UseDataPermission,默认"不启用")。

二、配置窗体布局

配置窗体(frmDataPermissionConfig)挂在"系统管理"菜单下,仅管理员可见:

区域组件说明
顶部【总开关】CheckBox数据权限总开关(UseDataPermission)
左栏8 个权限项 CheckBox数据驱动,动态生成自 sys_DataPermissionItem,按【行级权限】【列级权限】分组
右栏适用范围编辑区生效部门(多选)/ 生效角色(多选)/ 指定用户(逗号分隔文本框)
底部【保存】【关闭】保存 = 即时写库 + 失效缓存,即刻生效

线联ERP数据权限配置操作手册|C/S开发框架

左栏点击任一权限项,右栏加载该项的适用范围;范围下拉显示格式为 ID - Name(如 0003 - 采购部),但保存的是编码(GroupCode/RoleID),部门/角色改名不影响已存配置。

三、8 个权限项的配置逻辑

1、行级权限(4 项,预留)

行级权限控制"能看到哪些行"。4 个配置项已建好(可勾选、可保存),查询过滤逻辑预留待实施:

配置项编码计划过滤逻辑
业务员只能看自己的客户CustomerSelfdt_Customer.SalesUserId == 自己的 SalesCode(经 dt_SalesPerson.UserId 关联)OR SalesUserId 为空(无主客户人人可见)
部门经理可以看该部门的所有客户CustomerDept部门经理判定:tb_MyGroup.Owner1/Owner2 含当前账号;范围 = 管辖部门(含子部门)用户的 SalesCode 集合
制单人只能看自己的订单DocSelf制单人字段(CreatedBy/CreationBy/DocUser,按单据映射)== 当前账号
部门经理可以看该部门的订单DocDept制单人 ∈ 管辖部门(含子部门)用户集合

2、列级权限(4 项,已实施)

列级权限控制"能看到哪些列",同一用户命中多个项时敏感分类取并集:

配置项编码隐藏的敏感分类
业务不能看成本SalesHideCostCost(成本类)
采购不能看卖价PurchaseHidePriceSalesPrice(卖价类)
工程部不能看卖价、成本EngineerHideCostPriceCost + SalesPrice
屏蔽所有单价、金额AllHideAmountCost + SalesPrice + Amount(全部)

四、敏感字段映射:35 条字段字典

1、三类敏感分类

方案 B 的核心思想是:不逐窗体配置,而是建一张 FieldName → 敏感分类的集中映射字典。业务窗体打开时遍历 GridView 全部列,按列 FieldName 查字典得到分类,命中即整列隐藏。与数据源模型无关(res_tb_PO / tb_PO 通用),不受代码生成器再生成影响。

2、字段清单

分类字段(FieldName,忽略大小写)
Cost 成本类(7 个)CostPrice、CostAmount、ManufactCostPrice、ManufactCostAmount、ProcessPrice、ProcessAmount、StandardCost
SalesPrice 卖价类(2 个)PriceSO(成品入库订单卖价)、PriceNewly(价格表最新价格)
Amount 通用金额类(26 个)Price、Amount、LocalPrice、LocalAmount、ForeignPrice、ForeignAmount、PriceExclTax、AmountExclTax、TaxAmount、AmountSO、TotalAmount、TotalLocalAmount、TotalForeignAmount、TotalTaxAmount、TotalAmountExclTax、TotalAmountExclRate、SettleAmount、SettleAmountBal、TotalSettleAmount、TotalSettleAmountBal、TotalSettleLocalAmount、TotalSettleLocalAmountBal、InvoiceAmount、InvoiceAmountBal、TotalInvoiceAmount、TotalInvoiceAmountBal

新增/调整映射只需改字典文件;个别窗体的同名特例可用 sys_DataPermissionOverride(窗体×FieldName)覆盖,优先级高于全局字典。

五、核心实现

1、生效判定:DataPermissionContext

Business 层静态类,读系统库计算当前用户命中的敏感分类集合,带静态缓存:

//项编码 → 敏感分类集合(列级 4 项在此映射)
private static readonly Dictionary<string, string[]> _itemCategories
    = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
    { "SalesHideCost",         new[] { nameof(SensitiveType.Cost) } },
    { "PurchaseHidePrice",     new[] { nameof(SensitiveType.SalesPrice) } },
    { "EngineerHideCostPrice", new[] { nameof(SensitiveType.Cost), nameof(SensitiveType.SalesPrice) } },
    { "AllHideAmount",         new[] { nameof(SensitiveType.Cost), nameof(SensitiveType.SalesPrice), nameof(SensitiveType.Amount) } },
};

public static HashSet<string> GetHitCategories()
{
    //... 缓存加载
    var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    if (SystemSettings.Current.UseDataPermission)   //条件①:总开关
    {
        var items = db.GetQueryable<sys_DataPermissionItem>()
            .Where(w => w.Enabled == "启用" && w.ItemType == "Field").ToList(); //条件②:项勾选
        var scopes = db.GetQueryable<sys_DataPermissionItemScope>().ToList();

        foreach (var item in items)
        {
            var itemScopes = scopes.Where(w => w.ItemCode == item.ItemCode).ToList();
            //条件③:适用范围为空 = 对所有用户生效;否则部门/用户/角色任一命中
            var hit = itemScopes.Count == 0 || itemScopes.Any(s =>
                (s.ScopeType == "Dept" && s.ScopeCode == org)
                || (s.ScopeType == "User" && s.ScopeCode == account)
                || (s.ScopeType == "Role" && roles.Contains(s.ScopeCode)));
            if (hit) set.UnionWith(_itemCategories[item.ItemCode]); //多项目录取并集
        }
    }
    return set;
}

2、列隐藏:SensitiveFieldApplier

Library 层统一入口,遍历窗体内全部 GridControl 的所有 GridView 列,命中即隐藏(幂等):

private static void ApplyToView(GridView view, HashSet<string> hitTypes)
{
    foreach (DevExpress.XtraGrid.Columns.GridColumn col in view.Columns)
    {
        if (string.IsNullOrEmpty(col.FieldName)) continue;
        if (SensitiveFieldMap.TryGetCategory(col.FieldName, out var type)
            && hitTypes.Contains(type.ToString()))
        {
            col.Visible = false;   //整列隐藏,不打码
        }
    }
}

//调用点:业务窗体基类 DoViewContent,每次加载数据后重应用,覆盖布局恢复
SensitiveFieldApplier.ApplyAll(this);

3、缓存失效:保存即生效

配置窗体保存成功后调用 DataPermissionContext.ResetCache() 清空静态缓存,下一次任何业务窗体加载数据时按新配置重新应用。已打开的窗体不会自动刷新,重新打开或刷新数据即可。

六、适用范围与保存机制

范围类型存储值判定逻辑
生效部门ScopeType='Dept',ScopeCode=GroupCode当前用户所属组织(Loginer.CurrentUser.Org2)== GroupCode
生效角色ScopeType='Role',ScopeCode=RoleID当前用户角色串(Loginer.CurrentUser.Roles)包含 RoleID
指定用户ScopeType='User',ScopeCode=账号当前登录账号 == ScopeCode

三种范围类型是"或"的关系,命中任意一条即生效。保存时依次写:总开关 → 各项开关变更 → 当前选中项的适用范围(先清后存,全量覆盖)。

七、常见问题

Q1:勾选了"业务不能看成本",某用户还能看到成本列?
按三条件排查:总开关是否启用 → 该项是否勾选 → 用户是否命中适用范围。另注意列必须能被字段字典识别(按 FieldName 精确匹配)。

Q2:保存后当前窗体没变化?
缓存已失效但已打开的窗体不会重应用,重新打开窗体或刷新数据即可。

Q3:管理员自己也会被隐藏列吗?
会。数据权限不做管理员豁免,只要命中"启用项 + 适用范围"同样隐藏。不希望管理员受控时,把相关项的适用范围配置为不含管理员。

Q4:为什么不逐窗体配置要隐藏哪些列?
逐窗体配置工作量大且受代码生成器再生成影响;字段字典方案只在字典文件维护 35 条映射,一个字典覆盖全部窗体。

了解更多 CSFrameworkV6 快速开发框架,请访问 C/S框架网。