fix(pay): 修复区域代理分佣脏数据导致支付事务回滚与订单未付款问题并增加核心支付隔离保护

这个提交包含在:
wikiboss
2026-09-12 18:37:01 +08:00
父节点 65290f5742
当前提交 deb981a885
共有 5 个文件被更改,包括 194 次插入 和 14 次删除

查看文件

@@ -0,0 +1,140 @@
<?php
/**
* 订单支付流程与区域代理分佣健壮性单元测试 (TDD)
*/
class PaymentProcessRobustnessTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "=== 开始执行 订单支付与区域代理健壮性单元测试 (TDD) ===\n";
$this->testRewardSettleValidatesUserAndCatchesException();
$this->testCommissionMatchFiltersInvalidUserAgent();
$this->testOrderOperCorrectsSaveUserMessageUserId();
$this->testOrderOperIsolatesSideEffectsFromCorePayment();
$this->testHuifuNotifyHandlerExtractsHttpResponseException();
echo "\n测试结果: {$this->passed} 通过, {$this->failed} 失败\n";
if ($this->failed > 0) {
exit(1);
}
}
private function assert($condition, $message)
{
if ($condition) {
echo " [PASS] {$message}\n";
$this->passed++;
} else {
echo " [FAIL] {$message}\n";
$this->failed++;
}
}
/**
* 测试 1:Reward::settleReward 必须校验代理用户是否存在,且发放佣金必须有 try-catch 隔离保护
*/
public function testRewardSettleValidatesUserAndCatchesException()
{
$filePath = __DIR__ . '/../addons/shopro/library/regional_agent/Reward.php';
$this->assert(is_file($filePath), "Reward.php 文件必须存在");
$content = file_get_contents($filePath);
$startPos = strpos($content, 'function settleReward(');
$this->assert($startPos !== false, "Reward.php 必须包含 settleReward 方法");
$endPos = strpos($content, 'public function ', $startPos + 20);
$methodBody = $endPos !== false ? substr($content, $startPos, $endPos - $startPos) : substr($content, $startPos);
// 检查是否有对用户存在的校验
$hasUserCheck = (strpos($methodBody, 'UserModel::get') !== false || strpos($methodBody, 'UserModel::where') !== false || strpos($methodBody, '$agent->user') !== false);
$this->assert($hasUserCheck, "settleReward 必须校验代理绑定的用户是否存在");
// 检查是否有 try-catch 保护 UserModel::money
$hasTryCatch = preg_match('/try\s*\{[^}]*UserModel::money.+?\}\s*catch\s*\(\\\\?Throwable\s+\$e\)/s', $methodBody);
$this->assert((bool)$hasTryCatch, "settleReward 必须使用 try-catch(\\Throwable \$e) 隔离佣金发放异常");
}
/**
* 测试 2:Commission::matchRegionalAgents 必须过滤绑定的用户不存在的无效代理
*/
public function testCommissionMatchFiltersInvalidUserAgent()
{
$filePath = __DIR__ . '/../addons/shopro/library/regional_agent/Commission.php';
$content = file_get_contents($filePath);
$startPos = strpos($content, 'function matchRegionalAgents(');
$this->assert($startPos !== false, "Commission.php 必须包含 matchRegionalAgents 方法");
$endPos = strpos($content, 'function isGoodsRegionalEnabled(', $startPos + 20);
$methodBody = $endPos !== false ? substr($content, $startPos, $endPos - $startPos) : substr($content, $startPos);
$hasCheck = (strpos($methodBody, 'user') !== false && (strpos($methodBody, 'UserModel::get') !== false || strpos($methodBody, '$agent->user') !== false));
$this->assert($hasCheck, "matchRegionalAgents 必须校验代理的 user 是否存在有效");
}
/**
* 测试 3:OrderOper::paymentProcess 必须修复 saveUserMessage 的 user_id 为 $order->user_id
*/
public function testOrderOperCorrectsSaveUserMessageUserId()
{
$filePath = __DIR__ . '/../addons/shopro/library/traits/model/order/OrderOper.php';
$content = file_get_contents($filePath);
$startPos = strpos($content, 'function paymentProcess(');
$this->assert($startPos !== false, "OrderOper.php 必须包含 paymentProcess 方法");
$endPos = strpos($content, 'public function refundProcess(', $startPos + 20);
if ($endPos === false) {
$endPos = strpos($content, 'function refundProcess(', $startPos + 20);
}
$methodBody = $endPos !== false ? substr($content, $startPos, $endPos - $startPos) : substr($content, $startPos);
$hasBug = strpos($methodBody, "'user_id' => \$order->id") !== false;
$this->assert(!$hasBug, "paymentProcess 不能将 \$order->id 赋值给 user_id (应使用 \$order->user_id)");
$hasFix = strpos($methodBody, "'user_id' => \$order->user_id") !== false;
$this->assert($hasFix, "paymentProcess 必须使用 \$order->user_id 保存用户消息");
}
/**
* 测试 4:OrderOper::paymentProcess 必须对下游副作用(发消息、操作日志、order_payed_after 钩子)进行独立隔离保护
*/
public function testOrderOperIsolatesSideEffectsFromCorePayment()
{
$filePath = __DIR__ . '/../addons/shopro/library/traits/model/order/OrderOper.php';
$content = file_get_contents($filePath);
$startPos = strpos($content, 'function paymentProcess(');
$endPos = strpos($content, 'function refundProcess(', $startPos + 20);
if ($endPos === false) {
$endPos = strpos($content, 'public function refundProcess(', $startPos + 20);
}
$methodBody = $endPos !== false ? substr($content, $startPos, $endPos - $startPos) : substr($content, $startPos);
// 验证 $order->save() 在钩子和发信之前已经执行(核心状态先落地)
$savePos = strpos($methodBody, '$order->save()');
$hookPos = strpos($methodBody, "Hook::listen('order_payed_after'");
$this->assert($savePos !== false && $hookPos !== false && $savePos < $hookPos, "核心订单状态必须在执行 order_payed_after 钩子前保存");
// 验证 Hook::listen('order_payed_after') 必须处于 try-catch 保护下
$hasHookCatch = preg_match('/try\s*\{[^}]*Hook::listen\(\'order_payed_after\'.+?\}\s*catch\s*\(\\\\?Throwable\s+\$e\)/s', $methodBody);
$this->assert((bool)$hasHookCatch, "Hook::listen('order_payed_after') 必须在 try-catch(\\Throwable \$e) 保护下执行,不能回滚已付款订单");
}
/**
* 测试 5:HuifuNotifyHandler::handlePayNotify 遇 HttpResponseException 需解析响应信息记录完整日志
*/
public function testHuifuNotifyHandlerExtractsHttpResponseException()
{
$filePath = __DIR__ . '/../addons/shopro_huifu/library/HuifuNotifyHandler.php';
$content = file_get_contents($filePath);
$hasExtraction = strpos($content, 'HttpResponseException') !== false && strpos($content, 'getResponse') !== false;
$this->assert($hasExtraction, "HuifuNotifyHandler 必须捕获并解析 HttpResponseException 响应内容,避免空白错误日志");
}
}
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['SCRIPT_FILENAME'] ?? '')) {
(new PaymentProcessRobustnessTest())->run();
}