人岗客权限校验(数据权限控制)实战指南
人岗客权限校验数据权限控制实战指南一、概念定义什么是人岗客权限人岗客是企业级系统中的一种数据权限控制模型核心思想是用户只能操作其岗位职责范围内的客户数据。要素含义举例人当前操作用户张三工号 A001岗用户所在的组织/岗位青岛分公司销售岗客归属于特定组织的客户客户8800012345 归属青岛分公司控制逻辑用户 A 只能操作归属于自己所在分公司或渠道、区域的客户数据不能跨组织操作。与功能权限的区别维度功能权限数据权限人岗客控制粒度接口/菜单/按钮级别数据行级别判断依据角色是否有此操作权限数据是否属于用户管辖范围示例“能否调用导入接口”“能否导入这条客户数据”实现位置网关/拦截器/注解业务逻辑层注博客https://blog.csdn.net/badao_liumang_qizhi二、业务模式设计2.1 权限维度选择不同业务场景选择不同的权限维度权限维度适用场景数据关系分公司编码fcCode区域型组织按地区划分客户 → 归属分公司渠道编码channelCode渠道型组织按销售渠道划分客户 → 归属渠道门店编码storeCode零售型组织按门店划分客户 → 归属门店组合维度复杂组织多维度交叉需同时满足多个条件2.2 权限数据流转模式┌──────────────────────────────────────────────────────┐ │ 前端 │ │ 登录 → 获取用户权限列表 → 存储到本地/Token │ │ 操作时 → 将权限列表作为参数传给后端 │ └────────────────────────┬─────────────────────────────┘ │ 请求携带权限列表 ▼ ┌──────────────────────────────────────────────────────┐ │ 后端 │ │ 接收权限列表 → 查询目标数据的归属组织 │ │ → 判断目标归属是否在用户权限范围内 │ │ → 通过则执行操作不通过则拒绝 │ └──────────────────────────────────────────────────────┘2.3 权限获取方式对比方式描述优缺点前端传入前端登录时获取权限列表操作时传给后端简单但需信任前端数据后端查询后端根据当前用户Token中的userId去权限中心查安全但增加一次远程调用Token携带JWT 中直接编码权限列表无额外查询但Token体积大缓存 后端校验后端从Redis缓存中获取用户权限安全且高性能三、在新增和导入中的应用模式3.1 新增单条场景前端传入: { data: {...}, permissionList: [ORG001, ORG002] } │ ▼ 后端处理: 查询目标数据归属组织 → 比对是否在 permissionList 中 │ ┌───────┴───────┐ ▼ ▼ 在范围内 不在范围内 执行新增 抛出异常拒绝特点校验不通过直接抛异常中断操作。3.2 导入批量场景前端传入: { file: excel, permissionList: [ORG001, ORG002] } │ ▼ 后端处理: 批量查询所有数据的归属组织 → 逐条比对 │ ┌───────┴───────┐ ▼ ▼ 在范围内 不在范围内 加入成功列表 加入失败列表标注原因 │ │ ▼ ▼ 批量入库 生成失败报告Excel特点校验不通过不中断记录到失败列表最终生成失败报告。3.3 两者的核心差异维度单条新增批量导入失败处理抛异常中断记录失败原因继续下一条权限数据获取从请求体中取从请求参数中取DB 交互单条查询批量预查询 Map 内存比对返回值成功/异常成功数 失败数 失败文件URL四、关键技术点4.1 本地表关联 vs 远程调用方式实现适用场景本地表关联直接查本地数据库的组织字段本地有客户归属数据远程Feign调用调用用户中心/权限中心获取本地无客户详情选择原则优先使用本地数据减少远程调用对性能的影响。4.2 批量场景的性能考量// ❌ 逐条查询组织归属N次DBfor(Datadata:dataList){OrgInfoorgorgService.getByCode(data.getCode());if(!permList.contains(org.getOrgCode())){fail;}}// ✅ 批量预查询 内存比对1次DBMapString,OrgInfoorgMaporgService.batchGetByCodeIn(allCodes);for(Datadata:dataList){OrgInfoorgorgMap.get(data.getCode());// O(1)if(!permList.contains(org.getOrgCode())){fail;}}4.3 权限列表为空的处理策略策略逻辑适用场景严格模式权限列表为空 → 所有数据不通过强管控业务宽松模式权限列表为空 → 跳过校验管理员/超管场景// 严格模式if(isEmpty(permList)||!permList.contains(orgCode)){thrownewBizException(无操作此数据的权限);}// 宽松模式if(isNotEmpty(permList)!permList.contains(orgCode)){thrownewBizException(无操作此数据的权限);}五、完整示例代码以下以商品管理系统-商品上架为例演示人岗客权限的完整实现。5.1 业务场景商品归属于某个仓库warehouseCode用户只能上架归属于自己管辖仓库的商品前端传入用户有权限的仓库编码列表5.2 DTO 定义/** * 商品上架入参DTO. */DataApiModel(description商品上架入参)publicclassProductListingParamsDto{ApiModelProperty(value商品编码,requiredtrue)privateStringproductCode;ApiModelProperty(value上架价格)privateBigDecimalprice;ApiModelProperty(value当前登录人有权限的仓库编码列表,requiredtrue)privateListStringwarehouseCodeList;ApiModelProperty(value操作人工号)privateStringoperatorCode;}/** * 商品批量上架导入DTO. */DatapublicclassProductListingExcelDto{ApiModelProperty(value商品编码)privateStringproductCode;ApiModelProperty(value上架价格)privateStringprice;}/** * 导入结果DTO. */DatapublicclassImportResultDto{ApiModelProperty(value成功数量)privateintsuccessCount;ApiModelProperty(value失败数量)privateintfailCount;ApiModelProperty(value失败报告文件URL)privateStringfailFileUrl;}5.3 Entity 定义/** * 商品基础信息实体. */DataEntityTable(nameproduct_base)publicclassProductBase{IdGeneratedValue(strategyGenerationType.IDENTITY)privateIntegerid;/** 商品编码. */Column(nameproduct_code)privateStringproductCode;/** 商品名称. */Column(nameproduct_name)privateStringproductName;/** 归属仓库编码. */Column(namewarehouse_code)privateStringwarehouseCode;/** 商品状态1-待上架 2-已上架 3-已下架. */Column(namestatus)privateIntegerstatus;}5.4 Repository 定义/** * 商品基础信息Repository. */publicinterfaceProductBaseRepositoryextendsJpaRepositoryProductBase,Integer{/** * 根据商品编码查询第一条记录. */ProductBasefindFirstByProductCode(StringproductCode);/** * 根据商品编码列表批量查询. */ListProductBasefindByProductCodeIn(ListStringproductCodes);}5.5 Service 实现Slf4jServicepublicclassProductListingServiceImplimplementsProductListingService{ResourceprivateProductBaseRepositoryproductBaseRepository;/** * 单条商品上架严格模式权限为空则拒绝. */OverrideTransactional(rollbackForException.class)publicBooleanlistingProduct(ProductListingParamsDtoparamsDto){StringproductCodeparamsDto.getProductCode();// 1. 校验商品是否存在ProductBaseproductproductBaseRepository.findFirstByProductCode(productCode);if(productnull){thrownewBizException(-1,null,商品不存在);}// 2. 人岗客权限校验仓库维度ListStringwarehouseCodeListparamsDto.getWarehouseCodeList();if(warehouseCodeListnull||warehouseCodeList.isEmpty()||!warehouseCodeList.contains(product.getWarehouseCode())){thrownewBizException(-1,null,无操作此仓库商品的权限);}// 3. 业务状态校验if(!Objects.equals(product.getStatus(),1)){thrownewBizException(-1,null,当前商品状态不支持上架);}// 4. 执行上架product.setStatus(2);productBaseRepository.save(product);returnBoolean.TRUE;}/** * 批量导入上架批量预查询 失败记录不中断. */OverridepublicImportResultDtobatchListingImport(ListProductListingExcelDtodataList,ListStringwarehouseCodeList,StringoperatorCode){ImportResultDtoresultDtonewImportResultDto();ListString[]failListnewArrayList();ListProductBasesuccessListnewArrayList();// 批量预查询 ListStringallProductCodesdataList.stream().map(ProductListingExcelDto::getProductCode).filter(Objects::nonNull).map(String::trim).distinct().collect(Collectors.toList());// 一次性查出所有商品信息转MapMapString,ProductBaseproductMapnewHashMap();if(!allProductCodes.isEmpty()){ListProductBaseproductListproductBaseRepository.findByProductCodeIn(allProductCodes);if(productList!null){productMapproductList.stream().collect(Collectors.toMap(ProductBase::getProductCode,p-p,(p1,p2)-p1));}}// 逐条校验内存比对0次DB SetStringbatchDuplicatenewHashSet();for(ProductListingExcelDtodto:dataList){StringproductCodedto.getProductCode();// 校验编码不能为空if(productCodenull||productCode.trim().isEmpty()){failList.add(newString[]{productCode,商品编码不能为空});continue;}StringtrimmedCodeproductCode.trim();// 校验批次内重复if(batchDuplicate.contains(trimmedCode)){failList.add(newString[]{productCode,商品编码在导入文件中重复});continue;}// 校验商品是否存在Map查找 O(1)ProductBaseproductproductMap.get(trimmedCode);if(productnull){failList.add(newString[]{productCode,商品不存在});continue;}// ★ 人岗客权限校验仓库维度严格模式if(warehouseCodeListnull||warehouseCodeList.isEmpty()||!warehouseCodeList.contains(product.getWarehouseCode())){failList.add(newString[]{productCode,无操作此仓库商品的权限});continue;}// 业务状态校验if(!Objects.equals(product.getStatus(),1)){failList.add(newString[]{productCode,当前商品状态不支持上架});continue;}// 校验通过修改状态product.setStatus(2);successList.add(product);batchDuplicate.add(trimmedCode);}// 批量保存 intsuccessCount0;if(!successList.isEmpty()){try{productBaseRepository.saveAll(successList);successCountsuccessList.size();}catch(Exceptione){log.error(批量保存失败降级为逐条保存,e);for(ProductBaseproduct:successList){try{productBaseRepository.save(product);successCount;}catch(Exceptionex){failList.add(newString[]{product.getProductCode(),保存失败ex.getMessage()});}}}}resultDto.setSuccessCount(successCount);resultDto.setFailCount(failList.size());if(!failList.isEmpty()){resultDto.setFailFileUrl(generateFailReport(failList));}returnresultDto;}/** * 生成失败报告Excel. */privateStringgenerateFailReport(ListString[]failList){// 生成Excel → 上传OSS → 返回URL逻辑省略returnhttps://oss.example.com/import-fail-xxx.xlsx;}}5.6 Controller 层Slf4jRestControllerRequestMapping(/api/product)publicclassProductListingController{privatestaticfinalintMAX_IMPORT_SIZE5000;ResourceprivateProductListingServiceproductListingService;/** * 单条商品上架. */PostMapping(/listing)publicRestResultBooleanlistingProduct(RequestBodyProductListingParamsDtoparamsDto){if(paramsDtonull||paramsDto.getProductCode()null){thrownewBizException(-1,null,商品编码不能为空);}returnRestResult.success(productListingService.listingProduct(paramsDto));}/** * 批量导入上架. */PostMapping(/batch-listing-import)publicRestResultImportResultDtobatchListingImport(RequestParam(file)MultipartFilefile,RequestParam(warehouseCodeList)ListStringwarehouseCodeList,RequestParam(operatorCode)StringoperatorCode){// 文件校验if(filenull||file.isEmpty()){thrownewBizException(-1,null,导入文件不能为空);}// 解析ExcelListProductListingExcelDtodataListExcelUtil.parse(file,ProductListingExcelDto.class);if(dataListnull||dataList.isEmpty()){thrownewBizException(-1,null,导入数据为空);}// 条数限制if(dataList.size()MAX_IMPORT_SIZE){thrownewBizException(-1,null,单次导入不能超过MAX_IMPORT_SIZE条当前dataList.size()条);}returnRestResult.success(productListingService.batchListingImport(dataList,warehouseCodeList,operatorCode));}}六、设计要点总结要点说明权限维度要与业务匹配区域管控用分公司编码渠道管控用渠道编码优先使用本地数据校验避免为权限校验发起远程调用影响性能单条操作抛异常批量操作记失败单条中断用户流程批量允许部分成功严格模式 vs 宽松模式根据业务选强管控用严格模式权限为空无权限批量导入必须预查询避免 N 次循环查询改为 1 次 IN 查询 Map权限参数由前端传入前端登录时已获取权限操作时直接传递