CRMEB Pro 二开避坑:改一个上级推广人,为什么可能把分销关系改成死循环?
做 CRMEB Pro 普通分销二开时,很多人看到 spread_uid 字段会觉得很简单:用户 A 的上级推广人就是 spread_uid,要改上级,那就 update 一下这个字段。
但普通分销最怕的,恰恰就是“只改一个字段”。
如果没有校验自己不能绑自己、上级不能是自己的下级、团队分销身份不能混进普通分销、改绑后没有记录推广绑定历史,最后很容易出现推广关系闭环、统计异常、订单返佣口径错乱。
这篇就围绕 CRMEB Pro 普通分销里的添加分销员、修改上级推广人、清除推广关系几个动作,整理一套二开时可以直接参考的安全边界。
一、普通分销的核心字段
普通分销里,最关键的用户字段通常有几个:
is_promoter 是否是分销员 spread_open 是否有推广资格 spread_uid 当前上级推广人 UID spread_time 绑定上级推广人的时间 agent_level 分销员等级其中 spread_uid 是关系链核心,但它不能单独理解。一个用户能不能成为别人的上级,还要看状态、推广资格、分销员身份、团队分销身份等条件。
否则你可能会遇到这种关系:
A 的上级是 B B 的上级是 C 结果后台把 C 的上级改成 A这条链就绕回来了。后续计算一级、二级推广人、推广订单、分销排行时,都会变得很危险。
二、后台普通分销相关接口
后台分销管理相关路由里,普通分销至少有这些高频入口:
GET /adminapi/agent/candidate_user 添加分销员候选用户列表 POST /adminapi/agent/add 添加分销员 GET /adminapi/agent/spread_user 修改上级推广员候选用户列表 PUT /adminapi/agent/spread 修改上级推广人 PUT /adminapi/agent/stair/delete_spread/:uid 清除上级推广人 PUT /adminapi/agent/stair/delete_system_spread/:uid 删除系统推广关系 PUT /adminapi/agent/cancel_promoter/:uid 取消分销员身份这几个接口看起来都是“管理分销员”,但风险等级不一样:
- 添加分销员:主要是打开推广员权限。
- 修改上级推广人:会改变关系链,风险最高。
- 清除上级:会影响直属关系和统计展示。
- 取消分销员身份:会影响推广权限,但不能乱删历史佣金。
三、添加分销员不是简单打开 is_promoter
添加分销员时,不能只做:
$userServices->update($uid,['is_promoter'=>1],'uid');实际至少要校验:
1. 用户存在 2. 用户状态正常 3. 用户有推广资格 spread_open = 1 4. 用户还不是分销员 5. 用户不是团队分销身份 6. 分销等级存在且启用简化后的业务逻辑可以这样理解:
/** * 添加分销员 * @param int $uid 用户UID * @param int $agentLevel 分销等级ID * @return bool */publicfunctionaddAgent(int$uid,int$agentLevel=0):bool{$userInfo=$userServices->getUserInfo($uid,'uid,status,spread_open,is_promoter,division_type,agent_level');if(!$userInfo){thrownewAdminException('用户不存在');}if((int)$userInfo['status']!==1){thrownewAdminException('该用户状态异常,不能添加为分销员');}if((int)$userInfo['spread_open']!==1){thrownewAdminException('该用户没有推广资格');}if((int)$userInfo['is_promoter']===1){thrownewAdminException('该用户已经是分销员');}if((int)$userInfo['division_type']!==0){thrownewAdminException('团队分销身份用户不能添加为普通分销员');}returnfalse!==$userServices->update($uid,['is_promoter'=>1],'uid');}这里最容易漏的是 division_type。普通分销和团队分销如果不隔离,后续返佣、团队统计、上下级关系会互相污染。
四、候选上级列表只能做第一层过滤
后台修改上级推广人时,会先提供候选推广员列表。这个列表通常会筛掉:
- 禁用用户。
- 没有推广资格的用户。
- 不是分销员的用户。
- 团队分销身份用户。
- 当前用户自己。
但是注意:候选列表不能替代提交校验。
因为前端传参可以被绕过,页面上没出现的人,也可以被手动传到接口里。所以后端提交时还要再次校验。
这就是为什么候选接口里即使排除了当前用户,提交接口里仍然要判断:
if($uid===$spreadUid){thrownewAdminException('上级推广人不能为自己');}前端负责体验,后端负责规则。这个边界不要反过来。
五、改绑上级最关键的是防止闭环
修改上级推广人的核心接口可以理解成:
PUT /adminapi/agent/spread 参数:uid, spread_uid业务层关键校验包括:
1. uid 和 spread_uid 都不能为空 2. 上级不能是自己 3. 当前用户必须存在 4. 新上级必须存在且状态正常 5. 新上级必须有推广资格,并且是普通分销员 6. 新上级不能是团队分销身份 7. 新上级不能是当前用户的下级重点是第 7 条:
if($userServices->isSpreadUserChild($uid,$spreadUid)){thrownewAdminException('上级推广人不能为自己下级');}这一步是在防闭环。
如果不做这个校验,用户链路可能从树变成环:
A -> B -> C -> A一旦关系变成环,二级推广统计、团队人数、推广订单归属都可能出现重复、死循环或统计异常。
六、改绑不能只改 spread_uid
改绑上级时,更推荐走统一方法,比如:
return$userServices->saveUserSpreadUid($uid,$spreadUid);而不是业务层随手:
$userServices->update($uid,['spread_uid'=>$spreadUid]);因为统一方法里还会做这些事情:
- 校验用户和上级是否存在。
- 防止自己绑定自己。
- 防止绑定自己的下级。
- 生成完整的推广绑定数据。
- 同步团队/分销关系表。
- 分发推广绑定记录任务。
- 分发好友关系记录任务。
简化理解如下:
/** * 保存用户上级推广人 * @param int $uid 当前用户UID * @param int $spreadUid 上级推广人UID * @return bool */publicfunctionsaveUserSpreadUid(int$uid,int$spreadUid){if($uid===$spreadUid){thrownewValidateException('上级推广人不能为自己');}if($this->isSpreadUserChild($uid,$spreadUid)){thrownewValidateException('上级推广人不能为自己下级');}$data=$this->getSpreadBindData($spreadUid,true);$this->dao->update($uid,$data);app()->make(DivisionRelationServices::class)->syncUserRelation($uid);UserSpreadJob::dispatch([$uid,$spreadUid]);UserFriendsJob::dispatch([$uid,$spreadUid]);returntrue;}这就是统一封装的价值:不是为了少写几行代码,而是为了不漏业务副作用。
七、为什么要记录推广绑定历史
用户改绑上级后,当前关系只看 eb_user.spread_uid 当然够用,但运营排查问题时经常会问:
- 这个用户什么时候绑定过上级?
- 是注册时绑定,还是后台改绑?
- 之前的上级是谁?
- 为什么这笔邀请奖励发给了某个人?
所以改绑时要记录推广绑定历史。项目里通常通过队列任务处理:
UserSpreadJob::dispatch([$uid,$spreadUid]);UserFriendsJob::dispatch([$uid,$spreadUid]);这样当前关系和历史记录是两套口径:
eb_user.spread_uid 当前上级 eb_user_spread 历史绑定记录 好友/邀请关系任务 用于后续统计和消息触达二开时不要只想着当前字段改对了,历史链路也要能追溯。
八、清除上级和取消分销员不是一回事
普通分销里还有两个容易混的动作:
清除上级推广人:处理 spread_uid 取消分销员身份:处理 is_promoter清除上级推广人,是让这个用户不再挂某个上级。它影响的是“我属于谁”。
取消分销员身份,是让这个用户不再具备推广员权限。它影响的是“别人能不能属于我”。
这两个动作不要混成一个按钮,更不要为了取消分销员身份就把历史订单、历史佣金流水、已提现记录一起清掉。
正确边界应该是:
- 当前推广权限可以关闭。
- 当前上级关系可以按规则清除。
- 历史佣金流水不应被删除。
- 历史订单返佣快照不应被清空。
- 下级用户怎么处理要有明确规则,不要隐式连带。
九、后台用户详情里也要防止乱改
除了分销管理页,后台用户详情也可能编辑上级推广人。
这里同样要做防线:
/** * 校验后台用户详情编辑时是否允许修改上级推广人 * @param int $uid 当前用户UID * @param array $userInfo 当前用户信息 * @param int $spreadUid 新上级推广人UID,0表示清除上级 * @return void */protectedfunctioncheckUserEditSpreadUid(int$uid,$userInfo,int$spreadUid):void{if(in_array((int)$userInfo['division_type'],[1,2,3],true)){thrownewAdminException('团队分销身份用户不能在用户详情中修改上级推广人');}if($uid===$spreadUid){thrownewAdminException('上级推广人不能为自己');}if($this->isSpreadUserChild($uid,$spreadUid)){thrownewAdminException('上级推广人不能为自己下级');}}同一条业务规则,不能只在一个入口生效。否则 A 页面拦住了,B 页面又能绕进去。
十、二开测试清单
做普通分销改绑类需求时,建议至少测这些场景:
1. 普通用户添加为分销员 2. 禁用用户不能添加为分销员 3. 团队分销身份不能添加为普通分销员 4. 分销员不能把自己设置为上级 5. 分销员不能把自己的下级设置为上级 6. 上级必须是有效分销员 7. 改绑后 spread_uid 和 spread_time 正确变化 8. 改绑后推广绑定历史有记录 9. 改绑后普通分销统计口径正常 10. 清除上级不会删除历史订单和佣金流水如果涉及线上数据,改绑前最好先记录用户当前关系、订单快照和佣金流水。尤其是大客户环境,不要用临时 SQL 批量改 spread_uid。
总结
CRMEB Pro 普通分销二开里,spread_uid 看起来只是一个上级字段,但它背后连接的是推广链路、分销员身份、绑定历史、好友关系、订单返佣和统计报表。
真正安全的改绑逻辑,至少要做到:
不能绑自己 不能绑自己的下级 不能把团队分销身份混进普通分销 不能只改 spread_uid 而不记录历史 不能因为取消身份就清空历史佣金和订单一句话:普通分销改绑不是一个 update,是一次关系链校验和同步。二开时把这些边界守住,后面统计和返佣才不会出奇怪问题。