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(); }