210 lines
7.0 KiB
PHP
210 lines
7.0 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
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'=>'<form ...>']
|
||
* - 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);
|
||
}
|
||
}
|