PAYATHON 2026

调用 Payment API 时出现 BadMethodCallException

支付阿杰

结论

$this->checkDsa(...) 调用的是当前控制器 DsaAPIController 的实例方法。控制器没有定义 checkDsa(),也没有通过 Trait 引入这个方法,因此 Laravel 抛出以下异常:

BadMethodCallException: Method App\Http\Controllers\API\DsaAPIController::checkDsa does not exist.

同名的 Artisan Command 和 middleware 都是独立类,不会自动变成控制器的方法。该怎么处理,要看这段逻辑的用途:

  • 如果只需判断用户是否已有有效 DSA,可以直接查询模型,也可以把逻辑放进 Service 或模型方法。
  • 如果要在进入接口前统一检查,应注册并挂载 middleware。
  • 如果确实需要执行 Artisan Command,可以使用 Artisan::call(),但 Command 不适合用作控制器里的布尔判断方法。

推荐做法:封装查询逻辑

如果 paymentStatus() 需要检查当前用户是否已有有效 DSA,可以先在控制器中定义一个方法:

use App\Models\Dsa;
use Illuminate\Support\Facades\Auth;

private function checkDsa(int $userId): bool
{
    return Dsa::where('user_id', $userId)
        ->where('status', config('constants.subscriptions.status.1'))
        ->exists();
}

然后在 paymentStatus() 中调用:

public function paymentStatus(
    PaymentStatusRequest $request,
    Dsa $dsa_payment
) {
    $userId = Auth::guard('api')->id();

    if ($this->checkDsa($userId)) {
        return $this->getMessage(
            [],
            config('constants.messages.errors.already_dsa'),
            config('constants.validation_codes.unprocessable_entity'),
            false
        );
    }

    if (
        $dsa_payment->status == config('constants.dsa_payments.payment_status.1')
        || $dsa_payment->user_id != $userId
    ) {
        return $this->getMessage(
            [],
            config('constants.messages.errors.processing_old_request'),
            config('constants.validation_codes.unprocessable_entity'),
            false
        );
    }

    // 后续支付处理逻辑……
}

原来的调用方式:

$this->checkDsa(DSA::class, 'user_id', Auth::guard('api')->id())

只有在控制器或其父类中已经定义了一个接收这三个参数的 checkDsa() 方法时才有效。Laravel 不会根据类名、字段名和字段值自动创建这个方法。

如果多个控制器都需要这段逻辑,放到 Service 中更合适:

namespace App\Services;

use App\Models\Dsa;

class DsaService
{
    public function isActiveForUser(int $userId): bool
    {
        return Dsa::where('user_id', $userId)
            ->where('status', config('constants.subscriptions.status.1'))
            ->exists();
    }
}

控制器可以通过依赖注入使用该 Service:

use App\Services\DsaService;

public function paymentStatus(
    PaymentStatusRequest $request,
    Dsa $dsa_payment,
    DsaService $dsaService
) {
    $userId = Auth::guard('api')->id();

    if ($dsaService->isActiveForUser($userId)) {
        return $this->getMessage(
            [],
            config('constants.messages.errors.already_dsa'),
            config('constants.validation_codes.unprocessable_entity'),
            false
        );
    }

    // 后续逻辑……
}

如果要使用 middleware

Middleware 的 handle() 方法由 Laravel 的 HTTP 管道调用,不能通过 $this->checkDsa() 来执行。

先修正 middleware 中变量名不一致的问题。当前代码定义了 $is_subscribe,返回时却使用未定义的 $is_dsa。可以统一改成 $isDsa:

class CheckDsa
{
    use MessagesTrait;

    public function handle(Request $request, Closure $next)
    {
        $user = Auth::guard('api')->user();

        $isDsa = $user
            && $user->dsa_payment()
                ->where(
                    'status',
                    config('constants.dsa_payment.status.1')
                )
                ->exists();

        return $isDsa
            ? $next($request)
            : $this->getMessage(
                [],
                config('constants.messages.errors.not_dsa'),
                config('constants.validation_codes.forbidden'),
                false
            );
    }
}

接下来为 middleware 配置别名。Laravel 版本不同,注册位置也可能不同。

较早的版本通常在 app/Http/Kernel.php 中注册:

protected $routeMiddleware = [
    'check.dsa' => \App\Http\Middleware\CheckDsa::class,
];

部分后续版本使用 $middlewareAliases:

protected $middlewareAliases = [
    'check.dsa' => \App\Http\Middleware\CheckDsa::class,
];

注册后将其挂载到路由:

Route::post('/payment/status', [DsaAPIController::class, 'paymentStatus'])
    ->middleware(['auth:api', 'check.dsa']);

也可以直接传入 middleware 类名:

Route::post('/payment/status', [DsaAPIController::class, 'paymentStatus'])
    ->middleware([
        'auth:api',
        \App\Http\Middleware\CheckDsa::class,
    ]);

这里还要注意业务条件的方向。你给出的 middleware 只允许已经具备有效 DSA 的用户继续访问,而控制器的逻辑是在用户已有 DSA 时返回 already_dsa。两者的条件正好相反,不能同时照搬。需要先确认这个支付接口究竟允许未订阅用户访问,还是允许已订阅用户访问。

Artisan Command 不能当成控制器方法

在 Console\Kernel 中注册:

protected $commands = [
    Commands\CheckDsa::class,
];

这只表示该命令可以从命令行运行:

php artisan check:is_dsa

它不会向 DsaAPIController 添加 checkDsa() 方法。

应用代码在技术上可以调用这个命令:

use Illuminate\Support\Facades\Artisan;

$exitCode = Artisan::call('check:is_dsa');

不过,Artisan::call() 返回的是命令退出码,不是表示“用户是否已有 DSA”的布尔值。Command 更适合定时扫描、批量处理或后台维护任务,不适合承担 HTTP 请求中的业务判断。

如果 Command 和控制器需要使用相同的检查逻辑,应让它们共同调用 DsaService,而不是由控制器调用 Command:

public function handle(DsaService $dsaService): int
{
    // 调用 service 执行批量检查或处理

    return self::SUCCESS;
}

代码中还需要检查的问题

原代码还有几个与 checkDsa 异常无关,但可能引发其他错误的问题:

  1. 当 checkDsa() 返回 false 时,dd('poojan') 会立即终止请求。调试完成后应将其删除。

  2. middleware 中的 $is_subscribe 与 $is_dsa 名称不一致,会导致未定义变量错误。

  3. 代码中出现了三个不同的配置键:

constants.dsa_payment.status.1
constants.dsa_payments.payment_status.1
constants.subscriptions.status.1

需要确认这些键都已定义,并且各自对应正确的状态含义。

  1. 代码使用了 $dsa_plan:
$plan = Plan::find($dsa_plan->plan_id);

但给出的 paymentStatus() 片段并未定义 $dsa_plan,执行到这里时可能出现未定义变量错误。

  1. PHP 类名通常不区分大小写,但最好始终使用模型实际声明的名称:
Dsa::class

不要混用 DSA::class 和 Dsa::class。

较合适的代码结构是:Service 保存可复用的 DSA 判断逻辑,middleware 负责请求访问控制,Command 处理命令行或计划任务,控制器负责组织支付接口的处理流程。

备注:内容仅供参考。