Skip to content

🧾 支付业务对接

业务模块通过 System 支付基础能力统一创建支付单、调起支付、接收已支付/已退款回调。本页讲业务侧对接代码。基础能力组成、驱动契约、回调分发详见 支付基础能力

🎯 本页目标

读完本章节后,应能掌握:

  • 业务模块如何创建支付单、把订单交给支付基础能力。
  • 前端在小程序、H5、App 三端如何统一调起支付。
  • 业务处理器怎么实现、怎么注册、为什么必须幂等。
  • 退款单怎么创建、退款回调怎么接收。
  • 主动查单的适用场景和限制。

▶️ 发起支付

业务模块创建支付单时要明确业务归属。通常不需要传入 driver,支付基础能力会读取后台“应用接入 / 支付接入”中选择的默认支付驱动:

php
use App\Core\Attributes\Inject;
use Modules\System\Application\Services\Pay\PayOrderService;

class CourseOrderPayAction
{
    #[Inject]
    protected PayOrderService $payOrderService;

    public function pay(object $courseOrder, object $user): array
    {
        return $this->payOrderService->createAndPay([
            'module' => 'Course',
            'scene' => 'course_order',
            'biz_type' => 'course',
            'biz_no' => $courseOrder->order_no,
            'title' => $courseOrder->title,
            'user_id' => $user->id,
            'channel' => 'mini',
            'amount' => $courseOrder->amount,
            'pay_amount' => $courseOrder->pay_amount,
        ]);
    }
}

上面的示例没有传入 driver,支付基础能力会自动使用后台配置的默认支付驱动。只有少数需要覆盖默认通道的场景才建议显式传入 driver,例如某个模块固定使用独立商户或特定支付渠道。

📱 前端调起支付

uni-app 端提供统一支付工具,业务页面只需要把后端返回的支付参数交给工具处理:

ts
import { getPayChannel, requestPayment } from '@/utils/payment'

const result = await payOrder({
  id: orderId,
  channel: getPayChannel()
})

await requestPayment(result)

getPayChannel() 根据当前端类型返回 minimph5apprequestPayment() 处理小程序 uni.requestPayment 和 H5 支付链接。业务页面不需要重复判断端类型。

🧑‍🔧 业务处理器

业务模块通过 PayBusinessHandlerInterface 接收已支付和已退款事件:

php
namespace Modules\Course\Application\Services\Pay;

use App\Core\Attributes\Inject;
use Illuminate\Support\Facades\DB;
use Modules\Course\Dao\Order\CourseOrderDao;
use Modules\System\Application\Services\Pay\Contracts\PayBusinessHandlerInterface;
use Modules\System\Domain\Models\Pay\PayOrder;

class CourseOrderPayHandler implements PayBusinessHandlerInterface
{
    #[Inject]
    protected CourseOrderDao $courseOrderDao;

    public function supports(PayOrder $order): bool
    {
        return $order->module === 'Course'
            && $order->scene === 'course_order'
            && $order->biz_type === 'course';
    }

    public function handlePaid(PayOrder $order, array $payload): void
    {
        DB::transaction(function () use ($order) {
            $courseOrder = $this->courseOrderDao->query()
                ->where('order_no', $order->biz_no)
                ->lockForUpdate()
                ->firstOrFail();

            if ((int) $courseOrder->pay_status === 1) {
                return;
            }

            $courseOrder->pay_status = 1;
            $courseOrder->pay_time = now();
            $courseOrder->pay_order_no = $order->order_no;
            $courseOrder->channel_order_no = $order->channel_order_no;
            $courseOrder->save();
        });
    }

    public function handleRefunded(PayOrder $order, array $payload): void
    {
        DB::transaction(function () use ($order, $payload) {
            $courseOrder = $this->courseOrderDao->query()
                ->where('order_no', $order->biz_no)
                ->lockForUpdate()
                ->firstOrFail();

            $courseOrder->refund_callback = $payload['resource'] ?? [];
            $courseOrder->save();
        });
    }
}

模块服务提供者负责注册业务处理器:

php
public function register(): void
{
    $handlers = config('sunadmin.pay.biz_handlers', []);
    $handlers[] = CourseOrderPayHandler::class;

    config()->set('sunadmin.pay.biz_handlers', array_values(array_unique($handlers)));
}

处理器必须保证幂等。渠道可能重复推送支付或退款回调,业务处理器应通过业务状态、唯一流水或行锁避免重复发放权益。

💸 退款中心

退款基础能力在 System 中提供,后台入口是:

text
财务管理 / 退款中心

退款中心面向所有模块共用。业务模块只需要创建退款单,退款中心会根据支付单上的 driver 调用对应支付模块提交退款。退款成功后,回调仍按 PayBusinessHandlerInterface::handleRefunded() 分发给业务模块。

退款支持部分退款和多次退款。业务模块在创建退款前应计算订单剩余可退款金额,并把退款金额写入统一退款单。

🔎 主动查单

真实支付回调是支付状态流转的主入口。前端在拉起支付后,可以调用业务模块提供的查单接口,由后端使用 PayOrderService::syncPaidFromDriver() 主动向支付驱动确认支付状态。

主动查单用于处理前端页面返回、H5 支付跳转后用户回到页面、或渠道回调延迟等场景。查单确认支付成功后,会按与支付回调一致的统一支付单状态写入,并记录来源为驱动查单的回调日志。

主动查单不能替代渠道支付回调。支付回调必须保持可访问、可验签、可写入 pay_callback_logs,否则退款、对账和重复通知排查都会缺少渠道原始依据。

🧯 排查顺序

问题排查
提示支付驱动未安装检查对应支付模块是否安装、system_modules.status 是否为 1
前端无法拉起支付pay_payload_json 是否为空、driverchannel 是否正确
渠道没有回调检查回调 URL、证书、商户配置、服务器访问
回调验签失败检查支付模块配置、平台证书、密钥
业务状态没更新检查 pay_orders.status、回调日志、业务 handler 是否 supports()
退款状态没更新检查 pay_refunds.status、退款回调日志、业务 handler 是否实现 handleRefunded()
重复回调确认 handler 幂等,不要重复发放权益