如何用PHP和MySQL识别数据集递归收益分成关系并避免死循环
实现方案
不用调整现有表结构即可实现需求,核心是通过链路级已访问节点标记拦截循环引用,同时提前加载规则减少数据库IO,用整数计算规避金额精度问题。
一、表结构相关准备
现有表结构完全满足需求,只需要给revenue_account字段加普通索引,优化单账户规则查询效率即可,不需要新增字段或修改表关系。
二、核心实现逻辑
- 规则预加载:不要每次递归都查询数据库,脚本启动时一次性拉取所有生效的分成规则,组装成「收益账户=>对应分成规则列表」的内存映射,大幅降低IO开销。如果规则总量超过10万条,也可以改成递归时按账户单查,加了索引之后性能足够。
- 循环拦截:递归时通过值传递的数组存储当前分账分支已经经过的所有账户,每次处理下一级分成前,先判断分成接收方是否在当前分支的已访问列表中,如果存在说明出现循环引用,直接跳过该条规则终止当前分支递归,从根源避免无限循环。注意必须用值传递数组,不能用引用传递,否则不同分支的已访问标记会互相污染,导致合法的跨分支同节点被误拦截。
- 精度处理:所有金额计算统一转成最小货币单位(比如美分、分)用整数运算,禁止直接用浮点数乘除,避免浮点数精度误差导致分账金额不平。如果业务需要保留多位小数,可以替换为PHP的
bcmath高精度函数计算。
三、可直接运行的PHP代码示例
规则查询SQL:
SELECT revenue_account, distribution_account, percent FROM 你的分成表名 WHERE is_active = 1;
PHP实现代码:
<?php // 数据库连接 $pdo = new PDO('mysql:host=127.0.0.1;dbname=你的库名;charset=utf8mb4', '数据库账号', '数据库密码'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 加载所有有效分成规则,组装内存映射 $stmt = $pdo->query("SELECT revenue_account, distribution_account, percent FROM 你的分成表名 WHERE is_active = 1"); $ruleMap = []; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $revAccount = $row['revenue_account']; if (!isset($ruleMap[$revAccount])) { $ruleMap[$revAccount] = []; } $ruleMap[$revAccount][] = [ 'dist_account' => $row['distribution_account'], 'percent' => (float)$row['percent'] ]; } /** * 递归计算全链路分成 * @param string $currentAccount 当前产生可分收益的账户 * @param int $amount 当前账户待分账的金额,单位:美分 * @param array $visitedPath 当前分账分支已遍历的账户列表,用于拦截循环 * @param array $result 最终分账结果,引用传递累计金额 * @param array $ruleMap 全量分成规则映射 * @param int $maxDepth 最大递归深度兜底,防止栈溢出 * @param int $currentDepth 当前递归深度 * @return void */ function calculateSplit( string $currentAccount, int $amount, array $visitedPath, array &$result, array $ruleMap, int $maxDepth = 20, int $currentDepth = 0 ): void { // 超过最大递归深度直接终止,兜底防护 if ($currentDepth > $maxDepth) { return; } // 将当前账户加入已访问路径 $visitedPath[] = $currentAccount; // 当前账户无分成规则,终止分支 if (!isset($ruleMap[$currentAccount])) { return; } foreach ($ruleMap[$currentAccount] as $rule) { $distAccount = $rule['dist_account']; // 分成方已在当前链路中,出现循环,直接跳过 if (in_array($distAccount, $visitedPath, true)) { continue; } // 计算当前规则分成金额,整数运算避免精度问题 $splitAmount = (int)round($amount * $rule['percent'] / 100); if ($splitAmount <= 0) { continue; } // 累计分成金额 if (!isset($result[$distAccount])) { $result[$distAccount] = 0; } $result[$distAccount] += $splitAmount; // 递归处理分成方的下游分账,已访问路径值传递,各分支互不干扰 calculateSplit( $distAccount, $splitAmount, $visitedPath, $result, $ruleMap, $maxDepth, $currentDepth + 1 ); } } // 业务调用示例:acc_MVHYsAvJ5c9hjura产生100美元收益=10000美分 $splitResult = []; calculateSplit('acc_MVHYsAvJ5c9hjura', 10000, [], $splitResult, $ruleMap); // 输出结果(单位转美元) foreach ($splitResult as $account => $totalCent) { echo sprintf("账户%s 应得分成:%.2f美元\n", $account, $totalCent / 100); }
四、逻辑验证(基于给出的示例数据)
初始账户acc_MVHYsAvJ5c9hjura产生100美元收益时,执行逻辑如下:
- 第一分支:主账户给
acc_ndk1tzj57phsfcwv分10%(10美元)→ 该账户给acc_sPvybHKQUw1J2nVA分10%(1美元)→ 该账户给acc_28PGgHMdRskY4hnf分10%(0.1美元)→ 该账户给acc_8XENmdYBRygQxubA分10%(0.01美元)→ 该账户的分成方是初始账户acc_MVHYsAvJ5c9hjura,检测到已在当前链路中,直接终止该分支,不会循环。 - 第二分支:主账户给
acc_sPvybHKQUw1J2nVA分6%(6美元)→ 该账户给acc_28PGgHMdRskY4hnf分10%(0.6美元)→ 后续链路同上,检测到循环时自动终止。
两个分支独立计算,不会互相干扰,也不会出现无限递归。
内容的提问来源于stack exchange,提问作者user1730452
相关产品推荐
相关产品推荐

