更新文件

This commit is contained in:
技术老胡 2025-04-23 23:51:21 +08:00
parent a6b5133cc3
commit e524424628
29 changed files with 2388 additions and 6 deletions

7
.gitignore vendored
View File

@ -2,17 +2,12 @@
*.env
extend/*
!extend/ImgCaptcha.php
!extend/payclient.zip
!extend/Plugin.php
vendor/*
!vendor/vendor.zip
runtime/*
app/*
!app/controller/TestController.php
app/controller/TestController.php
# 忽略 config/extend 目录下所有文件
config/extend/*

22
app/AppService.php Normal file
View File

@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}

93
app/BaseController.php Normal file
View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace app;
use think\App;
use think\exception\ValidateException;
use think\Validate;
/**
* 控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 控制器中间件
* @var array
*/
protected $middleware = ['Auth'];
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize() {}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @return array|string|true
* @throws ValidateException
*/
protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false)
{
if (is_array($validate)) {
$v = new Validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
[$validate, $scene] = explode('.', $validate);
}
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
$v = new $class();
if (!empty($scene)) {
$v->scene($scene);
}
}
$v->message($message);
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
return $v->failException(true)->check($data);
}
}

19
app/BaseModel.php Normal file
View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace app;
use think\Model;
use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class BaseModel extends Model
{
use SoftDelete;
protected $deleteTime = 'delete_time';
protected $autoWriteTimestamp = 'timestamp';
}

58
app/ExceptionHandle.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace app;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\Response;
use Throwable;
/**
* 应用异常处理类
*/
class ExceptionHandle extends Handle
{
/**
* 不需要记录信息(日志)的异常类列表
* @var array
*/
protected $ignoreReport = [
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
DataNotFoundException::class,
ValidateException::class,
];
/**
* 记录异常信息(包括日志或者其它方式记录)
*
* @access public
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception): void
{
// 使用内置的方式记录异常日志
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @access public
* @param \think\Request $request
* @param Throwable $e
* @return Response
*/
public function render($request, Throwable $e): Response
{
// 添加自定义异常处理机制
// 其他错误交给系统处理
return parent::render($request, $e);
}
}

8
app/Request.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}

10
app/common.php Normal file
View File

