From e524424628a2d3791b74378bb24d9de0d746c5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=8A=80=E6=9C=AF=E8=80=81=E8=83=A1?= <1094551889@qq.com> Date: Wed, 23 Apr 2025 23:51:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +- app/AppService.php | 22 ++ app/BaseController.php | 93 ++++++ app/BaseModel.php | 19 ++ app/ExceptionHandle.php | 58 ++++ app/Request.php | 8 + app/common.php | 10 + app/controller/ConsoleController.php | 79 +++++ app/controller/IndexController.php | 23 ++ app/controller/InstallController.php | 335 +++++++++++++++++++ app/controller/OrderController.php | 45 +++ app/controller/PayController.php | 369 +++++++++++++++++++++ app/controller/PayManageController.php | 77 +++++ app/controller/PluginController.php | 18 + app/controller/UserController.php | 39 +++ app/controller/api/ConsoleController.php | 44 +++ app/controller/api/OrderController.php | 196 +++++++++++ app/controller/api/PayManageController.php | 181 ++++++++++ app/controller/api/PluginController.php | 235 +++++++++++++ app/controller/api/UserController.php | 95 ++++++ app/event.php | 17 + app/middleware.php | 10 + app/middleware/Auth.php | 30 ++ app/model/Order.php | 215 ++++++++++++ app/model/PayAccount.php | 89 +++++ app/model/PayChannel.php | 16 + app/model/User.php | 46 +++ app/provider.php | 9 + app/service.php | 9 + 29 files changed, 2388 insertions(+), 6 deletions(-) create mode 100644 app/AppService.php create mode 100644 app/BaseController.php create mode 100644 app/BaseModel.php create mode 100644 app/ExceptionHandle.php create mode 100644 app/Request.php create mode 100644 app/common.php create mode 100644 app/controller/ConsoleController.php create mode 100644 app/controller/IndexController.php create mode 100644 app/controller/InstallController.php create mode 100644 app/controller/OrderController.php create mode 100644 app/controller/PayController.php create mode 100644 app/controller/PayManageController.php create mode 100644 app/controller/PluginController.php create mode 100644 app/controller/UserController.php create mode 100644 app/controller/api/ConsoleController.php create mode 100644 app/controller/api/OrderController.php create mode 100644 app/controller/api/PayManageController.php create mode 100644 app/controller/api/PluginController.php create mode 100644 app/controller/api/UserController.php create mode 100644 app/event.php create mode 100644 app/middleware.php create mode 100644 app/middleware/Auth.php create mode 100644 app/model/Order.php create mode 100644 app/model/PayAccount.php create mode 100644 app/model/PayChannel.php create mode 100644 app/model/User.php create mode 100644 app/provider.php create mode 100644 app/service.php diff --git a/.gitignore b/.gitignore index ba92074..de0f44c 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* \ No newline at end of file diff --git a/app/AppService.php b/app/AppService.php new file mode 100644 index 0000000..96556e8 --- /dev/null +++ b/app/AppService.php @@ -0,0 +1,22 @@ +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); + } +} diff --git a/app/BaseModel.php b/app/BaseModel.php new file mode 100644 index 0000000..2345c8e --- /dev/null +++ b/app/BaseModel.php @@ -0,0 +1,19 @@ + $code, 'msg' => $msg]; + if ($data) { + $back_msg['data'] = $data; + } + return $back_msg; +} diff --git a/app/controller/ConsoleController.php b/app/controller/ConsoleController.php new file mode 100644 index 0000000..3d1d42b --- /dev/null +++ b/app/controller/ConsoleController.php @@ -0,0 +1,79 @@ + 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; + } +} diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php new file mode 100644 index 0000000..d80db47 --- /dev/null +++ b/app/controller/IndexController.php @@ -0,0 +1,23 @@ +domain()); + return View::fetch(); + } +} diff --git a/app/controller/InstallController.php b/app/controller/InstallController.php new file mode 100644 index 0000000..196c4ad --- /dev/null +++ b/app/controller/InstallController.php @@ -0,0 +1,335 @@ +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 <<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("无法写入安装锁文件"); + } + } +} diff --git a/app/controller/OrderController.php b/app/controller/OrderController.php new file mode 100644 index 0000000..021f7d0 --- /dev/null +++ b/app/controller/OrderController.php @@ -0,0 +1,45 @@ +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(); + } +} diff --git a/app/controller/PayController.php b/app/controller/PayController.php new file mode 100644 index 0000000..5e9c58c --- /dev/null +++ b/app/controller/PayController.php @@ -0,0 +1,369 @@ +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; + } +} diff --git a/app/controller/PayManageController.php b/app/controller/PayManageController.php new file mode 100644 index 0000000..12e224f --- /dev/null +++ b/app/controller/PayManageController.php @@ -0,0 +1,77 @@ +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(); + } +} diff --git a/app/controller/PluginController.php b/app/controller/PluginController.php new file mode 100644 index 0000000..ea210a1 --- /dev/null +++ b/app/controller/PluginController.php @@ -0,0 +1,18 @@ + ['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(); + } +} diff --git a/app/controller/api/ConsoleController.php b/app/controller/api/ConsoleController.php new file mode 100644 index 0000000..a5eab96 --- /dev/null +++ b/app/controller/api/ConsoleController.php @@ -0,0 +1,44 @@ +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); + } +} diff --git a/app/controller/api/OrderController.php b/app/controller/api/OrderController.php new file mode 100644 index 0000000..80b8ea7 --- /dev/null +++ b/app/controller/api/OrderController.php @@ -0,0 +1,196 @@ +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; + } +} diff --git a/app/controller/api/PayManageController.php b/app/controller/api/PayManageController.php new file mode 100644 index 0000000..bdbe9f2 --- /dev/null +++ b/app/controller/api/PayManageController.php @@ -0,0 +1,181 @@ +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']]); + } + } +} diff --git a/app/controller/api/PluginController.php b/app/controller/api/PluginController.php new file mode 100644 index 0000000..0af35fa --- /dev/null +++ b/app/controller/api/PluginController.php @@ -0,0 +1,235 @@ +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 = << ['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; + } + } +} diff --git a/app/event.php b/app/event.php new file mode 100644 index 0000000..e9851bb --- /dev/null +++ b/app/event.php @@ -0,0 +1,17 @@ + [ + ], + + 'listen' => [ + 'AppInit' => [], + 'HttpRun' => [], + 'HttpEnd' => [], + 'LogLevel' => [], + 'LogWrite' => [], + ], + + 'subscribe' => [ + ], +]; diff --git a/app/middleware.php b/app/middleware.php new file mode 100644 index 0000000..32d02ce --- /dev/null +++ b/app/middleware.php @@ -0,0 +1,10 @@ +isJson(); + if ($method) { + return \json(\backMsg(404, '身份过期,请重新登陆')); + } + return redirect('/User/login'); + } + } +} diff --git a/app/model/Order.php b/app/model/Order.php new file mode 100644 index 0000000..82eef1d --- /dev/null +++ b/app/model/Order.php @@ -0,0 +1,215 @@ + 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'); + } +} diff --git a/app/model/PayAccount.php b/app/model/PayAccount.php new file mode 100644 index 0000000..41b8f3e --- /dev/null +++ b/app/model/PayAccount.php @@ -0,0 +1,89 @@ + $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'); + } +} diff --git a/app/model/PayChannel.php b/app/model/PayChannel.php new file mode 100644 index 0000000..ed2ee6b --- /dev/null +++ b/app/model/PayChannel.php @@ -0,0 +1,16 @@ +belongsTo(PayAccount::class, 'account_id', 'id'); + } +} diff --git a/app/model/User.php b/app/model/User.php new file mode 100644 index 0000000..2954e65 --- /dev/null +++ b/app/model/User.php @@ -0,0 +1,46 @@ +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; + } +} diff --git a/app/provider.php b/app/provider.php new file mode 100644 index 0000000..73d99fa --- /dev/null +++ b/app/provider.php @@ -0,0 +1,9 @@ + Request::class, + 'think\exception\Handle' => ExceptionHandle::class, +]; diff --git a/app/service.php b/app/service.php new file mode 100644 index 0000000..db1ee6a --- /dev/null +++ b/app/service.php @@ -0,0 +1,9 @@ +