分销权限控制
刚开源了一个分销权限控制相关工具,说说设计思路
Sari 在雅加达做东南亚代购,分销体系铺了三百多个下线。商品搜索页面一到中午就卡死——后来查日志,每个分销商请求都要递归查询其下级代理的商品权限,数据库被几十层 JOIN 拖到响应时间从 200ms 飙到 5 秒。更麻烦的是,上层分销商能看到不该看的价格成本,下层代理又看不到总代库存。权限模型一塌糊涂。
所以我把这套分销权限控制逻辑抽成了开源工具,今天说说设计思路。
Overview
工具名叫 DistributorACL,解决两个核心问题:
数据范围权限:分销商只能查看自己树下的订单、库存、商品。
操作权限位:能否改价、能否看到采购价、能否触发自动采购。
底层用 RBAC 扩展,但把资源隔离从“角色-权限”升级为“角色-组织树-权限”。为了压住慢查询,所有权限判定都在 Redis 里用位图 + 有序集合完成,数据库只存原始关系。
Design Rationale
为什么不直接用 Laravel 的 Gate?因为分销树的深度不确定,且每个节点的权限可能覆盖父节点(比如子代理被单独禁止查看采购价)。常规方案在 policy 里递归查数据库,N 层就是 N 次查询。
我们选 预计算权限矩阵:每天凌晨跑一次定时任务,为每个分销商生成一份“可访问商品 ID 列表”的位图(商品总量 ≤ 10 万时,位图仅 12.5KB 每人)。运行时直接 bitcount 判断,O(1) 复杂度。
权限变更时(上下级关系调整、禁用某个商品),用消息队列异步刷新受影响的子树,避免实时递归。
另外,为了提升代购系统信任度,我们把分销链路上的关键动作(采购、发货、签收)哈希后写入 Hyperledger Fabric 测试网——每个分销商能通过订单详情页验证商品溯源信息,假货纠纷大幅减少。
Core Implementation
权限核心三张表:
distributor_tree(闭包表存上下级路径)
distributor_permission(位掩码:1-查看成本价,2-修改佣金,4-触发采购,8-查看下级订单)
product_visibility_bitmap(redis key: dv:user:{uid}:products,存位图)
// 权限位定义
class PermissionBits {
const VIEW_COST
= 1 << 0; // 1
const EDIT_COMMISION = 1 << 1; // 2
const TRIGGER_PURCHASE = 1 << 2; // 4
const VIEW_SUB_ORDERS = 1 << 3; // 8
}
// 检查分销商是否有权查看某个商品(位图)
function canAccessProduct($distributorId, $productId) {
$redis = getRedis();
$key = "dv:user:{$distributorId}:products";
// 若位图不存在,回源构建
if (!$redis->exists($key)) {
buildProductBitmap($distributorId);
}
// 位图第 $productId 位是否为 1
return $redis->getbit($key, $productId) === 1;
}
// 构建位图:递归收集下级可见商品(含自身)
function buildProductBitmap($distributorId) {
$productIds = fetchAccessibleProductsFromDB($distributorId); // 递归闭包表
$bitmap = '';
foreach ($productIds as $id) {
$bitmap = setBitInString($bitmap, $id, 1);
}
$redis->setex("dv:user:{$distributorId}:products", 86400, $bitmap);
}
自动采购功能集成在权限系统里:只有拥有 TRIGGER_PURCHASE 位的分销商才能在订单页面点“补货”。后端收到请求后,先检查权限位,再调用采购引擎。
// 自动采购入口权限控制
function requestAutoPurchase($distributorId, $productId, $quantity) {
$perms = getUserPermissions($distributorId); // 返回位掩码
if (!($perms & PermissionBits::TRIGGER_PURCHASE)) {
throw new UnauthorizedException("无自动采购权限");
}
// 库存预扣 + 发送采购任务(防超卖用 Redis 分布式锁)
$lockKey = "purchase:lock:{$productId}";
$lock = acquireRedisLock($lockKey, 5);
if (!$lock) {
throw new Exception("采购处理中,请稍后重试");
}
try {
if (deductStock($productId, $quantity)) {
dispatchJob('auto_purchase', compact('productId', 'quantity', 'distributorId'));
}
} finally {
releaseLock($lockKey);
}
}
Edge Cases
权限缓存不一致:当分销商的下级新增商品权限时,需要刷新该分销商及其所有上级的位图。我们用 refresh_tree 队列递归更新,避免实时开销。
跨级查询慢:分销商查看下级所有订单,如果每次请求都去闭包表找出所有子孙后代,复杂度 O(后代数)。优化方案:订单表冗余 distributor_path 字段(存储从根到当前分销商的ID路径,用逗号分隔),查询时 WHERE path LIKE '%,{$uid},%' 并加索引,单次查询 < 10ms。
区块链防伪写入性能:不阻塞主流程,订单状态变为 delivered 后,异步将订单哈希、时间戳、分销商签名推送到链上。用户前台展示“区块链存证ID”,可独立查询。
Usage
安装后配置分销商树数据源和商品表名。在需要鉴权的控制器方法前加中间件:
// Laravel 示例
Route::get('/products', function() {
// 中间件自动检查当前用户能否访问请求中的 product_id
})->middleware('distributor.permission:view_product');
中间件实现:
class DistributorPermissionMiddleware {
public function handle($request, $next, $perm) {
$user = auth()->user();
if ($perm === 'view_product') {
$productId = $request->input('product_id');
if (!canAccessProduct($user->distributor_id, $productId)) {
abort(403, '无该商品查看权限');
}
}
// 其他权限位检查。
return $next($request);
}
}
开源地址见个人 GitHub(搜 DistributorACL)。Sari 用这套工具把商品搜索从 5 秒压到 300ms 以下,分销商投诉几乎归零。回头想想,权限控制的核心不是“加更多角色”,而是把数据范围隔离做到 O(1) 可计算。