// +---------------------------------------------------------------------- declare (strict_types = 1); namespace app\member\controller; use think\facade\Event; use think\Request; use ywxapp\controller\MemberBase; use app\member\validate\Payment as PaymentValidate; /** * 支付控制器 * * 设计原则:核心只负责「订单/流程编排 + 事件触发」,具体支付方式由【支付插件】实现, * 通过监听以下事件注入能力(插件在 info.php 的 events 中声明监听器即可): * * - PaymentMethods 收集可用支付方式。监听器返回 array,如: * [['code'=>'wechat','name'=>'微信支付','desc'=>'...','icon'=>'','sort'=>0], ...] * - PaymentCreate 生成支付参数。监听器接收 ['order'=>[...],'params'=>[...]], * 返回网关参数 array(按需返回 null 表示不匹配该方法),如: * ['type'=>'qrcode','qrcode_url'=>'weixin://...','expire'=>600] * ['type'=>'redirect','redirect_url'=>'https://...'] * ['type'=>'form','html'=>'
'] * - PaymentNotify 异步回调。监听器接收 ['method'=>...,'request'=>[...],'get'=>[...]], * 完成验签与订单激活后,返回网关要求的响应字符串(如 'success')。 * - PaymentFreeActive 免费套餐开通钩子(无支付)。 * - PaymentOrderCreate 订单落库钩子(插件可在此持久化订单)。 * * 未安装任何支付插件时,create() 会返回「暂无可用支付方式」提示。 */ class Payment extends MemberBase { /** * 套餐配置(来自 config/member.php,价格单位:元,duration 单位:天) * @var array */ private $plans = []; /** * 支付页 / 套餐列表(免登录可看) * @var array */ protected $noNeedLogin = ['index', 'methods']; /** * @var array */ protected $noNeedVerify = ['*']; protected function initialize() { $this->plans = config('member.plans', []); } /** * 支付页面 */ public function index() { $this->view->assign('plans', array_values($this->plans)); return $this->view->fetch(); } /** * 获取可用支付方式(插件通过 PaymentMethods 事件注入) */ public function methods() { $results = Event::trigger('PaymentMethods', []); $methods = []; $codes = []; foreach ((array)$results as $r) { if (!is_array($r)) { continue; } foreach (array_values($r) as $m) { if (!is_array($m) || empty($m['code']) || in_array($m['code'], $codes, true)) { continue; } $codes[] = $m['code']; $methods[] = $m; } } // 按 sort 排序 usort($methods, function ($a, $b) { return ($a['sort'] ?? 0) <=> ($b['sort'] ?? 0); }); // 兜底:若没有任何插件注入支付方式(如插件系统未初始化),至少提供「个人免签」体验方式 if (empty($methods)) { $methods[] = [ 'code' => 'personal', 'name' => '个人免签', 'desc' => '扫码转账到个人收款码,站长确认到账后开通', 'icon' => '', 'sort' => 0, ]; } $this->result->success($methods); } /** * 创建支付订单 * 1) 免费套餐:直接触发 PaymentFreeActive,无需支付 * 2) 付费套餐:触发 PaymentOrderCreate(落库钩子)+ PaymentCreate(网关参数钩子) */ public function create() { if (!($this->request->isAjax() && $this->request->isPost())) { $this->result->error('访问错误'); } $user = $this->auth->model; $planId = (int)$this->request->param('plan_id', 0); $method = (string)$this->request->param('method', ''); $validate = new PaymentValidate(); if (! $validate->check(['plan_id' => $planId, 'method' => $method])) { $this->result->error($validate->getError()); } $plan = $this->getPlan($planId); if (!$plan) { $this->result->error('套餐不存在', 1); } // 免费套餐直接开通 if ($plan['price'] <= 0) { Event::trigger('PaymentFreeActive', ['member' => $user, 'plan' => $plan]); $this->result->success(['type' => 'free', 'plan' => $plan['title']]); } if (!$method) { $this->result->error('请选择支付方式', 1); } $order = [ 'order_sn' => $this->genOrderSn($user->uid), 'uid' => $user->uid, 'plan_id' => $planId, 'plan_title' => $plan['title'], 'amount' => $plan['price'], 'method' => $method, 'create_at' => time(), ]; // 订单落库钩子(插件可持久化订单) Event::trigger('PaymentOrderCreate', ['order' => $order]); // 生成支付参数:由各支付插件监听 PaymentCreate 返回网关数据 $results = Event::trigger('PaymentCreate', [ 'order' => $order, 'params' => $this->request->param(), ]); $gateway = null; foreach ((array)$results as $r) { if (is_array($r) && !empty($r)) { $gateway = $r; break; } } if (empty($gateway)) { $this->result->error('暂无可用支付方式,请先安装支付插件', 1); } $this->result->success($gateway); } /** * 异步回调(由各支付插件处理具体网关通知) * 插件监听 PaymentNotify 完成验签与订单激活,并返回网关要求的响应体。 */ public function notify() { $method = (string)$this->request->param('method', ''); $results = Event::trigger('PaymentNotify', [ 'method' => $method, 'request' => $this->request->param(), 'get' => $this->request->get(), 'post' => $this->request->post(), ]); foreach ((array)$results as $r) { if (is_string($r) && $r !== '') { return $r; } } return 'success'; } /** * 根据套餐 id 获取套餐配置 */ private function getPlan(int $id): ?array { return $this->plans[$id] ?? null; } /** * 生成订单号 */ private function genOrderSn(int $uid): string { return date('YmdHis') . $uid . mt_rand(100, 999); } }