@ -0,0 +1,10 @@
<?php
// 应用公共文件
function backMsg($code = 0, $msg = '', $data = []): array
{
$back_msg = ['code' => $code, 'msg' => $msg];
if ($data) {
$back_msg['data'] = $data;
}
return $back_msg;
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace app\controller;
use app\BaseController;
use app\model\Order;
use think\facade\View;
class ConsoleController extends BaseController
{
// 后台主页
public function index()
{
View::assign('version', 'V1');
return View::fetch();
}
// 管理菜单
public function menu()
{
// 加载菜单配置
$menu = \think\facade\Config::load("extend/menu", 'extend');
return json($menu);
}
// 管理菜单
public function message()
{
// 加载菜单配置
$message = \Plugin::getNotifyMessage();
if (empty($message)) {
$message = [
["id" => 1, "title" => "应用更新", "children" => []],
["id" => 2, "title" => "官方消息", "children" => []],
];
}
return json($message);
}
// 首页仪表盘
public function console()
{
// 查询近32天的订单
$orders = Order::where([['state', '=', 1], ['create_time', '>', date('Y-m-d 00:00:00', strtotime('-32 days'))]])->select();
$income = $this->getRevenueData($orders);
View::assign($income);
$servertime = date('Y-m-d H:i:s', time());
View::assign('servertime', $servertime);
return View::fetch();
}
// 获取收入数据总览
private function getRevenueData($orders)
{
// 时间段
$month_start = date('Y-m-01 00:00:00');
$month_end = date('Y-m-d 23:59:59', strtotime('last day of this month'));
$week_start = date('Y-m-d 00:00:00', strtotime('monday this week'));
$week_end = date('Y-m-d 23:59:59', strtotime('next monday') - 1);
$yesterday_start = date('Y-m-d 00:00:00', strtotime('yesterday'));
$yesterday_end = date('Y-m-d 23:59:59', strtotime('yesterday'));
$today_start = date('Y-m-d 00:00:00');
$today_end = date('Y-m-d 23:59:59');
// 本月流水
$month_income = $orders->whereBetween('create_time', [$month_start, $month_end])->column('really_price');
// 本周流水
$week_income = $orders->whereBetween('create_time', [$week_start, $week_end])->column('really_price');
// 昨日流水
$yesterday_income = $orders->whereBetween('create_time', [$yesterday_start, $yesterday_end])->column('really_price');
// 今天流水
$today_income = $orders->whereBetween('create_time', [$today_start, $today_end])->column('really_price');
// 收入数据
$income = [
'month_income' => \array_sum($month_income),
'week_income' => \array_sum($week_income),
'yesterday_income' => \array_sum($yesterday_income),
'today_income' => \array_sum($today_income),
];
return $income;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace app\controller;
use think\facade\Log;
use think\facade\View;
class IndexController
{
public function index()
{
if (session('?nickname')) {
$nickname = session('nickname');
View::assign('nickname', $nickname);
}
return View::fetch();
}
public function doc()
{
View::assign('domain', \request()->domain());
return View::fetch();
}
}

View File

@ -0,0 +1,335 @@
<?php
declare(strict_types=1);
namespace app\controller;
use think\facade\Db;
use think\Request;
use think\facade\View;
use think\facade\Log;
use think\exception\ValidateException;
use think\Validate;
class InstallController
{
private const INSTALL_LOCK_FILE = 'install.lock';
/**
* 连接数据库
* @return \think\db\Connection
*/
private function connectDatabase()
{
return Db::connect();
}
/**
* 首页,检查是否已安装,若已安装则跳转到登录页,否则显示安装页面
* @return \think\response\Redirect|\think\response\View
*/
public function index()
{
if ($this->checkLock()) {
return redirect('User/login');
}
return View::fetch();
}
/**
* 安装操作,检查环境、保存数据库配置信息
* @param Request $request
* @return \think\response\Json
*/
public function install(Request $request)
{
if ($this->checkLock()) {
return json(backMsg(1, '已经安装'));
}
$envCheck = $this->checkEnvironment();
if ($envCheck !== true) {
return json(backMsg(1, $envCheck));
}
$dbConfig = $request->post();
try {
$this->validateDbConfig($dbConfig);
$this->saveDbConfig($dbConfig);
return json(backMsg(0, '配置保存成功'));
} catch (ValidateException $e) {
return json(backMsg(1, $e->getMessage()));
} catch (\Exception $e) {
Log::error("保存数据库配置失败: " . $e->getMessage());
return json(backMsg(1, '配置保存失败'));
}
}
/**
* 初始化数据库,创建表并初始化数据
* @param Request $request
* @return \think\response\Json
*/
public function init(Request $request)
{
if ($this->checkLock()) {
return json(backMsg(1, '已经安装'));
}
$dbConfig = $request->post();
$startTime = microtime(true);
try {
$this->validateInitData($dbConfig);
$this->connectDatabase()->transaction(function () use ($dbConfig) {
$this->createTables();
$this->initData($dbConfig);
});
$this->setLock();
$endTime = microtime(true);
Log::info("数据库初始化完成,耗时: " . ($endTime - $startTime) . "");
return json(backMsg(0, '安装成功'));
} catch (ValidateException $e) {
return json(backMsg(1, $e->getMessage()));
} catch (\Exception $e) {
Log::error("数据库初始化失败: " . $e->getMessage());
return json(backMsg(1, '数据库初始化失败'));
}
}
/**
* 检查环境,包括 PHP 版本、文件上传写入权限、Fileinfo 扩展
* @return bool|string
*/
private function checkEnvironment()
{
if (version_compare(PHP_VERSION, '8.0', '<')) {
return 'PHP 版本必须大于等于 8.0';
}
if (!is_writable(sys_get_temp_dir())) {
return '文件上传目录没有写入权限';
}
if (!extension_loaded('fileinfo')) {
return 'Fileinfo 扩展未安装';
}
return true;
}
/**
* 验证数据库配置信息
* @param array $dbConfig
* @throws ValidateException
*/
private function validateDbConfig(array $dbConfig)
{
$validate = new Validate();
$rule = [
'host' => 'require',
'name' => 'require',
'user' => 'require',
'pass' => 'require',
'port' => 'require|integer',
];
if (!$validate->rule($rule)->check($dbConfig)) {
throw new ValidateException($validate->getError());
}
}
/**
* 验证初始化数据信息
* @param array $dbConfig
* @throws ValidateException
*/
private function validateInitData(array $dbConfig)
{
$validate = new Validate();
$rule = [
'nickname' => 'require',
'username' => 'require',
'password' => 'require'
];
if (!$validate->rule($rule)->check($dbConfig)) {
throw new ValidateException($validate->getError());
}
}
/**
* 保存数据库配置信息到 .env 文件
* @param array $dbConfig
* @throws \Exception
*/
private function saveDbConfig(array $dbConfig)
{
$envPath = app()->getRootPath() . '.env';
$envContent = $this->generateEnvContent($dbConfig);
if (file_put_contents($envPath, $envContent) === false) {
throw new \Exception("无法写入 .env 文件");
}
}
/**
* 生成 .env 文件内容
* @param array $dbConfig
* @return string
*/
private function generateEnvContent(array $dbConfig): string
{
return <<<EOT
APP_DEBUG = false
DB_TYPE = mysql
DB_HOST = {$dbConfig['host']}
DB_NAME = {$dbConfig['name']}
DB_USER = {$dbConfig['user']}
DB_PASS = {$dbConfig['pass']}
DB_PORT = {$dbConfig['port']}
DB_PREFIX = mpay_
DEFAULT_LANG = zh-cn
EOT;
}
/**
* 创建数据库表
* @throws \Exception
*/
private function createTables()
{
$db = $this->connectDatabase();
$tables = $this->getTableCreationSqls();
foreach ($tables as $tableName => $sql) {
try {
$db->execute("DROP TABLE IF EXISTS `$tableName`;");
$db->execute($sql);
Log::info("$tableName 表创建成功");
} catch (\Exception $e) {
throw new \Exception("创建 $tableName 表失败: " . $e->getMessage());
}
}
}
/**
* 获取表创建的 SQL 语句
* @return array
*/
private function getTableCreationSqls(): array
{
return [
'mpay_order' => "CREATE TABLE `mpay_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT 0,
`order_id` varchar(255) NOT NULL DEFAULT '',
`type` varchar(255) NOT NULL DEFAULT '',
`out_trade_no` varchar(255) NOT NULL DEFAULT '',
`notify_url` varchar(512) NOT NULL DEFAULT '',
`return_url` varchar(512) NOT NULL DEFAULT '',
`name` varchar(255) NOT NULL DEFAULT '',
`really_price` decimal(10, 2) NOT NULL DEFAULT 0.00,
`money` decimal(10, 2) NOT NULL DEFAULT 0.00,
`clientip` varchar(255) NOT NULL DEFAULT '',
`device` varchar(255) NOT NULL DEFAULT '',
`param` varchar(720) NOT NULL DEFAULT '',
`state` tinyint(4) NOT NULL DEFAULT 0,
`patt` tinyint(4) NOT NULL DEFAULT 0,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`close_time` datetime DEFAULT NULL,
`pay_time` datetime DEFAULT NULL,
`platform` varchar(15) NOT NULL DEFAULT '',
`platform_order` varchar(255) NOT NULL DEFAULT '',
`aid` int(11) NOT NULL DEFAULT 0,
`cid` int(11) NOT NULL DEFAULT 0,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
INDEX `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
'mpay_pay_account' => "CREATE TABLE `mpay_pay_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT 0,
`platform` varchar(255) NOT NULL DEFAULT '',
`account` varchar(255) NOT NULL DEFAULT '',
`password` varchar(255) NOT NULL DEFAULT '',
`state` tinyint(4) NOT NULL DEFAULT 1,
`pattern` tinyint(4) NOT NULL DEFAULT 1,
`params` text NOT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
'mpay_pay_channel' => "CREATE TABLE `mpay_pay_channel` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account_id` int(11) NOT NULL DEFAULT 0,
`channel` varchar(255) NOT NULL DEFAULT '',
`type` tinyint(4) NOT NULL DEFAULT 0,
`qrcode` varchar(512) NOT NULL DEFAULT '',
`last_time` datetime DEFAULT CURRENT_TIMESTAMP,
`state` tinyint(4) NOT NULL DEFAULT 1,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
'mpay_user' => "CREATE TABLE `mpay_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT 0,
`secret_key` varchar(255) NOT NULL DEFAULT '',
`nickname` varchar(255) NOT NULL DEFAULT '',
`username` varchar(255) NOT NULL DEFAULT '',
`password` varchar(255) NOT NULL DEFAULT '',
`state` tinyint(4) NOT NULL DEFAULT 1,
`role` tinyint(4) NOT NULL DEFAULT 0,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
];
}
/**
* 初始化数据
* @param array $dbConfig
* @throws \Exception
*/
private function initData(array $dbConfig)
{
$db = $this->connectDatabase();
$info = [
'secret_key' => md5(1000 . time() . mt_rand()),
'nickname' => $dbConfig['nickname'],
'username' => $dbConfig['username'],
'password' => password_hash($dbConfig['password'], PASSWORD_DEFAULT),
'create_time' => date('Y-m-d H:i:s'),
];
$sql = "INSERT INTO `mpay_user` (`id`, `pid`, `secret_key`, `nickname`, `username`, `password`, `state`, `role`, `create_time`) VALUES (1, 1000, :secret_key, :nickname, :username, :password, 1, 1, :create_time);";
try {
$db->execute($sql, $info);
Log::info("mpay_user 表数据初始化成功");
} catch (\Exception $e) {
throw new \Exception("初始化 mpay_user 表数据失败: " . $e->getMessage());
}
}
/**
* 检查是否已安装
* @return bool
*/
private function checkLock()
{
$path = runtime_path() . self::INSTALL_LOCK_FILE;
return file_exists($path);
}
/**
* 设置安装锁
* @throws \Exception
*/
private function setLock()
{
$path = runtime_path() . self::INSTALL_LOCK_FILE;
if (file_put_contents($path, time()) === false) {
throw new \Exception("无法写入安装锁文件");
}
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace app\controller;
use app\BaseController;
use app\model\Order;
use app\model\User;
use think\facade\View;
class OrderController extends BaseController
{
public function index()
{
$servertime = date('Y-m-d H:i:s', time());
View::assign('servertime', $servertime);
return View::fetch();
}
public function showOrder()
{
$id = $this->request->get('id');
$order = Order::showOrderDetail($id);
if ($order) {
View::assign($order);
return View::fetch();
} else {
return '订单不存在';
}
}
public function testPay()
{
$pid = 1000;
if (session('?pid')) {
$pid = session('pid');
}
View::assign('pid', $pid);
$key = User::where('pid', $pid)->where('state', 1)->value('secret_key');
if (!$key) {
return '用户禁用或不存在';
}
View::assign('key', $key);
return View::fetch();
}
}

View File

@ -0,0 +1,369 @@
<?php
namespace app\controller;
use think\Request;
use think\facade\View;
use app\model\User;
use app\model\Order;
use app\model\PayAccount;
use app\model\PayChannel;
class PayController
{
// 提交订单
public function submit(Request $request)
{
$req_method = $request->method();
$req_data = match ($req_method) {
'GET' => $request->get(),
'POST' => $request->post(),
default => []
};
if (!$req_data) return '参数错误';
// 验证签名
$key = User::where('pid', $req_data['pid'])->where('state', 1)->value('secret_key');
if (!$key) return '用户禁用或不存在';
$sign_str = self::getSign($req_data, $key);
if ($req_data['sign'] !== $sign_str) return '签名错误';
// 检查商户订单
$out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no');
if ($out_trade_no) return '订单提交重复';
// 创建新订单
$order_info = Order::createOrder($req_data);
if ($order_info['code'] !== 0) return $order_info['msg'];
return redirect("/Pay/console/{$order_info['data']['order_id']}");
}
// api提交订单
public function mapi(Request $request)
{
if (!$request->isPost()) return json(backMsg(0, '请求方式错误'));
$req_data = $request->post();
if (!$req_data) $req_data = $request->get();
if (!$req_data) return json(backMsg(0, '参数错误'));
// 验证签名
$key = User::where('pid', $req_data['pid'])->where('state', 1)->value('secret_key');
if (!$key) return json(backMsg(0, '用户禁用或不存在'));
$sign_str = self::getSign($req_data, $key);
if ($req_data['sign'] !== $sign_str) return json(backMsg(0, '签名错误'));
// 检查商户订单
$out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no');
if ($out_trade_no) return json(backMsg(0, '订单提交重复'));
// 创建新订单
$order_info = Order::createOrder($req_data);
if ($order_info['code'] !== 0) return json(backMsg(0, $order_info['msg']));
$payurl = $request->domain() . "/Pay/console/{$order_info['data']['order_id']}";
$info = ['code' => 1, 'msg' => '订单创建成功', 'trade_no' => $order_info['data']['order_id'], 'payurl' => $payurl];
return json($info);
}
// 收银台
public function console($order_id = '')
{
if ($order_id) {
$act_order = Order::where('order_id', $order_id)->find();
if ($act_order) {
$channel = PayChannel::where('id', $act_order->cid)->find();
View::assign($act_order->toArray());
$passtime = strtotime($act_order->close_time) - time();
View::assign('passtime', $passtime > 0 ? $passtime : 0);
// Alipay免输
if (preg_match('/^alipay4#\d+$/', $channel->channel)) {
$payurl = \payclient\AliPayf::getPayUrl($act_order->order_id, $act_order->money, $channel->qrcode);
View::assign('payUrl', $payurl['data'] ?? $payurl['msg']);
} else {
View::assign('payUrl', $channel->qrcode);
}
View::assign('code_type', $channel->type);
return View::fetch();
} else {
return '订单不存在';
}
} else {
return '订单号参数错误';
}
}
// 查询订单状态
public function getOrderState($order_id = '')
{
if ($order_id) {
$act_order = Order::where('order_id', $order_id)->find();
if ($act_order) {
$passtime = strtotime($act_order->close_time) - time();
$data = [];
if ($act_order->state === 0) {
$data['order_id'] = $act_order->order_id;
$data['passtime'] = $passtime > 0 ? $passtime : 0;
$data['state'] = $act_order->state;
return json($data);
} elseif ($act_order->state === 1) {
// 通知参数
$notify = self::crateNotify($act_order);
// 字符串签名
$user_key = User::where('pid', $act_order->pid)->value('secret_key');
$sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign;
// 跳转通知URL
$res_return_url = $act_order->return_url . '?' . http_build_query($notify);
if (strpos($act_order->return_url, '?')) $res_return_url = $act_order->return_url . '&' . http_build_query($notify);
// 响应消息
$data['order_id'] = $act_order->order_id;
$data['passtime'] = $passtime > 0 ? $passtime : 0;
$data['state'] = $act_order->state;
$data['return_url'] = $res_return_url;
return json($data);
}
} else {
return '订单不存在';
}
} else {
return '订单号参数错误';
}
}
// 验证支付结果
public function validatePayResult(Request $request)
{
$data = $request->post();
$order = Order::find($data['id']);
if (\strtotime($order->close_time) < \time()) {
return \json(\backMsg(1, '订单已关闭'));
}
$up_data = ['id' => $data['id'], 'patt' => $data['patt']];
$up_res = Order::update($up_data);
if ($up_res) {
return \json(\backMsg(0, '更新成功'));
} else {
return \json(\backMsg(1, '更新失败'));
}
}
// 处理收款通知
private function payHeart(array $records, array $config)
{
$pid = $config['pid'];
$aid = $config['aid'];
// 检测收款通知
if (!$records) {
return json(['code' => 0, 'msg' => '空收款通知']);
}
// 当前用户账号
$query = ['pid' => $pid, 'aid' => $aid];
// 排除已支付订单
$doneOrders = Order::scope('dealOrder')->where($query)->column('platform_order');
$new_orders = [];
foreach ($records as $order) {
if (!in_array($order['order_no'], $doneOrders)) $new_orders[] = $order;
}
if (!count($new_orders)) return json(['code' => 0, 'msg' => '收款通知无新消息']);
// 有效订单列表
$activeOrders = Order::scope('activeOrder')->where($query)->select();
if (!count($activeOrders)) return json(['code' => 0, 'msg' => '数据库无有效期订单']);
// 查找所有支付渠道
$channels = $activeOrders->column('cid');
$cids = PayChannel::whereIn('id', $channels)->column('channel', 'id');
// 订单处理
$notify = [];
foreach ($new_orders as $new_order) {
foreach ($activeOrders as $order) {
// 支付方式核对
$is_payway = $order->type == $new_order['payway'];
if ($new_order['payway'] == '') $is_payway = true;
// 支付渠道核对
$is_channel = $cids[$order->cid] == $new_order['channel'];
// 金额核对
$is_money = $order->really_price == $new_order['price'];
// 订单核对
if ($is_payway && $is_channel && $is_money) {
// 是否免输
if (isset($new_order['remark'])) {
if ($new_order['remark'] == $order->order_id) {
$res = $this->updateOrderState($order, $new_order['order_no']);
$notify[] = $res;
}
} else {
$res = $this->updateOrderState($order, $new_order['order_no']);
$notify[] = $res;
}
}
}
}
if (!$notify) $notify = ['code' => 0, 'msg' => '收款通知无匹配订单'];
return json($notify);
}
// 修改订单状态并通知
private function updateOrderState(Order $order, string $order_no = ''): array
{
// 支付成功
$set_order_state = $order->save(['state' => 1, 'pay_time' => date('Y-m-d H:i:s', time()), 'platform_order' => $order_no]);
if (!$set_order_state) {
return ['order' => $order->order_id, 'code' => 0, 'msg' => '修改订单状态失败'];
}
// 订单成交通知
$notify = self::crateNotify($order);
// 字符串签名
$user_key = User::where('pid', $order->pid)->value('secret_key');
$sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign;
// 异步通知
$notify_url = $order->notify_url . '?' . http_build_query($notify);
if (strpos($order->notify_url, '?')) $notify_url = $order->notify_url . '&' . http_build_query($notify);
$res_notify = self::getHttpResponse($notify_url);
if ($res_notify === 'success') {
return ['order' => $order->order_id, 'code' => 1, 'msg' => 'notify success'];
} else {
return ['order' => $order->order_id, 'code' => 0, 'msg' => 'notify fail'];
}
}
// [定时任务]获取收款明细,提交收款通知
public function checkPayResult(Request $request)
{
$req_info = $request->get();
$req_pid = $req_info['pid'];
$req_aid = $req_info['aid'];
// 获取订单
$new_order = cache('order');
if (!$new_order) return json(['code' => 3, 'msg' => '没有找到新订单缓存']);
// 检测新订单
if ($new_order['code'] !== 1) return json($new_order);
// 订单列表
$order_list = $new_order['orders'];
// 检测本账号订单
$orders = [];
foreach ($order_list as $key => $val) {
if ($req_pid == $val['pid'] && $req_aid == $val['aid'] && $val['patt'] == 1) {
$orders[] = $order_list[$key];
}
}
if (!$orders) return json(['code' => 0, 'msg' => '非本账号订单或监听模式不对']);
// 加载配置文件
$config = PayAccount::getAccountConfig($req_aid);
if ($config === false) return json(['code' => 4, 'msg' => '监听收款配置错误']);
// 登陆账号
$pay_config = ['username' => $config['account'], 'password' => $config['password']];
// 配置参数
$params = $config['params'];
// 实例监听客户端
$payclient_name = $config['payclass'];
// 插件类文件是否存在
$payclient_path = root_path() . '/extend/payclient/' . $payclient_name . '.php';
if (!file_exists($payclient_path)) return json(['code' => 5, 'msg' => '监听客户端文件不存在']);
$payclient_path = "\\payclient\\{$payclient_name}";
$Payclient = new $payclient_path($pay_config);
// 获取支付明细
$records = $Payclient->getOrderInfo($params);
if ($records['code'] === 0) {
// 提交收款记录
$upres = $this->payHeart($records['data'], $config);
return $upres;
} else {
return json(['code' => 0, 'msg' => $records['msg']], 320);
}
}
// [定时任务]监听新订单,生成缓存
public function checkOrder($pid = '', $sign = '')
{
if (!($pid && $sign)) return '参数错误';
$is_user = User::checkUser($pid, $sign);
if ($is_user) {
$orders = Order::scope('activeOrder')->field('id,pid,aid,cid,patt')->select();
$old_info = cache('order');
$num = count($orders);
if ($num > 0) {
$info = ['code' => 1, 'msg' => "{$num}个新订单"];
$order_list = ['code' => 1, 'msg' => "{$num}个新订单", 'orders' => $orders];
if ($old_info !== $order_list) {
cache('order', $order_list);
}
return json($info);
} else {
$info = ['code' => 0, 'msg' => '没有新订单'];
if ($old_info !== $info) {
cache('order', $info);
}
return json($info);
}
} else {
$info = ['code' => 2, 'msg' => '签名错误'];
return json($info);
}
}
// 处理微信/支付宝收款通知
public function mpayNotify(Request $request)
{
$info = $request->post();
$action = isset($info['action']) ? $info['action'] : '';
if ($action !== 'mpay' && $action !== 'mpaypc') return '非mpay的访问请求';
$data = json_decode($info['data'], true);
if (!is_array($data)) return '通知数据为空';
if (!isset($data['aid']) || !isset($data['pid'])) return 'aid和pid参数错误';
$config = PayAccount::getAccountConfig($data['aid'], $data['pid']);
$payclient_path = "\\payclient\\{$config['payclass']}";
$Payclient = new $payclient_path($info, $config);
if ($action == 'mpay') $res = $Payclient->notify();
if ($action == 'mpaypc') $res = $Payclient->pcNotify();
if ($res['code'] !== 0) return $res['msg'];
$this->payHeart($res['data'], $config);
return 200;
}
// 签名
private static function getSign(array $param = [], string $key = ''): string
{
ksort($param);
reset($param);
$signstr = '';
foreach ($param as $k => $v) {
if ($k != "sign" && $k != "sign_type" && $v != '') {
$signstr .= $k . '=' . $v . '&';
}
}
$signstr = substr($signstr, 0, -1);
$signstr .= $key;
$sign = md5($signstr);
return $sign;
}
// 构建通知
private static function crateNotify($param): array
{
$notify = [
'pid' => $param->pid,
'trade_no' => $param->order_id,
'out_trade_no' => $param->out_trade_no,
'type' => $param->type,
'name' => $param->name,
'money' => $param->money,
'trade_status' => 'TRADE_SUCCESS',
'sign_type' => 'MD5',
];
// 添加扩展参数
// $notify = array_merge($notify, unserialize($param->param));
$notify['param'] = unserialize($param->param);
// 删除空值
foreach ($notify as $key => $val) {
if ($val === '') unset($notify[$key]);
}
return $notify;
}
// 请求外部资源
private static function getHttpResponse($url, $header = [], $post = null, $timeout = 10)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
if ($header) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
} else {
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
}
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if ($post) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace app\controller;
use app\BaseController;
use app\model\PayAccount;
use app\model\PayChannel;
use think\facade\View;
class PayManageController extends BaseController
{
public function index()
{
$domain = $this->request->domain();
View::assign('domain', $domain);
return View::fetch();
}
// 编辑账号
public function editAccount()
{
$id = $this->request->get('id');
$account = PayAccount::find($id);
View::assign([
'id' => $id,
'platform' => $account->getData('platform'),
'account' => $account->account,
'password' => $account->password,
'state' => $account->state,
'pattern' => $account->getData('pattern'),
'params' => $account->params,
]);
return View::fetch();
}
// 添加账号
public function addAccount()
{
return View::fetch();
}
// 添加收款终端
public function addChannel()
{
$aid = $this->request->get('aid');
$account = PayAccount::find($aid);
$platform = $account->getData('platform');
View::assign(['aid' => $aid, 'platform' => $platform, 'account' => $account->account]);
if ($platform == 'wxpay' || $platform == 'alipay') {
return View::fetch('add_channel_code');
}
return View::fetch();
}
// 编辑收款终端
public function editChannel()
{
$cid = $this->request->get('cid');
$channel = PayChannel::with('payAccount')->where('id', $cid)->find();
View::assign([
'cid' => $channel->id,
'platform' => $channel->payAccount->platform,
'account' => $channel->payAccount->account,
'channel' => $channel->channel,
'qrcode' => $channel->qrcode,
'last_time' => $channel->last_time,
'state' => $channel->state,
'type' => $channel->type,
]);
return View::fetch();
}
// 收款终端列表
public function channelList()
{
$id = $this->request->get('id');
View::assign(['id' => $id]);
return View::fetch();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace app\controller;
use app\BaseController;
use think\facade\View;
use think\Request;
class PluginController extends BaseController
{
// 插件管理页
public function index()
{
return View::fetch();
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace app\controller;
use app\BaseController;
use think\facade\View;
use app\model\User;
class UserController extends BaseController
{
protected $middleware = ['Auth' => ['except' => ['login']]];
// 用户中心
public function index()
{
$userinfo = User::find(\session('userid'))->toArray();
View::assign($userinfo);
View::assign('url', $this->request->domain().'/');
$sign = md5($userinfo['pid'] . $userinfo['secret_key']);
View::assign('orderurl', $this->request->domain() . "/checkOrder/{$userinfo['pid']}/{$sign}");
return View::fetch();
}
// 登陆视图
public function login()
{
if (session('?islogin')) {
return redirect('/Console/index');
}
return View::fetch();
}
// 修改用户
public function setUser()
{
$userinfo = User::find(session('userid'))->toArray();
View::assign($userinfo);
return View::fetch();
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace app\controller\api;
use think\Request;
use app\model\Order;
class ConsoleController
{
public function orderinfo(Request $request)
{
$date = (int)$request->get('time') ?: 0;
$time = match ($date) {
0 => [date('Y') . '-01-01 00:00:00', date('Y-m-d 23:59:59')],
1 => [date('Y-m-d H:i:s', strtotime('-30 days')), date('Y-m-d 23:59:59')],
2 => [date('Y-m-d H:i:s', strtotime('-6 months')), date('Y-m-d 23:59:59')],
3 => [date('Y-m-d H:i:s', strtotime('-1 year')), date('Y-m-d 23:59:59')],
default => []
};
if (!$time) {
return json(['code' => 400, 'msg' => '参数错误']);
}
$orders = Order::whereBetweenTime('create_time', $time[0], $time[1])->where('state', 1)->field('id,type,really_price')->select();
$data = [
'ordernum' => count($orders),
'totalmoney' => \number_format(array_sum(array_column($orders->toArray(), 'really_price')), 2),
'wxpay' => [
'num' => count($orders->where('type', 'wxpay')),
'money' => \number_format(array_sum(array_column($orders->where('type', 'wxpay')->toArray(), 'really_price')), 2)
],
'alipay' => [
'num' => count($orders->where('type', 'alipay')),
'money' => \number_format(array_sum(array_column($orders->where('type', 'alipay')->toArray(), 'really_price')), 2)
],
'unionpay' => [
'num' => count($orders->where('type', 'unionpay')),
'money' => \number_format(array_sum(array_column($orders->where('type', 'unionpay')->toArray(), 'really_price')), 2)
]
];
return json($data);
}
}

View File

@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace app\controller\api;
use app\BaseController;
use app\model\Order;
use app\model\User;
class OrderController extends BaseController
{
// 查询订单
public function getOrders()
{
$query = $this->request->get();
$orders = Order::serchOrders($query)->order('id', 'desc')->paginate(['list_rows' => $query['limit'], 'page' => $query['page']]);
if ($orders) {
return json(['code' => 0, 'msg' => 'OK', 'count' => $orders->total(), 'data' => $orders->items()]);
} else {
return json(['code' => 1, 'msg' => '无数据记录', 'count' => 0, 'data' => []]);
}
}
// 修改订单支付状态
public function changeOrderState()
{
$info = $this->request->post();
$uporder_res = Order::update(['state' => $info['state'], 'id' => $info['id']]);
if ($uporder_res) {
return json(\backMsg(0, '修改成功'));
} else {
return json(\backMsg(1, '修改失败'));
}
}
// 手动补单
public function doPayOrder()
{
$info = $this->request->post();
// 修改支付状态
$order = Order::find($info['id']);
$order->state = $info['state'];
$res = $order->save();
if ($res) {
// 创建通知
$notify = self::crateNotify($order);
// 字符串签名
$user_key = User::where('pid', $order->pid)->value('secret_key');
$sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign;
// 异步通知
$notify_url = $order->notify_url . '?' . http_build_query($notify);
if (strpos($order->notify_url, '?')) $notify_url = $order->notify_url . '&' . http_build_query($notify);
$res_notify = self::getHttpResponse($notify_url);
if ($res_notify === 'success') {
return json(\backMsg(0, '订单通知成功'));
} else {
return json(\backMsg(1, '异步通知失败'));
}
} else {
return json(\backMsg(1, '支付状态修改失败'));
}
}
// 重新通知
public function redoPayOrder()
{
$id = $this->request->post('id');
// 修改支付状态
$order = Order::find($id);
if ($order) {
// 创建通知
$notify = self::crateNotify($order);
// 字符串签名
$user_key = User::where('pid', $order->pid)->value('secret_key');
$sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign;
// 异步通知
$notify_url = $order->notify_url . '?' . http_build_query($notify);
if (strpos($order->notify_url, '?')) $notify_url = $order->notify_url . '&' . http_build_query($notify);
$res_notify = self::getHttpResponse($notify_url);
if ($res_notify === 'success') {
return json(\backMsg(0, '订单通知成功'));
} else {
return json(\backMsg(1, '异步通知失败'));
}
} else {
return json(\backMsg(1, '订单不存在'));
}
}
// 删除订单
public function deleteOrder()
{
$id = $this->request->post('id');
$del_res = Order::destroy($id);
if ($del_res) {
return json(\backMsg(0, '删除成功'));
} else {
return json(\backMsg(1, '删除失败'));
}
}
// 批量删除订单
public function batchRemove()
{
$ids = $this->request->post('ids');
if (!$ids) {
return json(\backMsg(1, '参数错误'));
}
$del_res = Order::destroy($ids);
if ($del_res) {
return json(\backMsg(0, '删除成功'));
} else {
return json(\backMsg(1, '删除失败'));
}
}
// 清空超时订单
public function batchTimeout()
{
$ids = Order::scope('timeoutOrder')->column('id');
if (!$ids) {
return json(\backMsg(1, '无过期订单'));
}
$batch_del_res = Order::destroy($ids);
if ($batch_del_res) {
return json(\backMsg(0, '清理成功'));
} else {
return json(\backMsg(1, '清理失败'));
}
}
// 签名方法
private static function getSign(array $param = [], string $key = ''): string
{
if (!$param)
return '参数错误';
if (!$key)
return '密钥错误';
ksort($param);
reset($param);
$signstr = '';
foreach ($param as $k => $v) {
if ($k != "sign" && $k != "sign_type" && $v != '') {
$signstr .= $k . '=' . $v . '&';
}
}
$signstr = substr($signstr, 0, -1);
$signstr .= $key;
$sign = md5($signstr);
return $sign;
}
// 构建通知参数
private static function crateNotify($param): array
{
$notify = [
'pid' => $param->pid,
'trade_no' => $param->order_id,
'out_trade_no' => $param->out_trade_no,
'type' => $param->type,
'name' => $param->name,
'money' => $param->money,
'trade_status' => 'TRADE_SUCCESS',
'sign_type' => 'MD5',
];
// 添加扩展参数
// $notify = array_merge($notify, unserialize($param->param));
$notify['param'] = unserialize($param->param);
// 删除空值
foreach ($notify as $key => $val) {
if ($val === '') unset($notify[$key]);
}
return $notify;
}
// 请求外部资源
private static function getHttpResponse($url, $header = [], $post = null, $timeout = 10)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
if ($header) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
} else {
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
}
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if ($post) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}

View File

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace app\controller\api;
use app\BaseController;
use app\model\PayAccount;
use app\model\PayChannel;
class PayManageController extends BaseController
{
// 获取账号列表
public function getPayAccount()
{
$query = $this->request->get();
$accounts = PayAccount::serchAccount($query)->order('id', 'desc')->paginate(['list_rows' => $query['limit'], 'page' => $query['page']]);
if ($accounts) {
return json(['code' => 0, 'msg' => 'OK', 'count' => $accounts->total(), 'data' => $accounts->items()]);
} else {
return json(['code' => 1, 'msg' => '无数据记录', 'count' => 0, 'data' => []]);
}
}
// 收款终端列表
public function getChannelList()
{
$aid = $this->request->post('aid');
$res = PayChannel::where(['account_id' => $aid])->order('last_time', 'desc')->select();
if ($res) {
return json(backMsg(0, '获取成功', $res));
} else {
return json(backMsg(1, '失败'));
}
}
// 账号状态
public function accountEnable()
{
$info = $this->request->post();
$up_res = PayAccount::update($info);
if ($up_res) {
return json(backMsg(0, '成功'));
} else {
return json(backMsg(1, '失败'));
}
}
// 添加账号
public function addAccount()
{
$info = $this->request->post();
$pid = $this->request->session('pid');
$info['pid'] = $pid;
$info['params'] = '{}';
$check_acc = PayAccount::where(['account' => $info['account'], 'platform' => $info['platform'], 'pid' => $pid])->find();
if ($check_acc) {
return json(backMsg(1, '账号已存在'));
}
$acc = PayAccount::create($info);
if ($acc) {
return json(backMsg(0, '添加成功'));
} else {
return json(backMsg(1, '添加失败'));
}
}
// 编辑账号
public function editAccount()
{
$info = $this->request->post();
$up_res = PayAccount::update($info);
if ($up_res) {
return json(backMsg(0, '修改成功'));
} else {
return json(backMsg(1, '修改失败'));
}
}
// 删除账号
public function delAccount()
{
$ids = $this->request->post('ids');
$res = PayAccount::destroy($ids);
$res2 = PayChannel::whereIn('account_id', $ids)->select()->delete();
if ($res && $res2) {
return json(backMsg(0, '已删除'));
} else {
return json(backMsg(1, '失败'));
}
}
// 添加收款终端
public function addChannel()
{
$info = $this->request->post();
$check = PayChannel::where(['account_id' => $info['account_id'], 'channel' => $info['channel']])->count();
if ($check) {
return json(backMsg(1, '编号已存在'));
}
$info['last_time'] = date('Y-m-d H:i:s');
$res = PayChannel::create($info);
if ($res) {
return json(backMsg(0, '添加成功'));
} else {
return json(backMsg(1, '添加失败'));
}
}
// 编辑收款终端
public function editChannel()
{
$info = $this->request->post();
$up_res = PayChannel::update($info);
if ($up_res) {
return json(backMsg(0, '修改成功'));
} else {
return json(backMsg(1, '修改失败'));
}
}
// 删除收款终端
public function delChannel()
{
$cid = $this->request->post('id');
$res = PayChannel::destroy($cid);
if ($res) {
return json(backMsg(0, '已删除'));
} else {
return json(backMsg(1, '失败'));
}
}
// 上传二维码图片
public function uploadQrcode()
{
$img = $this->request->file('codeimg');
if (!$img) {
return json(backMsg(1, '请选择要上传的文件'));
}
// 验证文件类型
$allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
$fileMimeType = $img->getMime();
if (!in_array($fileMimeType, $allowedTypes)) {
return json(backMsg(1, '只允许上传PNG、JPEG或GIF格式的图片'));
}
// 生成唯一文件名
$filename = 'img_' . time() . '_' . uniqid() . '.' . $img->getOriginalExtension();
// 设置文件保存路径
$path = public_path() . '/files/qrcode/';
if (!is_dir($path)) {
mkdir($path, 0755, true);
}
// 移动文件到指定目录
$info = $img->move($path, $filename);
if ($info) {
$imgpath = '/files/qrcode/' . $filename;
return json(backMsg(0, '上传成功', ['imgpath' => $imgpath]));
} else {
return json(backMsg(1, '上传失败'));
}
}
// 获取账号交易流水
public function getAccountTrade()
{
$req_info = $this->request->get();
$req_pid = $req_info['pid'];
$req_aid = $req_info['aid'];
// 加载配置文件
$config = PayAccount::getAccountConfig($req_aid);
if ($config === false) return json(backMsg(1, '账号配置文件错误'));
if ($req_aid != $config['aid'] || $req_pid != session('pid')) return json(backMsg(1, '监听收款配置不一致'));
// 登陆账号
$pay_config = ['username' => $config['account'], 'password' => $config['password']];
// 收款查询
$params = $config['params'];
// 实例监听客户端
$payclient_name = $config['payclass'];
$payclient_path = "\\payclient\\{$payclient_name}";
$Payclient = new $payclient_path($pay_config);
// 获取支付明细
$records = $Payclient->getOrderInfo($params);
if ($records['code'] === 0) {
// 收款流水
return json(backMsg(0, '查询成功', $records['data']));
} else {
return json(['code' => 1, 'msg' => $records['msg']]);
}
}
}

View File

@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace app\controller\api;
use app\BaseController;
class PluginController extends BaseController
{
// 获取插件列表
public function getPluginList()
{
$local_plugin_config = self::getPluginConfig();
$show = $this->request->get('show', 1);
$plugin_config = match ((int)$show) {
0 => \Plugin::getAllPlugins($local_plugin_config),
1 => \Plugin::getInstall($local_plugin_config),
2 => \Plugin::getUninstallPlugins($local_plugin_config),
default => []
};
if ($plugin_config) {
return json(['code' => 0, 'msg' => 'OK', 'count' => count($plugin_config), 'data' => $plugin_config]);
} else {
return json(['code' => 1, 'msg' => '无数据记录', 'count' => 0, 'data' => []]);
}
}
// 安装插件
public function installPlugin()
{
$platform = $this->request->post('platform');
if (!$platform) return json(backMsg(1, '请选择插件'));
$intall_info = \Plugin::installPlugin($platform);
if ($intall_info['code'] !== 0) return json(backMsg(1, $intall_info['msg']));
// 需要授权
if ($intall_info['data']['status'] === 0) {
return json(['code' => 0, 'msg' => '请支付', 'state' => 0, 'data' => $intall_info['data']]);
}
$saved = $this->saveNewPluginConfig($intall_info['data']);
if ($saved['code'] !== 0) return json(backMsg(1, $saved['msg']));
return json(['code' => 0, 'msg' => '授权成功', 'state' => 1]);
}
// 更新插件
public function updatePlugin()
{
$platform = $this->request->post('platform');
if (!$platform) return json(backMsg(1, '请选择插件'));
$update_info = \Plugin::updatePlugin($platform);
if ($update_info['code'] !== 0) return json(backMsg(1, $update_info['msg']));
$saved = $this->saveNewPluginConfig($update_info['data']);
if ($saved['code'] !== 0) return json(backMsg(1, $saved['msg']));
return json(['code' => 0, 'msg' => '更新成功']);
}
// 保存全部插件信息
private function saveNewPluginConfig(array $config = [])
{
$plugin_config = $config['config'];
$plugin_auth = $config['authcode'];
$plugin_file = $config['file'];
if (!$this->savePluginFile($plugin_file, $plugin_config)) return backMsg(1, '保存插件文件失败');
if (!$this->saveAuthCode($plugin_auth, $plugin_config)) return backMsg(1, '保存插件授权码失败');
if (!$this->addPlugin($plugin_config)) return backMsg(1, '保存插件配置失败');
return backMsg(0, 'ok');
}
// 卸载插件
public function uninstallPlugin()
{
$platform = $this->request->post('platform');
if (!$platform) return json(backMsg(1, '请选择插件'));
$this->delPluginFile($platform);
$this->delPlugin($platform);
return json(backMsg(0, '卸载成功'));
}
// 添加或更新插件
public function addPlugin(array $option = [])
{
$keys = ['platform', 'name', 'class_name', 'price', 'describe', 'website', 'helplink', 'version'];
$config = [];
foreach ($option as $key => $value) {
if (in_array($key, $keys)) $config[$key] = $value;
}
$config['state'] = 1;
$plugin_config = self::getPluginConfig();
$plugin_platform = $config['platform'] ?: '';
foreach ($plugin_config as $i => $value) {
if ($plugin_platform == $value['platform']) {
$plugin_config[$i] = $config;
$this->savePluginConfig($plugin_config, '支付插件列表');
return true;
}
}
$plugin_config[] = $config;
$this->savePluginConfig($plugin_config, '支付插件列表');
return true;
}
// 删除插件配置
private function delPlugin(string $plugin_name = '')
{
$plugin_config = self::getPluginConfig();
$index = null;
foreach ($plugin_config as $i => $value) {
if ($value['platform'] == $plugin_name) {
$index = $i;
break;
}
}
if ($index === null) return false;
unset($plugin_config[$index]);
$config = array_values($plugin_config);
$this->savePluginConfig($config, '支付插件列表');
return true;
}
// 删除插件类库文件
private function delPluginFile(string $platform = '')
{
$file_name = self::getPluginInfo($platform)['class_name'];
if (!$file_name) return false;
$plugin_path = root_path() . '/extend/payclient/' . $file_name . '.php';
if (!file_exists($plugin_path)) return false;
unlink($plugin_path);
return true;
}
// 修改插件
public function setPlugin($platform = '', $option = [])
{
$config = self::getPluginConfig();
if (!$platform) return 1;
if (!$option) return 2;
foreach ($config as $index => $options) {
if ($options['platform'] == $platform) {
foreach ($options as $key => $value) {
if (\array_key_exists($key, $option)) {
$config[$index][$key] = $option[$key];
}
}
$this->savePluginConfig($config, '支付插件列表');
return 0;
}
}
}
// 插件启用
public function pluginEnable()
{
$info = $this->request->post();
if (!$this->isPluginInstall($info['platform'])) return json(backMsg(1, '插件未安装'));
$up_res = $this->setPlugin($info['platform'], ['state' => $info['state']]);
if ($up_res) {
return json(backMsg(1, '失败'));
} else {
return json(backMsg(0, '成功'));
}
}
// 检测插件是否安装
public function isPluginInstall(string $platform): bool
{
$config = self::getPluginConfig();
$platforms = [];
foreach ($config as $key => $value) {
$platforms[] = $value['platform'];
}
if (in_array($platform, $platforms)) {
return true;
} else {
return false;
}
}
// 插件选项
public function pluginOption()
{
// 加载平台配置
$config = self::getPluginConfig();
$option = [];
foreach ($config as $value) {
if ($value['state'] == 0) {
continue;
}
$option[] = ['platform' => $value['platform'], 'name' => $value['name']];
}
return json($option);
}
// 获取指定插件配置
public static function getPluginInfo($platform = '')
{
$config = self::getPluginConfig();
$info = [];
foreach ($config as $item) {
if ($item['platform'] == $platform) {
$info = $item;
break;
}
}
return $info;
}
// 保存授权码
private function saveAuthCode(string $authcode = '', array $config = [])
{
$dir_path = runtime_path() . "auth/";
if (!is_dir($dir_path)) mkdir($dir_path, 755, true);
$auth_path = $dir_path . md5("{$config['platform']}payclient\\{$config['class_name']}") . '.json';
return file_put_contents($auth_path, json_encode(['authcode' => $authcode])) !== false ? true : false;
}
// 保存插件类库文件
private function savePluginFile($file_url = '', array $config = [])
{
if (empty($file_url)) return false;
$file_content = @file_get_contents($file_url);
if ($file_content === false) return false;
$save_dir = root_path() . 'extend/payclient/';
if (!is_dir($save_dir)) mkdir($save_dir, 0755, true);
$save_path = $save_dir . $config['class_name'] . '.php';
return file_put_contents($save_path, $file_content) !== false ? true : false;
}
// 获取插件配置
private static function getPluginConfig(): array
{
$payplugin_path = config_path() . '/extend/payplugin.php';
if (!file_exists($payplugin_path)) return [];
// 加载插件配置
$payplugin_config = require $payplugin_path;
return $payplugin_config;
}
// 保存插件配置
private function savePluginConfig(array $config, string $note = '说明')
{
$payplugin_path = config_path() . '/extend/payplugin.php';
$note_tpl = <<<EOF
// +----------------------------------------------------------------------
// | $note
// +----------------------------------------------------------------------
EOF;
$config_str = "<?php\n" . $note_tpl . "\n\nreturn " . var_export($config, true) . ";\n";
\file_put_contents($payplugin_path, $config_str);
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace app\controller\api;
use app\BaseController;
use think\facade\Session;
use app\model\User;
class UserController extends BaseController
{
protected $middleware = ['Auth' => ['except' => ['login']]];
public function login()
{
$login_info = $this->request->post();
$userinfo = self::checkUser($login_info);
if ($userinfo['code'] === 0) {
Session::set('userid', $userinfo['data']->id);
Session::set('pid', $userinfo['data']->pid);
Session::set('nickname', $userinfo['data']->nickname);
Session::set('userrole', $userinfo['data']->role);
Session::set('islogin', true);
return json(backMsg(0, 'ok'));
} else {
return json($userinfo);
}
}
public function logout()
{
Session::clear();
return json(backMsg(0, '注销成功'));
}
public function editUser()
{
$userid = session('userid');
$info = $this->request->post();
$res = User::update($info, ['id' => $userid]);
if (!$res) {
return json(backMsg(1, '修改失败'));
}
return json(backMsg(0, '重置成功'));
}
public function changePassword()
{
$userid = session('userid');
$user_info = User::find($userid);
$post_info = $this->request->post();
if (password_verify($post_info['old_password'], $user_info->password)) {
$new_password = password_hash($post_info['new_password'], PASSWORD_DEFAULT);
$res = User::update(['password' => $new_password], ['id' => $userid]);
if (!$res) {
return json(backMsg(1, '修改失败'));
}
return json(backMsg(0, '修改成功'));
} else {
return json(backMsg(1, '原密码错误'));
}
}
public function resetKey()
{
$userid = session('userid');
$res = User::update(['secret_key' => $this->generateKey()], ['id' => $userid]);
if (!$res) {
return json(backMsg(1, '重置失败'));
}
return json(backMsg(0, '重置成功'));
}
private function checkUser(array $login_info): array
{
$username = $login_info['username'];
$password = $login_info['password'];
$userinfo = User::where('username', $username)->find();
if ($userinfo) {
if (password_verify($password, $userinfo->password)) {
return ['code' => 0, 'data' => $userinfo];
} else {
return backMsg(1, '登陆密码错误');
}
} else {
return backMsg(2, '用户不存在');
}
}
private function generateKey(bool $strong = true)
{
$bytes = openssl_random_pseudo_bytes(16, $strong);
if ($strong) {
$key = bin2hex($bytes);
return md5($key);
} else {
return false;
}
}
}

17
app/event.php Normal file
View File

@ -0,0 +1,17 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
'AppInit' => [],
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
],
'subscribe' => [
],
];

10
app/middleware.php Normal file
View File

@ -0,0 +1,10 @@
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
\think\middleware\SessionInit::class
];

30
app/middleware/Auth.php Normal file
View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace app\middleware;
class Auth
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
*/
public function handle($request, \Closure $next)
{
// 登陆状态
$islogin = session('?islogin');
if ($islogin) {
return $next($request);
} else {
$method = $request->isJson();
if ($method) {
return \json(\backMsg(404, '身份过期,请重新登陆'));
}
return redirect('/User/login');
}
}
}

215
app/model/Order.php Normal file
View File

@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace app\model;
use app\BaseModel;
use app\model\PayAccount;
use app\model\PayChannel;
class Order extends BaseModel
{
// 订单有效期
private static $activity_time = 180;
// 新建订单
public static function createOrder($data): array
{
$my_time = time();
$channel = self::setChannel($data['pid'], $data['type']);
if ($channel['code'] !== 0) return $channel;
$channel = $channel['data'];
$new_order = [
// 订单号
'order_id' => self::createOrderID('H'),
// 商户ID
'pid' => $data['pid'],
// 支付类型
'type' => $data['type'],
// 商户订单号
'out_trade_no' => $data['out_trade_no'],
// 异步通知
'notify_url' => $data['notify_url'],
// 跳转通知
'return_url' => isset($data['return_url']) ? $data['return_url'] : '',
// 商品名称
'name' => $data['name'],
// 商品金额
'money' => $data['money'],
// 实际成交金额
'really_price' => self::checkMoney($data['money'], $data['type'], $channel['aid'], $channel['cid'], $channel['chan']),
// 用户IP
'clientip' => isset($data['clientip']) ? $data['clientip'] : '',
// 设备类型
'device' => isset($data['device']) ? $data['device'] : '',
// 业务扩展参数
'param' => serialize(isset($data['param']) ? $data['param'] : ''),
// 等待/过期0, 支付成功1
'state' => 0,
// 开启监听1, 关闭监听0
'patt' => $channel['patt'],
// 平台
'platform' => $channel['platform'],
// 订单创建时间
'create_time' => self::getFormatTime($my_time),
// 订单关闭时间
'close_time' => self::getFormatTime($my_time + self::$activity_time),
// 支付时间
'pay_time' => self::getFormatTime($my_time),
// 收款账号id
'aid' => $channel['aid'],
// 交易终端id
'cid' => $channel['cid'],
];
$res = self::create($new_order);
if ($res->order_id) {
return backMsg(0, 'ok', ['order_id' => $res->order_id]);
} else {
return backMsg(4, '创建订单记录失败');
}
}
// 查询订单列表
public static function serchOrders($query)
{
$select = [];
$allow_field = ['id', 'order_id', 'pid', 'type', 'out_trade_no', 'name', 'really_price', 'money', 'state', 'create_time_start', 'create_time_end', 'close_time', 'pay_time', 'platform', 'platform_order', 'aid', 'cid',];
foreach ($query as $key => $value) {
if (in_array($key, $allow_field) && isset($value)) {
if ($key === 'name') {
$select[] = [$key, 'like', '%' . $value . '%'];
continue;
}
if ($key === 'create_time_start') {
$select[] = ['create_time', '>', $value];
continue;
}
if ($key === 'create_time_end') {
$select[] = ['create_time', '<', $value];
continue;
}
$select[] = [$key, '=', $value];
}
}
return self::where($select);
}
// 查询订单详细
public static function showOrderDetail($id)
{
$order = self::find($id);
$a_list = PayAccount::withTrashed()->find($order->aid);
$c_list = PayChannel::withTrashed()->find($order->cid);
if (!$order) {
return [];
}
$order->platform = $a_list['platform'] ?? '···';
$order->account = $a_list['account'] ?? '···';
$order->channel = $c_list['channel'] ?? '···';
$order->qrcode = $c_list['qrcode'] ?? '···';
$order->url_type = $c_list['type'] ?? '···';
return $order->toArray();
}
// 选择收款通道
private static function setChannel($pid, $type): array
{
// 查询有效收款账户及通道
$aids = PayAccount::where('pid', $pid)->where('state', 1)->column('id');
if (!$aids) return backMsg(1, '用户无可用收款账户');
$channel_infos = PayChannel::whereIn('account_id', $aids)->where('state', 1)->order('last_time', 'asc')->select();
if ($channel_infos->isEmpty()) return backMsg(2, '用户账户无可用收款码');
// 微信/支付宝收款处理
$channel_info = null;
foreach ($channel_infos as $key => $value) {
$check_wx = preg_match('/^wxpay\d+#/i', $value->channel);
$check_ali = preg_match('/^alipay\d+#/i', $value->channel);
if ($check_wx && $type === 'wxpay') {
$channel_info = $channel_infos[$key];
break;
} elseif ($check_ali && $type === 'alipay') {
$channel_info = $channel_infos[$key];
break;
} else {
if ($check_wx || $check_ali) {
continue;
}
$channel_info = $channel_infos[$key];
break;
}
}
if (!$channel_info) return backMsg(3, '用户账户无可用收款通道');
// 选取收款通道
$patt = PayAccount::find($channel_info->account_id);
$channel = ['aid' => $channel_info->account_id, 'cid' => $channel_info->id, 'patt' => $patt->getData('pattern'), 'chan' => $channel_info->channel, 'platform' => $patt->getData('platform')];
PayChannel::update(['last_time' => self::getFormatTime(), 'id' => $channel['cid']]);
return backMsg(0, 'ok', $channel);
}
// 获取扩展参数数组
// private static function getParams(array $data): array
// {
// $keys = ['pid', 'type', 'out_trade_no', 'notify_url', 'return_url', 'name', 'money', 'sign', 'sign_type'];
// $params = [];
// foreach ($data as $key => $value) {
// if (!in_array($key, $keys)) {
// $params[$key] = $value;
// }
// }
// return $params;
// }
// 检查金额
private static function checkMoney($money, $type, $aid, $cid, $chan): float
{
$money = (float) $money;
// Alipay免输
if (preg_match('/^alipay4#\d+$/', $chan)) {
return $money;
}
// 查询有效订单
$query = self::scope('activeOrder')->where(['type' => $type, 'aid' => $aid, 'cid' => $cid]);
$activeOrders = $query->column('really_price');
$num = count($activeOrders);
if ($num > 0) {
for ($i = 0; $i < $num; $i++) {
if (in_array($money, $activeOrders)) {
$money += 0.01;
} else {
break;
}
}
}
return $money;
}
// 获取格式时间
private static function getFormatTime($time = 0)
{
if ($time) {
return date('Y-m-d H:i:s', $time);
}
return date('Y-m-d H:i:s', time());
}
// 生成订单号
private static function createOrderID(string $prefix = ''): string
{
$date = date('YmdHis');
$rand = rand(1000, 9999);
return $prefix . $date . $rand;
}
// 查询有效期内的未支付订单
public function scopeActiveOrder($query)
{
$query->where('close_time', '>', self::getFormatTime())->where('state', 0);
}
// 查询有效期内的成交订单
public function scopeDealOrder($query)
{
$query->where('close_time', '>', self::getFormatTime(time() - self::$activity_time))->where('state', 1);
}
// 查询超时过期订单
public function scopeTimeoutOrder($query)
{
$query->where('close_time', '<', self::getFormatTime())->where('state', 0);
}
// 模型多对一关联
public function payAccount()
{
return $this->belongsTo(PayAccount::class, 'aid', 'id');
}
}

89
app/model/PayAccount.php Normal file
View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace app\model;
use app\BaseModel;
use app\model\User;
use app\controller\api\PluginController;
class PayAccount extends BaseModel
{
// 查询账号列表
public static function serchAccount($query)
{
$select = [];
$allow_field = ['state', 'platform', 'account', 'pattern'];
foreach ($query as $key => $value) {
if (in_array($key, $allow_field) && isset($value)) {
if ($key === 'account') {
$select[] = [$key, 'like', '%' . $value . '%'];
continue;
}
$select[] = [$key, '=', $value];
}
}
return self::withCount(['payChannel' => 'channel'])->where($select);
}
// 获取账号配置
public static function getAccountConfig($aid, $pid = null): array|bool
{
$aid_info = self::find($aid);
if (!$aid_info) return false;
// 插件配置
$platform = PluginController::getPluginInfo($aid_info->getData('platform'));
// 查询参数
$params = json_decode($aid_info->params, true);
if ($aid_info && $platform) {
$config = [
'pid' => $aid_info->pid,
// 账号id
'aid' => $aid_info->id,
// 收款平台
'platform' => $aid_info->getData('platform'),
// 插件类名
'payclass' => $platform['class_name'],
// 账号
'account' => $aid_info->account,
// 密码
'password' => $aid_info->password,
// 配置参数
'params' => $params,
];
if ($pid !== null) {
$pid_info = User::where('pid', $pid)->find();
$config['key'] = $pid_info->secret_key;
}
return $config;
} else {
return false;
}
}
// 获取器
public function getPlatformAttr($value)
{
$payplugin_path = config_path() . '/extend/payplugin.php';
if (!file_exists($payplugin_path)) {
return [];
}
// 加载插件配置
$payplugin_config = require $payplugin_path;
$option = [];
foreach ($payplugin_config as $config) {
$option[$config['platform']] = $config['name'];
}
return isset($option[$value]) ? $option[$value] : '[已卸载,请停用]';
}
public function getPatternAttr($value)
{
// 监听模式
$pattern = ['0' => '单次监听·被动', '1' => '连续监听·主动'];
return $pattern[$value];
}
// 一对多关联
public function payChannel()
{
return $this->hasMany(PayChannel::class, 'account_id', 'id');
}
}

16
app/model/PayChannel.php Normal file
View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace app\model;
use app\BaseModel;
class PayChannel extends BaseModel
{
// 模型多对一关联
public function payAccount()
{
return $this->belongsTo(PayAccount::class, 'account_id', 'id');
}
}

46
app/model/User.php Normal file
View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace app\model;
use app\BaseModel;
class User extends BaseModel
{
// 验证用户
public static function checkUser($pid, $sign)
{
$user = self::where('pid', $pid)->find();
$sign2 = md5($user->pid . $user->secret_key);
if ($sign === $sign2) {
return true;
} else {
return false;
}
}
// 创建用户
public static function createUser(array $userinfo)
{
$last_pid = self::withTrashed()->max('pid');
$find_username = self::withTrashed()->where(['username' => $userinfo['username']])->find();
if ($find_username) {
return 1; // 账户已注册
}
$pid = $last_pid ? $last_pid + 1 : 1000;
$secret = md5($pid . time() . mt_rand());
$res = self::create(['pid' => $pid, 'secret_key' => $secret, 'username' => $userinfo['username'], 'password' => $userinfo['password'], 'nickname' => self::getNickname('小可爱', 5)]);
return $res;
}
// 随机用户昵称
private static function getNickname($pre = '', $length = 8)
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $pre . $randomString;
}
}

9
app/provider.php Normal file
View File

@ -0,0 +1,9 @@
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => ExceptionHandle::class,
];

9
app/service.php Normal file
View File

@ -0,0 +1,9 @@
<?php
use app\AppService;
// 系统服务定义文件
// 服务在完成全局初始化之后执行
return [
AppService::class,
];