first commit
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
USER_TOKEN_TTL=604800
|
||||
ADMIN_TOKEN_TTL=86400
|
||||
ADMIN_INIT_USERNAME=admin
|
||||
ADMIN_INIT_PASSWORD=
|
||||
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/runtime
|
||||
/.idea
|
||||
/.vscode
|
||||
/vendor
|
||||
*.log
|
||||
.env
|
||||
/tests/tmp
|
||||
/tests/.phpunit.result.cache
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM php:8.3.22-cli-alpine
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update --no-cache \
|
||||
&& docker-php-source extract
|
||||
|
||||
# install extensions
|
||||
RUN docker-php-ext-install pdo pdo_mysql -j$(nproc) pcntl
|
||||
|
||||
# enable opcache and pcntl
|
||||
RUN docker-php-ext-enable opcache pcntl
|
||||
RUN docker-php-source delete \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
70
README.md
Normal file
70
README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
<div style="padding:18px;max-width: 1024px;margin:0 auto;background-color:#fff;color:#333">
|
||||
<h1>webman</h1>
|
||||
|
||||
基于<a href="https://www.workerman.net" target="__blank">workerman</a>开发的超高性能PHP框架
|
||||
|
||||
|
||||
<h1>学习</h1>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/webman" target="__blank">主页 / Home page</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://webman.workerman.net" target="__blank">文档 / Document</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/doc/webman/install.html" target="__blank">安装 / Install</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/questions" target="__blank">问答 / Questions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/apps" target="__blank">市场 / Apps</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/sponsor" target="__blank">赞助 / Sponsors</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.workerman.net/doc/webman/thanks.html" target="__blank">致谢 / Thanks</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div style="float:left;padding-bottom:30px;">
|
||||
|
||||
<h1>赞助商</h1>
|
||||
|
||||
<h4>特别赞助</h4>
|
||||
<a href="https://www.crmeb.com/?form=workerman" target="__blank">
|
||||
<img src="https://www.workerman.net/img/sponsors/6429/20230719111500.svg" width="200">
|
||||
</a>
|
||||
|
||||
<h4>铂金赞助</h4>
|
||||
<a href="https://www.fadetask.com/?from=workerman" target="__blank"><img src="https://www.workerman.net/img/sponsors/1/20230719084316.png" width="200"></a>
|
||||
<a href="https://www.yilianyun.net/?from=workerman" target="__blank" style="margin-left:20px;"><img src="https://www.workerman.net/img/sponsors/6218/20230720114049.png" width="200"></a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div style="float:left;padding-bottom:30px;clear:both">
|
||||
|
||||
<h1>请作者喝咖啡</h1>
|
||||
|
||||
<img src="https://www.workerman.net/img/wx_donate.png" width="200">
|
||||
<img src="https://www.workerman.net/img/ali_donate.png" width="200">
|
||||
<br>
|
||||
<b>如果您觉得webman对您有所帮助,欢迎捐赠。</b>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div style="clear: both">
|
||||
<h1>LICENSE</h1>
|
||||
The webman is open-sourced software licensed under the MIT.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
73
alter_orders_pay.php
Normal file
73
alter_orders_pay.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
if (Capsule::schema()->hasTable('orders')) {
|
||||
if (!Capsule::schema()->hasColumn('orders', 'pay_channel')) {
|
||||
Capsule::schema()->table('orders', function ($table) {
|
||||
$table->string('pay_channel', 20)->nullable()->after('pay_time');
|
||||
});
|
||||
echo "Added 'pay_channel' to orders.\n";
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('orders', 'pay_status')) {
|
||||
Capsule::schema()->table('orders', function ($table) {
|
||||
$table->string('pay_status', 20)->default('unpaid')->after('pay_channel');
|
||||
});
|
||||
echo "Added 'pay_status' to orders.\n";
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('orders', 'pay_merchant_id')) {
|
||||
Capsule::schema()->table('orders', function ($table) {
|
||||
$table->unsignedBigInteger('pay_merchant_id')->default(0)->after('pay_status');
|
||||
});
|
||||
echo "Added 'pay_merchant_id' to orders.\n";
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('orders', 'pay_out_trade_no')) {
|
||||
Capsule::schema()->table('orders', function ($table) {
|
||||
$table->string('pay_out_trade_no', 64)->nullable()->after('pay_merchant_id');
|
||||
});
|
||||
echo "Added 'pay_out_trade_no' to orders.\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('payment_transactions')) {
|
||||
Capsule::schema()->create('payment_transactions', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('order_id')->index();
|
||||
$table->string('channel', 20)->default('wechat')->index();
|
||||
$table->unsignedBigInteger('merchant_id')->default(0)->index();
|
||||
$table->string('out_trade_no', 64)->unique();
|
||||
$table->decimal('amount', 10, 2)->default(0.00);
|
||||
$table->string('status', 20)->default('created')->index();
|
||||
$table->string('prepay_id', 64)->nullable();
|
||||
$table->string('code_url', 255)->nullable();
|
||||
$table->json('raw_json')->nullable();
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
echo "Table 'payment_transactions' created successfully.\n";
|
||||
}
|
||||
|
||||
echo "Alter orders_pay completed.\n";
|
||||
|
||||
39
alter_orders_return.php
Normal file
39
alter_orders_return.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
if (!Capsule::schema()->hasColumn('orders', 'return_express_company')) {
|
||||
Capsule::schema()->table('orders', function ($table) {
|
||||
$table->string('return_express_company', 32)->nullable()->after('express_no');
|
||||
$table->string('return_express_no', 64)->nullable()->after('return_express_company');
|
||||
$table->timestamp('return_ship_time')->nullable()->after('return_express_no');
|
||||
});
|
||||
echo "Added return shipping fields to orders.\n";
|
||||
}
|
||||
|
||||
echo "Alter orders completed.\n";
|
||||
|
||||
48
alter_tables.php
Normal file
48
alter_tables.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
// 加载环境变量
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
// 补充 roles 字段
|
||||
if (!Capsule::schema()->hasColumn('roles', 'description')) {
|
||||
Capsule::schema()->table('roles', function ($table) {
|
||||
$table->string('description', 255)->nullable()->after('name');
|
||||
});
|
||||
echo "Added 'description' to roles.\n";
|
||||
}
|
||||
|
||||
// 补充 permissions 字段
|
||||
if (!Capsule::schema()->hasColumn('permissions', 'parent_id')) {
|
||||
Capsule::schema()->table('permissions', function ($table) {
|
||||
$table->unsignedBigInteger('parent_id')->default(0)->after('id');
|
||||
$table->tinyInteger('type')->default(1)->comment('1菜单 2按钮')->after('code');
|
||||
$table->integer('sort')->default(0)->after('type');
|
||||
});
|
||||
echo "Added 'parent_id', 'type', 'sort' to permissions.\n";
|
||||
}
|
||||
|
||||
echo "Alter tables completed.\n";
|
||||
44
alter_user_wechat_identities.php
Normal file
44
alter_user_wechat_identities.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
if (!Capsule::schema()->hasTable('user_wechat_identities')) {
|
||||
Capsule::schema()->create('user_wechat_identities', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->string('app_id', 32)->index();
|
||||
$table->string('openid', 64)->index();
|
||||
$table->string('unionid', 64)->nullable()->index();
|
||||
$table->string('scene', 20)->default('unknown')->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'app_id']);
|
||||
$table->unique(['app_id', 'openid']);
|
||||
});
|
||||
echo "Table 'user_wechat_identities' created successfully.\n";
|
||||
}
|
||||
|
||||
echo "Alter user_wechat_identities completed.\n";
|
||||
|
||||
45
alter_wechat_apps.php
Normal file
45
alter_wechat_apps.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
if (!Capsule::schema()->hasTable('wechat_apps')) {
|
||||
Capsule::schema()->create('wechat_apps', function ($table) {
|
||||
$table->id();
|
||||
$table->string('name', 50);
|
||||
$table->string('type', 20)->default('h5');
|
||||
$table->string('app_id', 32)->unique();
|
||||
$table->string('app_secret', 64)->nullable();
|
||||
$table->tinyInteger('status')->default(1);
|
||||
$table->string('remark', 255)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['type']);
|
||||
$table->index(['status']);
|
||||
});
|
||||
echo "Table 'wechat_apps' created successfully.\n";
|
||||
}
|
||||
|
||||
echo "Alter wechat_apps completed.\n";
|
||||
|
||||
51
alter_wechat_merchants.php
Normal file
51
alter_wechat_merchants.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
if (!Capsule::schema()->hasTable('wechat_merchants')) {
|
||||
Capsule::schema()->create('wechat_merchants', function ($table) {
|
||||
$table->id();
|
||||
$table->string('name', 50);
|
||||
$table->string('mode', 20)->default('direct');
|
||||
$table->string('mch_id', 32);
|
||||
$table->string('app_id', 32)->nullable();
|
||||
$table->string('sub_mch_id', 32)->nullable();
|
||||
$table->string('sub_app_id', 32)->nullable();
|
||||
$table->string('service_provider', 50)->nullable();
|
||||
$table->tinyInteger('is_default')->default(0);
|
||||
$table->tinyInteger('status')->default(1);
|
||||
$table->string('remark', 255)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mch_id']);
|
||||
$table->index(['mode']);
|
||||
$table->index(['status']);
|
||||
$table->index(['is_default']);
|
||||
});
|
||||
echo "Table 'wechat_merchants' created successfully.\n";
|
||||
}
|
||||
|
||||
echo "Alter wechat_merchants completed.\n";
|
||||
|
||||
46
alter_wechat_merchants_cert_files.php
Normal file
46
alter_wechat_merchants_cert_files.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
if (!Capsule::schema()->hasTable('wechat_merchants')) {
|
||||
echo "Table 'wechat_merchants' not found.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasColumn('wechat_merchants', 'apiclient_cert_path')) {
|
||||
Capsule::schema()->table('wechat_merchants', function ($table) {
|
||||
$table->string('apiclient_cert_path', 255)->nullable()->after('private_key_pem');
|
||||
});
|
||||
echo "Added 'apiclient_cert_path' to wechat_merchants.\n";
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('wechat_merchants', 'apiclient_key_path')) {
|
||||
Capsule::schema()->table('wechat_merchants', function ($table) {
|
||||
$table->string('apiclient_key_path', 255)->nullable()->after('apiclient_cert_path');
|
||||
});
|
||||
echo "Added 'apiclient_key_path' to wechat_merchants.\n";
|
||||
}
|
||||
|
||||
echo "Alter wechat_merchants_cert_files completed.\n";
|
||||
|
||||
58
alter_wechat_merchants_pay.php
Normal file
58
alter_wechat_merchants_pay.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
if (!Capsule::schema()->hasTable('wechat_merchants')) {
|
||||
echo "Table 'wechat_merchants' not found.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasColumn('wechat_merchants', 'serial_no')) {
|
||||
Capsule::schema()->table('wechat_merchants', function ($table) {
|
||||
$table->string('serial_no', 64)->nullable()->after('app_id');
|
||||
});
|
||||
echo "Added 'serial_no' to wechat_merchants.\n";
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('wechat_merchants', 'api_v3_key')) {
|
||||
Capsule::schema()->table('wechat_merchants', function ($table) {
|
||||
$table->string('api_v3_key', 64)->nullable()->after('serial_no');
|
||||
});
|
||||
echo "Added 'api_v3_key' to wechat_merchants.\n";
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('wechat_merchants', 'private_key_pem')) {
|
||||
Capsule::schema()->table('wechat_merchants', function ($table) {
|
||||
$table->text('private_key_pem')->nullable()->after('api_v3_key');
|
||||
});
|
||||
echo "Added 'private_key_pem' to wechat_merchants.\n";
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('wechat_merchants', 'notify_url')) {
|
||||
Capsule::schema()->table('wechat_merchants', function ($table) {
|
||||
$table->string('notify_url', 255)->nullable()->after('private_key_pem');
|
||||
});
|
||||
echo "Added 'notify_url' to wechat_merchants.\n";
|
||||
}
|
||||
|
||||
echo "Alter wechat_merchants_pay completed.\n";
|
||||
|
||||
134
app/admin/controller/AdminUserController.php
Normal file
134
app/admin/controller/AdminUserController.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\AdminUser;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class AdminUserController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
|
||||
$query = AdminUser::with('roles');
|
||||
|
||||
if ($username = $request->get('username')) {
|
||||
$query->where('username', 'like', "%{$username}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$username = trim($request->post('username', ''));
|
||||
$password = $request->post('password', '');
|
||||
$roleIds = $request->post('role_ids', []);
|
||||
|
||||
if (!$username || !$password) {
|
||||
return jsonResponse(null, '用户名和密码必填', 400);
|
||||
}
|
||||
|
||||
if (AdminUser::where('username', $username)->exists()) {
|
||||
return jsonResponse(null, '用户名已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$admin = AdminUser::create([
|
||||
'username' => $username,
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'status' => (int)$request->post('status', 1),
|
||||
'is_super' => (int)$request->post('is_super', 0),
|
||||
]);
|
||||
|
||||
if (!empty($roleIds)) {
|
||||
$admin->roles()->sync($roleIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '创建成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$admin = AdminUser::find($id);
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '用户不存在', 404);
|
||||
}
|
||||
|
||||
$username = trim($request->post('username', ''));
|
||||
if ($username && $username !== $admin->username) {
|
||||
if (AdminUser::where('username', $username)->exists()) {
|
||||
return jsonResponse(null, '用户名已存在', 400);
|
||||
}
|
||||
$admin->username = $username;
|
||||
}
|
||||
|
||||
$password = $request->post('password');
|
||||
if ($password) {
|
||||
$admin->password_hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
if ($request->post('status') !== null) {
|
||||
$admin->status = (int)$request->post('status');
|
||||
}
|
||||
|
||||
if ($request->post('is_super') !== null) {
|
||||
$admin->is_super = (int)$request->post('is_super');
|
||||
}
|
||||
|
||||
$roleIds = $request->post('role_ids');
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$admin->save();
|
||||
if (is_array($roleIds)) {
|
||||
$admin->roles()->sync($roleIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
if ($id === 1) {
|
||||
return jsonResponse(null, '超级管理员不可删除', 403);
|
||||
}
|
||||
$admin = AdminUser::find($id);
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '用户不存在', 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$admin->roles()->detach();
|
||||
$admin->delete();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '删除成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '删除失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
app/admin/controller/AuthController.php
Normal file
64
app/admin/controller/AuthController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\AdminUser;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
public function login(Request $request)
|
||||
{
|
||||
$username = trim((string)$request->post('username', ''));
|
||||
$password = (string)$request->post('password', '');
|
||||
if ($username === '' || $password === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
|
||||
$admin = AdminUser::where('username', $username)->first();
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '账号或密码错误', 401);
|
||||
}
|
||||
if (intval($admin->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
if (!password_verify($password, $admin->password_hash)) {
|
||||
return jsonResponse(null, '账号或密码错误', 401);
|
||||
}
|
||||
|
||||
$token = AuthService::issueAdminToken($admin);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'admin' => $admin
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function me(Request $request)
|
||||
{
|
||||
$admin = $request->admin;
|
||||
$permissions = [];
|
||||
if (intval($admin->is_super) === 1) {
|
||||
$permissions = ['*'];
|
||||
} else {
|
||||
$admin->loadMissing(['roles.permissions']);
|
||||
$map = [];
|
||||
foreach ($admin->roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$map[$permission->code] = true;
|
||||
}
|
||||
}
|
||||
$permissions = array_keys($map);
|
||||
}
|
||||
return jsonResponse([
|
||||
'admin' => $admin,
|
||||
'permissions' => $permissions
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
AuthService::revokeAdminToken($request->token ?? null);
|
||||
return jsonResponse(null, '已退出登录');
|
||||
}
|
||||
}
|
||||
|
||||
26
app/admin/controller/DashboardController.php
Normal file
26
app/admin/controller/DashboardController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\Report;
|
||||
|
||||
class DashboardController
|
||||
{
|
||||
public function stat(Request $request)
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
|
||||
$stat = [
|
||||
'today_orders' => Order::where('created_at', '>=', $today . ' 00:00:00')->count(),
|
||||
'wait_receive_orders' => Order::whereIn('status', ['shipping', 'wait_receive'])->count(),
|
||||
'inspecting_orders' => Order::where('status', 'inspecting')->count(),
|
||||
'today_reports' => Report::where('created_at', '>=', $today . ' 00:00:00')->count(),
|
||||
'total_users' => User::count(),
|
||||
'total_amount' => Order::where('status', 'finished')->sum('total_price') ?? 0
|
||||
];
|
||||
|
||||
return jsonResponse($stat);
|
||||
}
|
||||
}
|
||||
85
app/admin/controller/OrderController.php
Normal file
85
app/admin/controller/OrderController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
|
||||
class OrderController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$status = $request->get('status', 'all');
|
||||
$page = max(1, intval($request->get('page', 1)));
|
||||
$pageSize = min(50, max(1, intval($request->get('page_size', 10))));
|
||||
|
||||
$query = Order::query();
|
||||
if ($status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
if ($orderNo = $request->get('order_no')) {
|
||||
$query->where('order_no', 'like', "%{$orderNo}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('id', 'desc')
|
||||
->offset(($page - 1) * $pageSize)
|
||||
->limit($pageSize)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$id = (int)$request->get('id');
|
||||
$order = Order::with(['logs'])->find($id);
|
||||
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
return jsonResponse($order);
|
||||
}
|
||||
|
||||
public function receive(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$order = Order::find($id);
|
||||
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
\app\common\service\OrderFlowService::adminReceive($order, $request->admin->id);
|
||||
return jsonResponse(null, '确认收件成功,已进入鉴定状态');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function returnShip(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$expressCompany = trim((string)$request->post('express_company', ''));
|
||||
$expressNo = trim((string)$request->post('express_no', ''));
|
||||
|
||||
$order = Order::find($id);
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
\app\common\service\OrderFlowService::adminReturnShip($order, $request->admin->id, $expressCompany, $expressNo);
|
||||
return jsonResponse(null, '回寄信息已提交');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
app/admin/controller/PermissionController.php
Normal file
94
app/admin/controller/PermissionController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Permission;
|
||||
|
||||
class PermissionController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
// 返回树形结构或者普通列表
|
||||
$permissions = Permission::orderBy('sort', 'asc')->get();
|
||||
return jsonResponse($permissions);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim($request->post('name', ''));
|
||||
$code = trim($request->post('code', ''));
|
||||
$parent_id = (int)$request->post('parent_id', 0);
|
||||
$type = (int)$request->post('type', 1); // 1菜单 2按钮
|
||||
$sort = (int)$request->post('sort', 0);
|
||||
|
||||
if (!$name || !$code) {
|
||||
return jsonResponse(null, '名称和代码必填', 400);
|
||||
}
|
||||
|
||||
if (Permission::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '代码已存在', 400);
|
||||
}
|
||||
|
||||
$permission = Permission::create([
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'parent_id' => $parent_id,
|
||||
'type' => $type,
|
||||
'sort' => $sort,
|
||||
]);
|
||||
|
||||
return jsonResponse($permission, '创建成功');
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$permission = Permission::find($id);
|
||||
if (!$permission) {
|
||||
return jsonResponse(null, '权限不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim($request->post('name', ''));
|
||||
$code = trim($request->post('code', ''));
|
||||
|
||||
if ($name) $permission->name = $name;
|
||||
if ($code && $code !== $permission->code) {
|
||||
if (Permission::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '代码已存在', 400);
|
||||
}
|
||||
$permission->code = $code;
|
||||
}
|
||||
|
||||
if ($request->post('parent_id') !== null) {
|
||||
$permission->parent_id = (int)$request->post('parent_id');
|
||||
}
|
||||
if ($request->post('type') !== null) {
|
||||
$permission->type = (int)$request->post('type');
|
||||
}
|
||||
if ($request->post('sort') !== null) {
|
||||
$permission->sort = (int)$request->post('sort');
|
||||
}
|
||||
|
||||
$permission->save();
|
||||
return jsonResponse(null, '更新成功');
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$permission = Permission::find($id);
|
||||
if (!$permission) {
|
||||
return jsonResponse(null, '权限不存在', 404);
|
||||
}
|
||||
|
||||
if (Permission::where('parent_id', $id)->exists()) {
|
||||
return jsonResponse(null, '存在子权限,不可删除', 400);
|
||||
}
|
||||
|
||||
$permission->delete();
|
||||
// Remove from role_permissions
|
||||
\Illuminate\Database\Capsule\Manager::table('role_permissions')->where('permission_id', $id)->delete();
|
||||
|
||||
return jsonResponse(null, '删除成功');
|
||||
}
|
||||
}
|
||||
114
app/admin/controller/ReportController.php
Normal file
114
app/admin/controller/ReportController.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\Report;
|
||||
use app\common\service\OrderFlowService;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class ReportController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = max(1, intval($request->get('page', 1)));
|
||||
$pageSize = min(50, max(1, intval($request->get('page_size', 10))));
|
||||
|
||||
$query = Report::with(['order', 'inspector']);
|
||||
|
||||
if ($reportNo = $request->get('report_no')) {
|
||||
$query->where('report_no', 'like', "%{$reportNo}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('id', 'desc')
|
||||
->offset(($page - 1) * $pageSize)
|
||||
->limit($pageSize)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$id = (int)$request->get('id');
|
||||
$report = Report::with(['order.logs', 'inspector'])->find($id);
|
||||
|
||||
if (!$report) {
|
||||
return jsonResponse(null, '报告不存在', 404);
|
||||
}
|
||||
|
||||
return jsonResponse($report);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->post('order_id');
|
||||
$conclusion = trim($request->post('conclusion', '')); // REAL, FAKE, DOUBT
|
||||
$level = trim($request->post('level', ''));
|
||||
$flawsJson = $request->post('flaws_json', []);
|
||||
$imagesJson = $request->post('images_json', []);
|
||||
|
||||
if (!in_array($conclusion, ['REAL', 'FAKE', 'DOUBT'])) {
|
||||
return jsonResponse(null, '鉴定结论不合法', 400);
|
||||
}
|
||||
|
||||
if (empty($imagesJson)) {
|
||||
return jsonResponse(null, '必须上传证据图片', 400);
|
||||
}
|
||||
|
||||
$order = Order::find($orderId);
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
if ($order->status !== 'inspecting') {
|
||||
return jsonResponse(null, '订单当前状态不可出具报告', 400);
|
||||
}
|
||||
|
||||
if (Report::where('order_id', $orderId)->exists()) {
|
||||
return jsonResponse(null, '该订单已出具报告,不可重复出具', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$reportNo = 'R' . date('YmdHis') . rand(1000, 9999);
|
||||
$verifyCode = bin2hex(random_bytes(8)); // 16字符防伪码
|
||||
|
||||
$report = Report::create([
|
||||
'report_no' => $reportNo,
|
||||
'order_id' => $orderId,
|
||||
'conclusion' => $conclusion,
|
||||
'level' => $level,
|
||||
'flaws_json' => $flawsJson,
|
||||
'images_json' => $imagesJson,
|
||||
'inspector_id' => $request->admin->id,
|
||||
'verify_code' => $verifyCode
|
||||
]);
|
||||
|
||||
// 扭转订单状态
|
||||
$order->status = 'finished';
|
||||
$order->save();
|
||||
|
||||
$conclusionMap = [
|
||||
'REAL' => '正品',
|
||||
'FAKE' => '仿品',
|
||||
'DOUBT' => '存疑'
|
||||
];
|
||||
$conclusionText = $conclusionMap[$conclusion] ?? '未知';
|
||||
|
||||
OrderFlowService::addLog($orderId, 'report_generated', '报告已出具', "鉴定结论:{$conclusionText}", 'admin', $request->admin->id);
|
||||
|
||||
DB::commit();
|
||||
return jsonResponse($report, '报告出具成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '出具报告失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
140
app/admin/controller/RoleController.php
Normal file
140
app/admin/controller/RoleController.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Role;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class RoleController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
|
||||
$query = Role::with('permissions');
|
||||
|
||||
if ($name = $request->get('name')) {
|
||||
$query->where('name', 'like', "%{$name}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list
|
||||
]);
|
||||
}
|
||||
|
||||
public function all(Request $request)
|
||||
{
|
||||
$roles = Role::all();
|
||||
return jsonResponse($roles);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim($request->post('name', ''));
|
||||
$code = trim($request->post('code', ''));
|
||||
$description = trim($request->post('description', ''));
|
||||
$permissionIds = $request->post('permission_ids', []);
|
||||
|
||||
if (!$name || !$code) {
|
||||
return jsonResponse(null, '角色名称和编码必填', 400);
|
||||
}
|
||||
|
||||
if (Role::where('name', $name)->exists()) {
|
||||
return jsonResponse(null, '角色名称已存在', 400);
|
||||
}
|
||||
if (Role::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '角色编码已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$role = Role::create([
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'description' => $description,
|
||||
]);
|
||||
|
||||
if (!empty($permissionIds)) {
|
||||
$role->permissions()->sync($permissionIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '创建成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$role = Role::find($id);
|
||||
if (!$role) {
|
||||
return jsonResponse(null, '角色不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim($request->post('name', ''));
|
||||
if ($name && $name !== $role->name) {
|
||||
if (Role::where('name', $name)->exists()) {
|
||||
return jsonResponse(null, '角色名称已存在', 400);
|
||||
}
|
||||
$role->name = $name;
|
||||
}
|
||||
|
||||
$code = trim($request->post('code', ''));
|
||||
if ($code && $code !== $role->code) {
|
||||
if (Role::where('code', $code)->exists()) {
|
||||
return jsonResponse(null, '角色编码已存在', 400);
|
||||
}
|
||||
$role->code = $code;
|
||||
}
|
||||
|
||||
if ($request->post('description') !== null) {
|
||||
$role->description = trim($request->post('description'));
|
||||
}
|
||||
|
||||
$permissionIds = $request->post('permission_ids');
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$role->save();
|
||||
if (is_array($permissionIds)) {
|
||||
$role->permissions()->sync($permissionIds);
|
||||
}
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$role = Role::find($id);
|
||||
if (!$role) {
|
||||
return jsonResponse(null, '角色不存在', 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$role->permissions()->detach();
|
||||
$role->delete();
|
||||
DB::table('admin_roles')->where('role_id', $id)->delete();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '删除成功');
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '删除失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/admin/controller/UploadController.php
Normal file
37
app/admin/controller/UploadController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
class UploadController
|
||||
{
|
||||
public function image(Request $request)
|
||||
{
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
|
||||
$ext = strtolower($file->getUploadExtension());
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
return jsonResponse(null, '仅支持图片文件', 400);
|
||||
}
|
||||
|
||||
$dir = public_path() . '/upload/images/' . date('Ymd');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$filename = uniqid() . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$path = $dir . '/' . $filename;
|
||||
$file->move($path);
|
||||
|
||||
$url = '/upload/images/' . date('Ymd') . '/' . $filename;
|
||||
|
||||
return jsonResponse([
|
||||
'url' => $url,
|
||||
'name' => $file->getUploadName(),
|
||||
], '上传成功');
|
||||
}
|
||||
}
|
||||
53
app/admin/controller/UserController.php
Normal file
53
app/admin/controller/UserController.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = max(1, intval($request->get('page', 1)));
|
||||
$pageSize = min(50, max(1, intval($request->get('page_size', 10))));
|
||||
|
||||
$query = User::query();
|
||||
|
||||
if ($mobile = $request->get('mobile')) {
|
||||
$query->where('mobile', 'like', "%{$mobile}%");
|
||||
}
|
||||
|
||||
if ($nickname = $request->get('nickname')) {
|
||||
$query->where('nickname', 'like', "%{$nickname}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('id', 'desc')
|
||||
->offset(($page - 1) * $pageSize)
|
||||
->limit($pageSize)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$status = (int)$request->post('status');
|
||||
|
||||
$user = User::find($id);
|
||||
if (!$user) {
|
||||
return jsonResponse(null, '用户不存在', 404);
|
||||
}
|
||||
|
||||
$user->status = $status === 1 ? 1 : 0;
|
||||
$user->save();
|
||||
|
||||
return jsonResponse(null, '更新状态成功');
|
||||
}
|
||||
}
|
||||
136
app/admin/controller/WechatAppController.php
Normal file
136
app/admin/controller/WechatAppController.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\WechatApp;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class WechatAppController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
if ($page < 1) $page = 1;
|
||||
if ($limit < 1) $limit = 15;
|
||||
|
||||
$query = WechatApp::query();
|
||||
if (($name = trim((string)$request->get('name', ''))) !== '') {
|
||||
$query->where('name', 'like', "%{$name}%");
|
||||
}
|
||||
if (($appId = trim((string)$request->get('app_id', ''))) !== '') {
|
||||
$query->where('app_id', 'like', "%{$appId}%");
|
||||
}
|
||||
if (($type = trim((string)$request->get('type', ''))) !== '') {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
if ($request->get('status') !== null && $request->get('status') !== '') {
|
||||
$query->where('status', (int)$request->get('status'));
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->select([
|
||||
'id',
|
||||
'name',
|
||||
'type',
|
||||
'app_id',
|
||||
'status',
|
||||
'remark',
|
||||
'created_at',
|
||||
])
|
||||
->selectRaw("IF(app_secret IS NULL OR app_secret = '', 0, 1) as has_secret")
|
||||
->orderByDesc('id')
|
||||
->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim((string)$request->post('name', ''));
|
||||
$type = trim((string)$request->post('type', 'h5'));
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$appSecret = trim((string)$request->post('app_secret', ''));
|
||||
$status = (int)$request->post('status', 1);
|
||||
$remark = trim((string)$request->post('remark', ''));
|
||||
|
||||
if ($name === '' || $appId === '') {
|
||||
return jsonResponse(null, '名称和AppID必填', 400);
|
||||
}
|
||||
if (!in_array($type, ['h5', 'mini'], true)) {
|
||||
return jsonResponse(null, '类型不合法', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$row = WechatApp::create([
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'app_id' => $appId,
|
||||
'app_secret' => $appSecret ?: null,
|
||||
'status' => $status ? 1 : 0,
|
||||
'remark' => $remark ?: null,
|
||||
]);
|
||||
return jsonResponse($row, '创建成功');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatApp::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '应用不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim((string)$request->post('name', $row->name));
|
||||
$type = trim((string)$request->post('type', $row->type));
|
||||
$appId = trim((string)$request->post('app_id', $row->app_id));
|
||||
$appSecret = trim((string)$request->post('app_secret', ''));
|
||||
$status = (int)$request->post('status', $row->status);
|
||||
$remark = trim((string)$request->post('remark', $row->remark ?? ''));
|
||||
|
||||
if ($name === '' || $appId === '') {
|
||||
return jsonResponse(null, '名称和AppID必填', 400);
|
||||
}
|
||||
if (!in_array($type, ['h5', 'mini'], true)) {
|
||||
return jsonResponse(null, '类型不合法', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$row->name = $name;
|
||||
$row->type = $type;
|
||||
$row->app_id = $appId;
|
||||
if ($appSecret !== '') {
|
||||
$row->app_secret = $appSecret;
|
||||
}
|
||||
$row->status = $status ? 1 : 0;
|
||||
$row->remark = $remark ?: null;
|
||||
$row->save();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatApp::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '应用不存在', 404);
|
||||
}
|
||||
$row->delete();
|
||||
return jsonResponse(null, '删除成功');
|
||||
}
|
||||
}
|
||||
|
||||
336
app/admin/controller/WechatMerchantController.php
Normal file
336
app/admin/controller/WechatMerchantController.php
Normal file
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\WechatMerchant;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
class WechatMerchantController
|
||||
{
|
||||
public function list(Request $request)
|
||||
{
|
||||
$page = (int)$request->get('page', 1);
|
||||
$limit = (int)$request->get('limit', 15);
|
||||
if ($page < 1) $page = 1;
|
||||
if ($limit < 1) $limit = 15;
|
||||
|
||||
$query = WechatMerchant::query();
|
||||
|
||||
if (($name = trim((string)$request->get('name', ''))) !== '') {
|
||||
$query->where('name', 'like', "%{$name}%");
|
||||
}
|
||||
if (($mchId = trim((string)$request->get('mch_id', ''))) !== '') {
|
||||
$query->where('mch_id', 'like', "%{$mchId}%");
|
||||
}
|
||||
if ($request->get('status') !== null && $request->get('status') !== '') {
|
||||
$query->where('status', (int)$request->get('status'));
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->select([
|
||||
'id',
|
||||
'name',
|
||||
'mode',
|
||||
'mch_id',
|
||||
'app_id',
|
||||
'sub_mch_id',
|
||||
'sub_app_id',
|
||||
'service_provider',
|
||||
'serial_no',
|
||||
'notify_url',
|
||||
'is_default',
|
||||
'status',
|
||||
'remark',
|
||||
'apiclient_cert_path',
|
||||
'apiclient_key_path',
|
||||
'created_at',
|
||||
])
|
||||
->selectRaw("IF(api_v3_key IS NULL OR api_v3_key = '', 0, 1) as has_api_v3_key")
|
||||
->selectRaw("IF((private_key_pem IS NULL OR private_key_pem = '') AND (apiclient_key_path IS NULL OR apiclient_key_path = ''), 0, 1) as has_private_key")
|
||||
->selectRaw("IF(apiclient_cert_path IS NULL OR apiclient_cert_path = '', 0, 1) as has_apiclient_cert")
|
||||
->orderByDesc('is_default')
|
||||
->orderByDesc('id')
|
||||
->offset(($page - 1) * $limit)
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
return jsonResponse([
|
||||
'total' => $total,
|
||||
'list' => $list,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = trim((string)$request->post('name', ''));
|
||||
$mode = trim((string)$request->post('mode', 'direct'));
|
||||
$mchId = trim((string)$request->post('mch_id', ''));
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$subMchId = trim((string)$request->post('sub_mch_id', ''));
|
||||
$subAppId = trim((string)$request->post('sub_app_id', ''));
|
||||
$serviceProvider = trim((string)$request->post('service_provider', ''));
|
||||
$serialNo = trim((string)$request->post('serial_no', ''));
|
||||
$apiV3Key = trim((string)$request->post('api_v3_key', ''));
|
||||
$privateKeyPem = trim((string)$request->post('private_key_pem', ''));
|
||||
$notifyUrl = trim((string)$request->post('notify_url', ''));
|
||||
$remark = trim((string)$request->post('remark', ''));
|
||||
$status = (int)$request->post('status', 1);
|
||||
$isDefault = (int)$request->post('is_default', 0) ? 1 : 0;
|
||||
|
||||
if ($name === '' || $mchId === '') {
|
||||
return jsonResponse(null, '名称和商户号必填', 400);
|
||||
}
|
||||
if (!in_array($mode, ['direct', 'service_provider', 'third_party'], true)) {
|
||||
return jsonResponse(null, '商户类型不合法', 400);
|
||||
}
|
||||
if ($mode === 'service_provider' && $subMchId === '') {
|
||||
return jsonResponse(null, '服务商模式必须填写子商户号', 400);
|
||||
}
|
||||
|
||||
if ($this->existsConflict(0, $mode, $mchId, $subMchId, $appId, $subAppId)) {
|
||||
return jsonResponse(null, '该商户配置已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
if ($isDefault === 1) {
|
||||
WechatMerchant::where('is_default', 1)->update(['is_default' => 0]);
|
||||
}
|
||||
$row = WechatMerchant::create([
|
||||
'name' => $name,
|
||||
'mode' => $mode,
|
||||
'mch_id' => $mchId,
|
||||
'app_id' => $appId ?: null,
|
||||
'serial_no' => $serialNo ?: null,
|
||||
'api_v3_key' => $apiV3Key ?: null,
|
||||
'private_key_pem' => $privateKeyPem ?: null,
|
||||
'notify_url' => $notifyUrl ?: null,
|
||||
'sub_mch_id' => $subMchId ?: null,
|
||||
'sub_app_id' => $subAppId ?: null,
|
||||
'service_provider' => $serviceProvider ?: null,
|
||||
'remark' => $remark ?: null,
|
||||
'status' => $status ? 1 : 0,
|
||||
'is_default' => $isDefault,
|
||||
]);
|
||||
DB::commit();
|
||||
return jsonResponse($row, '创建成功');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '创建失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
|
||||
$name = trim((string)$request->post('name', $row->name));
|
||||
$mode = trim((string)$request->post('mode', $row->mode));
|
||||
$mchId = trim((string)$request->post('mch_id', $row->mch_id));
|
||||
$appId = trim((string)$request->post('app_id', $row->app_id ?? ''));
|
||||
$subMchId = trim((string)$request->post('sub_mch_id', $row->sub_mch_id ?? ''));
|
||||
$subAppId = trim((string)$request->post('sub_app_id', $row->sub_app_id ?? ''));
|
||||
$serviceProvider = trim((string)$request->post('service_provider', $row->service_provider ?? ''));
|
||||
$serialNo = trim((string)$request->post('serial_no', $row->serial_no ?? ''));
|
||||
$apiV3Key = trim((string)$request->post('api_v3_key', ''));
|
||||
$privateKeyPem = trim((string)$request->post('private_key_pem', ''));
|
||||
$notifyUrl = trim((string)$request->post('notify_url', $row->notify_url ?? ''));
|
||||
$remark = trim((string)$request->post('remark', $row->remark ?? ''));
|
||||
$status = (int)$request->post('status', $row->status);
|
||||
$isDefault = $request->post('is_default') !== null ? ((int)$request->post('is_default') ? 1 : 0) : (int)$row->is_default;
|
||||
|
||||
if ($name === '' || $mchId === '') {
|
||||
return jsonResponse(null, '名称和商户号必填', 400);
|
||||
}
|
||||
if (!in_array($mode, ['direct', 'service_provider', 'third_party'], true)) {
|
||||
return jsonResponse(null, '商户类型不合法', 400);
|
||||
}
|
||||
if ($mode === 'service_provider' && $subMchId === '') {
|
||||
return jsonResponse(null, '服务商模式必须填写子商户号', 400);
|
||||
}
|
||||
|
||||
if ($this->existsConflict($id, $mode, $mchId, $subMchId, $appId, $subAppId)) {
|
||||
return jsonResponse(null, '该商户配置已存在', 400);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
if ($isDefault === 1) {
|
||||
WechatMerchant::where('is_default', 1)->where('id', '<>', $id)->update(['is_default' => 0]);
|
||||
}
|
||||
$row->name = $name;
|
||||
$row->mode = $mode;
|
||||
$row->mch_id = $mchId;
|
||||
$row->app_id = $appId ?: null;
|
||||
$row->serial_no = $serialNo ?: null;
|
||||
if ($apiV3Key !== '') {
|
||||
$row->api_v3_key = $apiV3Key;
|
||||
}
|
||||
if ($privateKeyPem !== '') {
|
||||
$row->private_key_pem = $privateKeyPem;
|
||||
}
|
||||
$row->notify_url = $notifyUrl ?: null;
|
||||
$row->sub_mch_id = $subMchId ?: null;
|
||||
$row->sub_app_id = $subAppId ?: null;
|
||||
$row->service_provider = $serviceProvider ?: null;
|
||||
$row->remark = $remark ?: null;
|
||||
$row->status = $status ? 1 : 0;
|
||||
$row->is_default = $isDefault;
|
||||
$row->save();
|
||||
DB::commit();
|
||||
return jsonResponse(null, '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return jsonResponse(null, '更新失败: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
if ((int)$row->is_default === 1) {
|
||||
return jsonResponse(null, '默认商户号不可删除', 400);
|
||||
}
|
||||
$row->delete();
|
||||
return jsonResponse(null, '删除成功');
|
||||
}
|
||||
|
||||
public function uploadApiclientCert(Request $request)
|
||||
{
|
||||
return $this->uploadPem($request, 'apiclient_cert');
|
||||
}
|
||||
|
||||
public function uploadApiclientKey(Request $request)
|
||||
{
|
||||
return $this->uploadPem($request, 'apiclient_key');
|
||||
}
|
||||
|
||||
public function uploadApiV3Key(Request $request)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
|
||||
/** @var UploadFile|null $file */
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
|
||||
$size = $file->getSize();
|
||||
if ($size === false || $size > 1024) {
|
||||
return jsonResponse(null, '文件过大', 400);
|
||||
}
|
||||
|
||||
$content = file_get_contents($file->getPathname());
|
||||
$content = is_string($content) ? $content : '';
|
||||
$key = preg_replace('/\s+/', '', $content);
|
||||
$key = is_string($key) ? trim($key) : '';
|
||||
if ($key === '' || strlen($key) !== 32) {
|
||||
return jsonResponse(null, 'APIv3 Key 格式不正确(应为32位)', 400);
|
||||
}
|
||||
|
||||
$row->api_v3_key = $key;
|
||||
$row->save();
|
||||
|
||||
return jsonResponse([
|
||||
'id' => $row->id,
|
||||
'has_api_v3_key' => 1,
|
||||
], '上传成功');
|
||||
}
|
||||
|
||||
private function uploadPem(Request $request, string $type)
|
||||
{
|
||||
$id = (int)$request->post('id');
|
||||
$row = WechatMerchant::find($id);
|
||||
if (!$row) {
|
||||
return jsonResponse(null, '商户号不存在', 404);
|
||||
}
|
||||
|
||||
/** @var UploadFile|null $file */
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
$ext = strtolower($file->getUploadExtension());
|
||||
if ($ext !== 'pem') {
|
||||
return jsonResponse(null, '仅支持 pem 文件', 400);
|
||||
}
|
||||
|
||||
$dir = runtime_path() . '/wechatpay/merchants/' . $row->id;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0700, true);
|
||||
}
|
||||
$filename = $type === 'apiclient_cert' ? 'apiclient_cert.pem' : 'apiclient_key.pem';
|
||||
$path = $dir . '/' . $filename;
|
||||
$file->move($path);
|
||||
@chmod($path, 0600);
|
||||
|
||||
if ($type === 'apiclient_cert') {
|
||||
$row->apiclient_cert_path = $path;
|
||||
try {
|
||||
$certPem = file_get_contents($path);
|
||||
$x509 = openssl_x509_read($certPem);
|
||||
if ($x509) {
|
||||
$info = openssl_x509_parse($x509);
|
||||
$serialHex = $info['serialNumberHex'] ?? '';
|
||||
if (is_string($serialHex) && $serialHex !== '') {
|
||||
$row->serial_no = $row->serial_no ?: strtoupper($serialHex);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
} else {
|
||||
$row->apiclient_key_path = $path;
|
||||
}
|
||||
$row->save();
|
||||
|
||||
return jsonResponse([
|
||||
'id' => $row->id,
|
||||
'serial_no' => $row->serial_no,
|
||||
'has_apiclient_cert' => $row->apiclient_cert_path ? 1 : 0,
|
||||
'has_private_key' => ($row->private_key_pem || $row->apiclient_key_path) ? 1 : 0,
|
||||
], '上传成功');
|
||||
}
|
||||
|
||||
private function existsConflict(int $id, string $mode, string $mchId, string $subMchId, string $appId, string $subAppId): bool
|
||||
{
|
||||
$query = WechatMerchant::query()->where('mode', $mode)->where('mch_id', $mchId);
|
||||
if ($mode === 'service_provider') {
|
||||
$query->where('sub_mch_id', $subMchId);
|
||||
} else {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('sub_mch_id')->orWhere('sub_mch_id', '');
|
||||
});
|
||||
}
|
||||
if ($appId !== '') {
|
||||
$query->where('app_id', $appId);
|
||||
} else {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('app_id')->orWhere('app_id', '');
|
||||
});
|
||||
}
|
||||
if ($subAppId !== '') {
|
||||
$query->where('sub_app_id', $subAppId);
|
||||
} else {
|
||||
$query->where(function ($q) {
|
||||
$q->whereNull('sub_app_id')->orWhere('sub_app_id', '');
|
||||
});
|
||||
}
|
||||
if ($id > 0) {
|
||||
$query->where('id', '<>', $id);
|
||||
}
|
||||
return $query->exists();
|
||||
}
|
||||
}
|
||||
35
app/admin/middleware/AuthMiddleware.php
Normal file
35
app/admin/middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace app\admin\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$token = $this->getBearerToken($request);
|
||||
$admin = AuthService::getAdminByToken($token);
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '未登录', 401);
|
||||
}
|
||||
$request->admin = $admin;
|
||||
$request->token = $token;
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
protected function getBearerToken(Request $request): ?string
|
||||
{
|
||||
$authorization = $request->header('authorization');
|
||||
if (!$authorization) {
|
||||
return null;
|
||||
}
|
||||
if (stripos($authorization, 'Bearer ') === 0) {
|
||||
return trim(substr($authorization, 7));
|
||||
}
|
||||
return trim($authorization);
|
||||
}
|
||||
}
|
||||
|
||||
39
app/admin/middleware/PermissionMiddleware.php
Normal file
39
app/admin/middleware/PermissionMiddleware.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace app\admin\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
class PermissionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$admin = $request->admin ?? null;
|
||||
if (!$admin) {
|
||||
return jsonResponse(null, '未登录', 401);
|
||||
}
|
||||
if (intval($admin->is_super) === 1) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
$route = $request->route;
|
||||
$permissionCode = $route ? $route->getName() : null;
|
||||
if (!$permissionCode) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
$admin->loadMissing(['roles.permissions']);
|
||||
$codes = [];
|
||||
foreach ($admin->roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$codes[$permission->code] = true;
|
||||
}
|
||||
}
|
||||
if (!isset($codes[$permissionCode])) {
|
||||
return jsonResponse(null, '无权限', 403);
|
||||
}
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
|
||||
49
app/api/controller/AuthController.php
Normal file
49
app/api/controller/AuthController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
public function login(Request $request)
|
||||
{
|
||||
$mobile = trim((string)$request->post('mobile', ''));
|
||||
$code = trim((string)$request->post('code', ''));
|
||||
if ($mobile === '' || $code === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
if (!preg_match('/^\d{11}$/', $mobile)) {
|
||||
return jsonResponse(null, '手机号格式错误', 400);
|
||||
}
|
||||
|
||||
$user = User::firstOrCreate(
|
||||
['mobile' => $mobile],
|
||||
['nickname' => '用户' . substr($mobile, -4), 'status' => 1]
|
||||
);
|
||||
if (intval($user->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
|
||||
$token = AuthService::issueUserToken($user);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'user' => $user
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function me(Request $request)
|
||||
{
|
||||
return jsonResponse([
|
||||
'user' => $request->user
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
AuthService::revokeUserToken($request->token ?? null);
|
||||
return jsonResponse(null, '已退出登录');
|
||||
}
|
||||
}
|
||||
|
||||
132
app/api/controller/OrderController.php
Normal file
132
app/api/controller/OrderController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\service\OrderFlowService;
|
||||
use app\common\service\PaymentService;
|
||||
use app\common\model\Order;
|
||||
|
||||
class OrderController
|
||||
{
|
||||
public function create(Request $request)
|
||||
{
|
||||
$params = $request->post();
|
||||
$userId = $request->user->id;
|
||||
|
||||
try {
|
||||
$order = OrderFlowService::createOrder($params, $userId);
|
||||
|
||||
return jsonResponse([
|
||||
'order_id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'pay_amount' => $order->total_price
|
||||
], '下单成功');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, '下单失败: ' . $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function list(Request $request)
|
||||
{
|
||||
$status = $request->get('status', 'all');
|
||||
$userId = $request->user->id;
|
||||
|
||||
$query = Order::where('user_id', $userId);
|
||||
if ($status !== 'all') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$orders = $query->orderBy('id', 'desc')->get();
|
||||
|
||||
return jsonResponse(['items' => $orders, 'total' => $orders->count()]);
|
||||
}
|
||||
|
||||
public function detail(Request $request, $id)
|
||||
{
|
||||
$userId = $request->user->id;
|
||||
$order = Order::with(['logs'])->where('id', $id)->where('user_id', $userId)->first();
|
||||
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
$timeline = [];
|
||||
$isFirst = true;
|
||||
foreach ($order->logs as $log) {
|
||||
$timeline[] = [
|
||||
'title' => $log->title,
|
||||
'time' => $log->created_at->format('Y-m-d H:i:s'),
|
||||
'desc' => $log->description,
|
||||
'is_current' => $isFirst,
|
||||
'is_done' => true
|
||||
];
|
||||
$isFirst = false;
|
||||
}
|
||||
|
||||
return jsonResponse([
|
||||
'id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'category' => $order->category,
|
||||
'service_type' => $order->service_type,
|
||||
'status' => $order->status,
|
||||
'is_fast' => (bool)$order->is_fast,
|
||||
'express_company' => $order->express_company,
|
||||
'express_no' => $order->express_no,
|
||||
'timeline' => $timeline
|
||||
]);
|
||||
}
|
||||
|
||||
public function pay(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->post('order_id');
|
||||
$userId = $request->user->id;
|
||||
$payType = trim((string)$request->post('pay_type', 'jsapi'));
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
|
||||
$order = Order::where('id', $orderId)->where('user_id', $userId)->first();
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($payType === 'native') {
|
||||
$pay = PaymentService::createWechatNativePay($order);
|
||||
return jsonResponse($pay, '支付发起成功');
|
||||
}
|
||||
|
||||
if ($appId === '') {
|
||||
return jsonResponse(null, '缺少 app_id,无法发起 JSAPI 支付', 400);
|
||||
}
|
||||
|
||||
$openid = trim((string)$request->post('openid', ''));
|
||||
$pay = PaymentService::createWechatJsapiPay($order, $appId, $openid);
|
||||
return jsonResponse($pay, '支付发起成功');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function ship(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->post('order_id');
|
||||
$expressCompany = trim($request->post('express_company', ''));
|
||||
$expressNo = trim($request->post('express_no', ''));
|
||||
|
||||
if (!$expressCompany || !$expressNo) {
|
||||
return jsonResponse(null, '物流信息不完整', 400);
|
||||
}
|
||||
|
||||
$userId = $request->user->id;
|
||||
$order = Order::where('id', $orderId)->where('user_id', $userId)->first();
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
OrderFlowService::userShip($order, $expressCompany, $expressNo);
|
||||
return jsonResponse(null, '发货信息已提交');
|
||||
} catch (\Exception $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
app/api/controller/PayController.php
Normal file
103
app/api/controller/PayController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\PaymentTransaction;
|
||||
use app\common\model\WechatMerchant;
|
||||
use app\common\service\OrderFlowService;
|
||||
use app\common\service\WechatPayV3Client;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class PayController
|
||||
{
|
||||
public function wechatNotify(Request $request)
|
||||
{
|
||||
$body = (string)$request->rawBody();
|
||||
|
||||
$timestamp = (string)$request->header('Wechatpay-Timestamp', '');
|
||||
$nonce = (string)$request->header('Wechatpay-Nonce', '');
|
||||
$signature = (string)$request->header('Wechatpay-Signature', '');
|
||||
if ($timestamp === '' || $nonce === '' || $signature === '') {
|
||||
return json(['code' => 'FAIL', 'message' => 'missing headers'], 400);
|
||||
}
|
||||
|
||||
$merchants = WechatMerchant::where('status', 1)->get();
|
||||
if ($merchants->count() === 0) {
|
||||
return json(['code' => 'FAIL', 'message' => 'no merchant'], 500);
|
||||
}
|
||||
|
||||
$client = new WechatPayV3Client($merchants->first());
|
||||
$ok = $client->verifyPlatformSignature($timestamp, $nonce, $body, $signature);
|
||||
if (!$ok) {
|
||||
return json(['code' => 'FAIL', 'message' => 'invalid signature'], 400);
|
||||
}
|
||||
|
||||
$payload = json_decode($body, true) ?: [];
|
||||
$resource = $payload['resource'] ?? null;
|
||||
if (!is_array($resource)) {
|
||||
return json(['code' => 'FAIL', 'message' => 'invalid body'], 400);
|
||||
}
|
||||
|
||||
$decrypt = null;
|
||||
$matchedMerchant = null;
|
||||
foreach ($merchants as $m) {
|
||||
$apiV3Key = (string)($m->api_v3_key ?? '');
|
||||
if ($apiV3Key === '') continue;
|
||||
try {
|
||||
$decrypt = $client->decryptNotifyResource($resource, $apiV3Key);
|
||||
$matchedMerchant = $m;
|
||||
break;
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
if (!$decrypt || !$matchedMerchant) {
|
||||
return json(['code' => 'FAIL', 'message' => 'decrypt failed'], 400);
|
||||
}
|
||||
|
||||
$outTradeNo = (string)($decrypt['out_trade_no'] ?? '');
|
||||
$tradeState = (string)($decrypt['trade_state'] ?? '');
|
||||
if ($outTradeNo === '') {
|
||||
return json(['code' => 'FAIL', 'message' => 'missing out_trade_no'], 400);
|
||||
}
|
||||
|
||||
$tx = PaymentTransaction::where('out_trade_no', $outTradeNo)->first();
|
||||
if (!$tx) {
|
||||
return json(['code' => 'SUCCESS', 'message' => 'OK']);
|
||||
}
|
||||
|
||||
if ($tradeState !== 'SUCCESS') {
|
||||
return json(['code' => 'SUCCESS', 'message' => 'OK']);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$tx->status = 'paid';
|
||||
$tx->paid_at = date('Y-m-d H:i:s');
|
||||
$tx->raw_json = $decrypt;
|
||||
$tx->save();
|
||||
|
||||
$order = Order::find($tx->order_id);
|
||||
if ($order) {
|
||||
$order->pay_channel = 'wechat';
|
||||
$order->pay_status = 'paid';
|
||||
$order->pay_merchant_id = (int)$matchedMerchant->id;
|
||||
$order->pay_out_trade_no = $outTradeNo;
|
||||
if (!$order->pay_time) {
|
||||
$order->pay_time = date('Y-m-d H:i:s');
|
||||
}
|
||||
$order->save();
|
||||
|
||||
OrderFlowService::payOrder($order);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return json(['code' => 'FAIL', 'message' => 'server error'], 500);
|
||||
}
|
||||
|
||||
return json(['code' => 'SUCCESS', 'message' => 'OK']);
|
||||
}
|
||||
}
|
||||
|
||||
70
app/api/controller/ReportController.php
Normal file
70
app/api/controller/ReportController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Report;
|
||||
use app\common\model\Order;
|
||||
|
||||
class ReportController
|
||||
{
|
||||
// 获取C端自己的报告
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$orderId = (int)$request->get('order_id');
|
||||
$userId = $request->user->id;
|
||||
|
||||
$order = Order::where('id', $orderId)->where('user_id', $userId)->first();
|
||||
if (!$order) {
|
||||
return jsonResponse(null, '订单不存在', 404);
|
||||
}
|
||||
|
||||
$report = Report::with(['inspector'])->where('order_id', $orderId)->first();
|
||||
if (!$report) {
|
||||
return jsonResponse(null, '报告尚未出具', 404);
|
||||
}
|
||||
|
||||
return jsonResponse([
|
||||
'report_no' => $report->report_no,
|
||||
'conclusion' => $report->conclusion,
|
||||
'level' => $report->level,
|
||||
'flaws' => $report->flaws_json,
|
||||
'images' => $report->images_json,
|
||||
'verify_code' => $report->verify_code,
|
||||
'created_at' => $report->created_at->format('Y-m-d H:i:s'),
|
||||
'inspector' => [
|
||||
'name' => $report->inspector->nickname ?? $report->inspector->username,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// 公开验证防伪码 (无需登录)
|
||||
public function verify(Request $request)
|
||||
{
|
||||
$code = trim($request->get('code', ''));
|
||||
if (!$code) {
|
||||
return jsonResponse(null, '防伪码不能为空', 400);
|
||||
}
|
||||
|
||||
$report = Report::with(['order', 'inspector'])->where('verify_code', $code)->first();
|
||||
if (!$report) {
|
||||
return jsonResponse(null, '无效的防伪码或报告不存在', 404);
|
||||
}
|
||||
|
||||
return jsonResponse([
|
||||
'report_no' => $report->report_no,
|
||||
'conclusion' => $report->conclusion,
|
||||
'level' => $report->level,
|
||||
'flaws' => $report->flaws_json,
|
||||
'images' => $report->images_json,
|
||||
'created_at' => $report->created_at->format('Y-m-d H:i:s'),
|
||||
'order' => [
|
||||
'category' => $report->order->category,
|
||||
'brand' => $report->order->brand,
|
||||
'model' => $report->order->model,
|
||||
],
|
||||
'inspector' => [
|
||||
'name' => $report->inspector->nickname ?? $report->inspector->username,
|
||||
]
|
||||
], '验证成功,该报告真实有效');
|
||||
}
|
||||
}
|
||||
37
app/api/controller/UploadController.php
Normal file
37
app/api/controller/UploadController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use Webman\Http\UploadFile;
|
||||
|
||||
class UploadController
|
||||
{
|
||||
public function image(Request $request)
|
||||
{
|
||||
$file = $request->file('file');
|
||||
if (!$file || !$file->isValid()) {
|
||||
return jsonResponse(null, '未找到文件或文件无效', 400);
|
||||
}
|
||||
|
||||
$ext = strtolower($file->getUploadExtension());
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
return jsonResponse(null, '仅支持图片文件', 400);
|
||||
}
|
||||
|
||||
$dir = public_path() . '/upload/images/' . date('Ymd');
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$filename = uniqid() . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$path = $dir . '/' . $filename;
|
||||
$file->move($path);
|
||||
|
||||
$url = '/upload/images/' . date('Ymd') . '/' . $filename;
|
||||
|
||||
return jsonResponse([
|
||||
'url' => $url,
|
||||
'name' => $file->getUploadName(),
|
||||
], '上传成功');
|
||||
}
|
||||
}
|
||||
49
app/api/controller/UserController.php
Normal file
49
app/api/controller/UserController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\Report;
|
||||
use app\common\model\User;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function stat(Request $request)
|
||||
{
|
||||
$userId = $request->user->id;
|
||||
|
||||
$totalOrders = Order::where('user_id', $userId)->count();
|
||||
$totalReports = Report::whereHas('order', function ($query) use ($userId) {
|
||||
$query->where('user_id', $userId);
|
||||
})->count();
|
||||
|
||||
return jsonResponse([
|
||||
'total_orders' => $totalOrders,
|
||||
'total_reports' => $totalReports
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateInfo(Request $request)
|
||||
{
|
||||
$userId = $request->user->id;
|
||||
$user = User::find($userId);
|
||||
|
||||
if (!$user) {
|
||||
return jsonResponse(null, '用户异常', 404);
|
||||
}
|
||||
|
||||
$nickname = trim($request->post('nickname', ''));
|
||||
$avatar = trim($request->post('avatar', ''));
|
||||
|
||||
if ($nickname) {
|
||||
$user->nickname = $nickname;
|
||||
}
|
||||
if ($avatar) {
|
||||
$user->avatar = $avatar;
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return jsonResponse($user, '更新成功');
|
||||
}
|
||||
}
|
||||
205
app/api/controller/WechatAuthController.php
Normal file
205
app/api/controller/WechatAuthController.php
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
namespace app\api\controller;
|
||||
|
||||
use support\Request;
|
||||
use app\common\model\User;
|
||||
use app\common\model\WechatApp;
|
||||
use app\common\model\UserWechatIdentity;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class WechatAuthController
|
||||
{
|
||||
public function appList(Request $request)
|
||||
{
|
||||
$type = trim((string)$request->get('type', ''));
|
||||
$query = WechatApp::query()->where('status', 1);
|
||||
if ($type !== '') {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
$list = $query->select(['id', 'name', 'type', 'app_id'])->orderByDesc('id')->get();
|
||||
return jsonResponse($list);
|
||||
}
|
||||
|
||||
public function miniLogin(Request $request)
|
||||
{
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$code = trim((string)$request->post('code', ''));
|
||||
if ($appId === '' || $code === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$app = $this->getApp($appId, 'mini');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
$secret = (string)($app->app_secret ?? '');
|
||||
if ($secret === '') {
|
||||
return jsonResponse(null, '未配置 app_secret', 400);
|
||||
}
|
||||
|
||||
$url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' . urlencode($appId) .
|
||||
'&secret=' . urlencode($secret) .
|
||||
'&js_code=' . urlencode($code) .
|
||||
'&grant_type=authorization_code';
|
||||
$res = $this->httpGetJson($url);
|
||||
$openid = isset($res['openid']) ? trim((string)$res['openid']) : '';
|
||||
$unionid = isset($res['unionid']) ? trim((string)$res['unionid']) : '';
|
||||
if ($openid === '') {
|
||||
$msg = $res['errmsg'] ?? '获取 openid 失败';
|
||||
return jsonResponse(null, (string)$msg, 400);
|
||||
}
|
||||
|
||||
$user = $this->resolveUserByWechatIdentity($appId, $openid, $unionid, 'mini');
|
||||
if (intval($user->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
|
||||
$this->upsertIdentity($user->id, $appId, $openid, $unionid, 'mini');
|
||||
$user->openid = $openid;
|
||||
$user->save();
|
||||
|
||||
$token = AuthService::issueUserToken($user);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
'openid' => $openid,
|
||||
'app_id' => $appId,
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function h5Login(Request $request)
|
||||
{
|
||||
$appId = trim((string)$request->post('app_id', ''));
|
||||
$code = trim((string)$request->post('code', ''));
|
||||
if ($appId === '' || $code === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$app = $this->getApp($appId, 'h5');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
$secret = (string)($app->app_secret ?? '');
|
||||
if ($secret === '') {
|
||||
return jsonResponse(null, '未配置 app_secret', 400);
|
||||
}
|
||||
|
||||
$url = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=' . urlencode($appId) .
|
||||
'&secret=' . urlencode($secret) .
|
||||
'&code=' . urlencode($code) .
|
||||
'&grant_type=authorization_code';
|
||||
$res = $this->httpGetJson($url);
|
||||
$openid = isset($res['openid']) ? trim((string)$res['openid']) : '';
|
||||
$unionid = isset($res['unionid']) ? trim((string)$res['unionid']) : '';
|
||||
if ($openid === '') {
|
||||
$msg = $res['errmsg'] ?? '获取 openid 失败';
|
||||
return jsonResponse(null, (string)$msg, 400);
|
||||
}
|
||||
|
||||
$user = $this->resolveUserByWechatIdentity($appId, $openid, $unionid, 'h5');
|
||||
if (intval($user->status) !== 1) {
|
||||
return jsonResponse(null, '账号已禁用', 403);
|
||||
}
|
||||
|
||||
$this->upsertIdentity($user->id, $appId, $openid, $unionid, 'h5');
|
||||
$user->openid = $openid;
|
||||
$user->save();
|
||||
|
||||
$token = AuthService::issueUserToken($user);
|
||||
return jsonResponse([
|
||||
'token' => $token,
|
||||
'user' => $user,
|
||||
'openid' => $openid,
|
||||
'app_id' => $appId,
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
public function h5AuthorizeUrl(Request $request)
|
||||
{
|
||||
$appId = trim((string)$request->get('app_id', ''));
|
||||
$redirectUri = trim((string)$request->get('redirect_uri', ''));
|
||||
$scope = trim((string)$request->get('scope', 'snsapi_base'));
|
||||
$state = trim((string)$request->get('state', ''));
|
||||
|
||||
if ($appId === '' || $redirectUri === '') {
|
||||
return jsonResponse(null, '参数错误', 400);
|
||||
}
|
||||
if (!in_array($scope, ['snsapi_base', 'snsapi_userinfo'], true)) {
|
||||
return jsonResponse(null, 'scope 不合法', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->getApp($appId, 'h5');
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse(null, $e->getMessage(), 400);
|
||||
}
|
||||
|
||||
$url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' . urlencode($appId) .
|
||||
'&redirect_uri=' . urlencode($redirectUri) .
|
||||
'&response_type=code&scope=' . urlencode($scope) .
|
||||
'&state=' . urlencode($state) .
|
||||
'#wechat_redirect';
|
||||
|
||||
return jsonResponse(['url' => $url]);
|
||||
}
|
||||
|
||||
private function getApp(string $appId, string $type): WechatApp
|
||||
{
|
||||
$row = WechatApp::where('app_id', $appId)->where('status', 1)->first();
|
||||
if (!$row) {
|
||||
throw new \RuntimeException('AppID 未配置或已停用');
|
||||
}
|
||||
if ((string)$row->type !== $type) {
|
||||
throw new \RuntimeException('AppID 类型不匹配');
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function httpGetJson(string $url): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$body = curl_exec($ch);
|
||||
if ($body === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new \RuntimeException('微信接口请求失败: ' . $err);
|
||||
}
|
||||
curl_close($ch);
|
||||
return json_decode($body, true) ?: [];
|
||||
}
|
||||
|
||||
private function resolveUserByWechatIdentity(string $appId, string $openid, string $unionid, string $scene): User
|
||||
{
|
||||
if ($unionid !== '') {
|
||||
$identity = UserWechatIdentity::where('unionid', $unionid)->first();
|
||||
if ($identity) {
|
||||
$user = User::find($identity->user_id);
|
||||
if ($user) return $user;
|
||||
}
|
||||
}
|
||||
|
||||
$identity = UserWechatIdentity::where('app_id', $appId)->where('openid', $openid)->first();
|
||||
if ($identity) {
|
||||
$user = User::find($identity->user_id);
|
||||
if ($user) return $user;
|
||||
}
|
||||
|
||||
return User::create([
|
||||
'openid' => $openid,
|
||||
'nickname' => $scene === 'mini' ? '小程序用户' : '微信用户',
|
||||
'status' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function upsertIdentity(int $userId, string $appId, string $openid, string $unionid, string $scene): void
|
||||
{
|
||||
UserWechatIdentity::updateOrCreate(
|
||||
['user_id' => $userId, 'app_id' => $appId],
|
||||
['openid' => $openid, 'unionid' => $unionid ?: null, 'scene' => $scene]
|
||||
);
|
||||
}
|
||||
}
|
||||
35
app/api/middleware/AuthMiddleware.php
Normal file
35
app/api/middleware/AuthMiddleware.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace app\api\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use app\common\service\AuthService;
|
||||
|
||||
class AuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$token = $this->getBearerToken($request);
|
||||
$user = AuthService::getUserByToken($token);
|
||||
if (!$user) {
|
||||
return jsonResponse(null, '未登录', 401);
|
||||
}
|
||||
$request->user = $user;
|
||||
$request->token = $token;
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
protected function getBearerToken(Request $request): ?string
|
||||
{
|
||||
$authorization = $request->header('authorization');
|
||||
if (!$authorization) {
|
||||
return null;
|
||||
}
|
||||
if (stripos($authorization, 'Bearer ') === 0) {
|
||||
return trim(substr($authorization, 7));
|
||||
}
|
||||
return trim($authorization);
|
||||
}
|
||||
}
|
||||
|
||||
24
app/common/exception/BusinessException.php
Normal file
24
app/common/exception/BusinessException.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace app\common\exception;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Exception\ExceptionHandler;
|
||||
|
||||
class BusinessException extends Exception
|
||||
{
|
||||
protected $data;
|
||||
|
||||
public function __construct(string $message = "", int $code = 400, $data = null, Throwable $previous = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
15
app/common/model/AdminToken.php
Normal file
15
app/common/model/AdminToken.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminToken extends Model
|
||||
{
|
||||
protected $table = 'admin_tokens';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['token_hash', 'updated_at'];
|
||||
protected $casts = [
|
||||
'expired_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
17
app/common/model/AdminUser.php
Normal file
17
app/common/model/AdminUser.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminUser extends Model
|
||||
{
|
||||
protected $table = 'admin_users';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['password_hash', 'updated_at'];
|
||||
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'admin_roles', 'admin_id', 'role_id');
|
||||
}
|
||||
}
|
||||
|
||||
20
app/common/model/Order.php
Normal file
20
app/common/model/Order.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
protected $table = 'orders';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
// 隐藏时间戳等不必要字段,保持前端接口整洁
|
||||
protected $hidden = ['updated_at'];
|
||||
|
||||
// 关联订单流转日志 (时间轴)
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(OrderLog::class, 'order_id', 'id')->orderBy('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
11
app/common/model/OrderLog.php
Normal file
11
app/common/model/OrderLog.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OrderLog extends Model
|
||||
{
|
||||
protected $table = 'order_logs';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
15
app/common/model/PaymentTransaction.php
Normal file
15
app/common/model/PaymentTransaction.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PaymentTransaction extends Model
|
||||
{
|
||||
protected $table = 'payment_transactions';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
|
||||
protected $casts = [
|
||||
'raw_json' => 'array',
|
||||
];
|
||||
}
|
||||
12
app/common/model/Permission.php
Normal file
12
app/common/model/Permission.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
protected $table = 'permissions';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
}
|
||||
|
||||
26
app/common/model/Report.php
Normal file
26
app/common/model/Report.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Report extends Model
|
||||
{
|
||||
protected $table = 'reports';
|
||||
protected $guarded = [];
|
||||
|
||||
// Cast JSON fields automatically
|
||||
protected $casts = [
|
||||
'flaws_json' => 'array',
|
||||
'images_json' => 'array',
|
||||
];
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class, 'order_id');
|
||||
}
|
||||
|
||||
public function inspector()
|
||||
{
|
||||
return $this->belongsTo(AdminUser::class, 'inspector_id');
|
||||
}
|
||||
}
|
||||
17
app/common/model/Role.php
Normal file
17
app/common/model/Role.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
protected $table = 'roles';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany(Permission::class, 'role_permissions', 'role_id', 'permission_id');
|
||||
}
|
||||
}
|
||||
|
||||
12
app/common/model/User.php
Normal file
12
app/common/model/User.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
protected $table = 'users';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
}
|
||||
|
||||
15
app/common/model/UserToken.php
Normal file
15
app/common/model/UserToken.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserToken extends Model
|
||||
{
|
||||
protected $table = 'user_tokens';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['token_hash', 'updated_at'];
|
||||
protected $casts = [
|
||||
'expired_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
12
app/common/model/UserWechatIdentity.php
Normal file
12
app/common/model/UserWechatIdentity.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserWechatIdentity extends Model
|
||||
{
|
||||
protected $table = 'user_wechat_identities';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at'];
|
||||
}
|
||||
|
||||
12
app/common/model/WechatApp.php
Normal file
12
app/common/model/WechatApp.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WechatApp extends Model
|
||||
{
|
||||
protected $table = 'wechat_apps';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at', 'app_secret'];
|
||||
}
|
||||
|
||||
11
app/common/model/WechatMerchant.php
Normal file
11
app/common/model/WechatMerchant.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace app\common\model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WechatMerchant extends Model
|
||||
{
|
||||
protected $table = 'wechat_merchants';
|
||||
protected $guarded = [];
|
||||
protected $hidden = ['updated_at', 'api_v3_key', 'private_key_pem', 'apiclient_cert_path', 'apiclient_key_path'];
|
||||
}
|
||||
90
app/common/service/AuthService.php
Normal file
90
app/common/service/AuthService.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserToken;
|
||||
use app\common\model\AdminUser;
|
||||
use app\common\model\AdminToken;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
public static function issueUserToken(User $user): string
|
||||
{
|
||||
$ttl = intval(getenv('USER_TOKEN_TTL') ?: 604800);
|
||||
$token = generateToken();
|
||||
$hash = hashToken($token);
|
||||
|
||||
UserToken::create([
|
||||
'user_id' => $user->id,
|
||||
'token_hash' => $hash,
|
||||
'expired_at' => $ttl > 0 ? Carbon::now()->addSeconds($ttl) : null,
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function getUserByToken(?string $token): ?User
|
||||
{
|
||||
if (!$token) {
|
||||
return null;
|
||||
}
|
||||
$hash = hashToken($token);
|
||||
$row = UserToken::where('token_hash', $hash)->first();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
if ($row->expired_at && $row->expired_at->lt(Carbon::now())) {
|
||||
return null;
|
||||
}
|
||||
return User::find($row->user_id);
|
||||
}
|
||||
|
||||
public static function revokeUserToken(?string $token): void
|
||||
{
|
||||
if (!$token) {
|
||||
return;
|
||||
}
|
||||
UserToken::where('token_hash', hashToken($token))->delete();
|
||||
}
|
||||
|
||||
public static function issueAdminToken(AdminUser $admin): string
|
||||
{
|
||||
$ttl = intval(getenv('ADMIN_TOKEN_TTL') ?: 86400);
|
||||
$token = generateToken();
|
||||
$hash = hashToken($token);
|
||||
|
||||
AdminToken::create([
|
||||
'admin_id' => $admin->id,
|
||||
'token_hash' => $hash,
|
||||
'expired_at' => $ttl > 0 ? Carbon::now()->addSeconds($ttl) : null,
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function getAdminByToken(?string $token): ?AdminUser
|
||||
{
|
||||
if (!$token) {
|
||||
return null;
|
||||
}
|
||||
$hash = hashToken($token);
|
||||
$row = AdminToken::where('token_hash', $hash)->first();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
if ($row->expired_at && $row->expired_at->lt(Carbon::now())) {
|
||||
return null;
|
||||
}
|
||||
return AdminUser::find($row->admin_id);
|
||||
}
|
||||
|
||||
public static function revokeAdminToken(?string $token): void
|
||||
{
|
||||
if (!$token) {
|
||||
return;
|
||||
}
|
||||
AdminToken::where('token_hash', hashToken($token))->delete();
|
||||
}
|
||||
}
|
||||
|
||||
126
app/common/service/OrderFlowService.php
Normal file
126
app/common/service/OrderFlowService.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\Order;
|
||||
use app\common\model\OrderLog;
|
||||
use Exception;
|
||||
|
||||
class OrderFlowService
|
||||
{
|
||||
/**
|
||||
* 模拟创建订单事务
|
||||
*/
|
||||
public static function createOrder(array $params, int $userId)
|
||||
{
|
||||
$orderNo = 'AXY' . date('YmdHis') . rand(100, 999);
|
||||
|
||||
// 1. 创建主订单
|
||||
$order = Order::create([
|
||||
'order_no' => $orderNo,
|
||||
'user_id' => $userId,
|
||||
'category' => $params['category'] ?? '奢品包袋',
|
||||
'service_type' => $params['service_type'] ?? '真伪鉴定',
|
||||
'brand' => $params['brand'] ?? '未知品牌',
|
||||
'model' => $params['model'] ?? '',
|
||||
'remark' => $params['remark'] ?? '',
|
||||
'is_fast' => $params['is_fast'] ?? 0,
|
||||
'total_price' => $params['total_price'] ?? 49.00,
|
||||
'status' => 'wait_pay',
|
||||
]);
|
||||
|
||||
// 2. 写入初始流转日志
|
||||
self::addLog($order->id, 'create', '订单已创建', '等待用户支付', 'user', $userId);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟支付成功
|
||||
*/
|
||||
public static function payOrder(Order $order)
|
||||
{
|
||||
if ($order->status !== 'wait_pay') {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$order->status = 'shipping'; // 待寄送
|
||||
$order->pay_time = date('Y-m-d H:i:s');
|
||||
$order->pay_status = 'paid';
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'pay_success', '支付成功', '请尽快寄出物品', 'user', $order->user_id);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户填写物流并发货
|
||||
*/
|
||||
public static function userShip(Order $order, string $expressCompany, string $expressNo)
|
||||
{
|
||||
if ($order->status !== 'shipping') {
|
||||
throw new Exception("当前状态不允许发货");
|
||||
}
|
||||
|
||||
$order->express_company = $expressCompany;
|
||||
$order->express_no = $expressNo;
|
||||
$order->status = 'wait_receive'; // 等待平台收件(可复用为在途)
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'user_ship', '物品已寄出', "物流公司: {$expressCompany}, 单号: {$expressNo}", 'user', $order->user_id);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台确认收件并开始鉴定
|
||||
*/
|
||||
public static function adminReceive(Order $order, int $adminId)
|
||||
{
|
||||
if ($order->status !== 'wait_receive' && $order->status !== 'shipping') {
|
||||
throw new Exception("当前状态不允许收件");
|
||||
}
|
||||
|
||||
$order->status = 'inspecting';
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'admin_receive', '平台已收件', '物品已入库,即将开始鉴定', 'admin', $adminId);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
public static function adminReturnShip(Order $order, int $adminId, string $expressCompany, string $expressNo)
|
||||
{
|
||||
if ($order->status !== 'finished') {
|
||||
throw new Exception("当前状态不允许回寄");
|
||||
}
|
||||
if ($expressCompany === '' || $expressNo === '') {
|
||||
throw new Exception("回寄物流信息不完整");
|
||||
}
|
||||
|
||||
$order->return_express_company = $expressCompany;
|
||||
$order->return_express_no = $expressNo;
|
||||
$order->return_ship_time = date('Y-m-d H:i:s');
|
||||
$order->status = 'return_shipping';
|
||||
$order->save();
|
||||
|
||||
self::addLog($order->id, 'return_ship', '已回寄', "物流公司: {$expressCompany}, 单号: {$expressNo}", 'admin', $adminId);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入时间轴日志
|
||||
*/
|
||||
public static function addLog(int $orderId, string $actionType, string $title, string $desc = '', string $operatorType = 'system', int $operatorId = 0)
|
||||
{
|
||||
return OrderLog::create([
|
||||
'order_id' => $orderId,
|
||||
'action_type' => $actionType,
|
||||
'title' => $title,
|
||||
'description' => $desc,
|
||||
'operator_type' => $operatorType,
|
||||
'operator_id' => $operatorId
|
||||
]);
|
||||
}
|
||||
}
|
||||
255
app/common/service/PaymentService.php
Normal file
255
app/common/service/PaymentService.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\Order;
|
||||
use app\common\model\PaymentTransaction;
|
||||
use app\common\model\WechatMerchant;
|
||||
use app\common\model\UserWechatIdentity;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
|
||||
class PaymentService
|
||||
{
|
||||
public static function createWechatJsapiPay(Order $order, string $appId, string $openid = ''): array
|
||||
{
|
||||
if ($order->status !== 'wait_pay') {
|
||||
throw new \RuntimeException('订单状态不正确');
|
||||
}
|
||||
|
||||
$merchant = self::selectWechatMerchantForJsapi($order, $appId);
|
||||
if (!$merchant) {
|
||||
throw new \RuntimeException('未配置可用的微信商户号');
|
||||
}
|
||||
|
||||
$notifyUrl = (string)($merchant->notify_url ?? '');
|
||||
if ($notifyUrl === '') {
|
||||
$notifyUrl = (string)(getenv('WECHATPAY_NOTIFY_URL') ?: '');
|
||||
}
|
||||
if ($notifyUrl === '') {
|
||||
throw new \RuntimeException('回调地址未配置');
|
||||
}
|
||||
|
||||
$outTradeNo = self::genOutTradeNo($order);
|
||||
$amountFen = (int)round(((float)$order->total_price) * 100);
|
||||
$description = '安心验-鉴定订单 ' . $order->order_no;
|
||||
|
||||
$openid = trim($openid);
|
||||
if ($openid === '') {
|
||||
$identity = UserWechatIdentity::where('user_id', $order->user_id)->where('app_id', $appId)->first();
|
||||
if ($identity) {
|
||||
$openid = (string)$identity->openid;
|
||||
}
|
||||
}
|
||||
if ($openid === '') {
|
||||
throw new \RuntimeException('缺少 openid,无法发起 JSAPI 支付');
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$tx = PaymentTransaction::create([
|
||||
'order_id' => $order->id,
|
||||
'channel' => 'wechat',
|
||||
'merchant_id' => $merchant->id,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'amount' => $order->total_price,
|
||||
'status' => 'created',
|
||||
]);
|
||||
|
||||
$order->pay_channel = 'wechat';
|
||||
$order->pay_status = 'paying';
|
||||
$order->pay_merchant_id = $merchant->id;
|
||||
$order->pay_out_trade_no = $outTradeNo;
|
||||
$order->save();
|
||||
|
||||
$client = new WechatPayV3Client($merchant);
|
||||
$resp = $client->createJsapiTransaction($outTradeNo, $description, $amountFen, $notifyUrl, $openid);
|
||||
|
||||
$tx->prepay_id = $resp['prepay_id'] ?? null;
|
||||
$tx->raw_json = $resp;
|
||||
$tx->save();
|
||||
|
||||
if (!$tx->prepay_id) {
|
||||
throw new \RuntimeException('微信支付下单失败:缺少 prepay_id');
|
||||
}
|
||||
|
||||
$payParams = $client->buildJsapiPayParams($appId, $tx->prepay_id);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return [
|
||||
'channel' => 'wechat',
|
||||
'pay_type' => 'jsapi',
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'merchant' => [
|
||||
'id' => $merchant->id,
|
||||
'name' => $merchant->name,
|
||||
'mode' => $merchant->mode,
|
||||
'mch_id' => $merchant->mch_id,
|
||||
],
|
||||
'pay_params' => $payParams,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function createWechatNativePay(Order $order): array
|
||||
{
|
||||
if ($order->status !== 'wait_pay') {
|
||||
throw new \RuntimeException('订单状态不正确');
|
||||
}
|
||||
|
||||
$merchant = self::selectWechatMerchant($order);
|
||||
if (!$merchant) {
|
||||
throw new \RuntimeException('未配置可用的微信商户号');
|
||||
}
|
||||
|
||||
$notifyUrl = (string)($merchant->notify_url ?? '');
|
||||
if ($notifyUrl === '') {
|
||||
$notifyUrl = (string)(getenv('WECHATPAY_NOTIFY_URL') ?: '');
|
||||
}
|
||||
if ($notifyUrl === '') {
|
||||
throw new \RuntimeException('回调地址未配置');
|
||||
}
|
||||
|
||||
$outTradeNo = self::genOutTradeNo($order);
|
||||
$amountFen = (int)round(((float)$order->total_price) * 100);
|
||||
$description = '安心验-鉴定订单 ' . $order->order_no;
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$tx = PaymentTransaction::create([
|
||||
'order_id' => $order->id,
|
||||
'channel' => 'wechat',
|
||||
'merchant_id' => $merchant->id,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'amount' => $order->total_price,
|
||||
'status' => 'created',
|
||||
]);
|
||||
|
||||
$order->pay_channel = 'wechat';
|
||||
$order->pay_status = 'paying';
|
||||
$order->pay_merchant_id = $merchant->id;
|
||||
$order->pay_out_trade_no = $outTradeNo;
|
||||
$order->save();
|
||||
|
||||
$client = new WechatPayV3Client($merchant);
|
||||
$resp = $client->createNativeTransaction($outTradeNo, $description, $amountFen, $notifyUrl);
|
||||
|
||||
$tx->prepay_id = $resp['prepay_id'] ?? null;
|
||||
$tx->code_url = $resp['code_url'] ?? null;
|
||||
$tx->raw_json = $resp;
|
||||
$tx->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return [
|
||||
'channel' => 'wechat',
|
||||
'pay_type' => 'native',
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'code_url' => $tx->code_url,
|
||||
'merchant' => [
|
||||
'id' => $merchant->id,
|
||||
'name' => $merchant->name,
|
||||
'mode' => $merchant->mode,
|
||||
'mch_id' => $merchant->mch_id,
|
||||
],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function selectWechatMerchant(Order $order): ?WechatMerchant
|
||||
{
|
||||
$list = WechatMerchant::where('status', 1)
|
||||
->whereIn('mode', ['direct', 'service_provider'])
|
||||
->whereNotNull('serial_no')->where('serial_no', '<>', '')
|
||||
->whereNotNull('api_v3_key')->where('api_v3_key', '<>', '')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereNotNull('private_key_pem')->where('private_key_pem', '<>', '');
|
||||
})->orWhere(function ($q2) {
|
||||
$q2->whereNotNull('apiclient_key_path')->where('apiclient_key_path', '<>', '');
|
||||
});
|
||||
})
|
||||
->orderByDesc('is_default')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
if ($list->count() === 0) return null;
|
||||
if ($list->count() === 1) return $list->first();
|
||||
|
||||
$default = $list->firstWhere('is_default', 1);
|
||||
if ($default) return $default;
|
||||
|
||||
$idx = abs(crc32((string)$order->order_no)) % $list->count();
|
||||
return $list->values()->get($idx);
|
||||
}
|
||||
|
||||
public static function selectWechatMerchantForJsapi(Order $order, string $appId): ?WechatMerchant
|
||||
{
|
||||
$appId = trim($appId);
|
||||
if ($appId === '') {
|
||||
throw new \RuntimeException('缺少 app_id');
|
||||
}
|
||||
|
||||
$list = WechatMerchant::where('status', 1)
|
||||
->whereIn('mode', ['direct', 'service_provider'])
|
||||
->whereNotNull('serial_no')->where('serial_no', '<>', '')
|
||||
->whereNotNull('api_v3_key')->where('api_v3_key', '<>', '')
|
||||
->where(function ($q) {
|
||||
$q->where(function ($q2) {
|
||||
$q2->whereNotNull('private_key_pem')->where('private_key_pem', '<>', '');
|
||||
})->orWhere(function ($q2) {
|
||||
$q2->whereNotNull('apiclient_key_path')->where('apiclient_key_path', '<>', '');
|
||||
});
|
||||
})
|
||||
->get()
|
||||
->filter(function ($m) use ($appId) {
|
||||
$mode = (string)($m->mode ?? '');
|
||||
if ($mode === 'direct') {
|
||||
return (string)($m->app_id ?? '') === $appId;
|
||||
}
|
||||
if ($mode === 'service_provider') {
|
||||
$subAppId = (string)($m->sub_app_id ?? '');
|
||||
if ($subAppId !== '') return $subAppId === $appId;
|
||||
return (string)($m->app_id ?? '') === $appId;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
->values();
|
||||
|
||||
if ($list->count() === 0) return null;
|
||||
if ($list->count() === 1) return $list->first();
|
||||
|
||||
$default = $list->firstWhere('is_default', 1);
|
||||
if ($default) return $default;
|
||||
|
||||
$idx = abs(crc32((string)$order->order_no)) % $list->count();
|
||||
return $list->get($idx);
|
||||
}
|
||||
|
||||
private static function genOutTradeNo(Order $order): string
|
||||
{
|
||||
return 'AXY' . date('YmdHis') . $order->id . random_int(1000, 9999);
|
||||
}
|
||||
|
||||
private static function resolveJsapiAppId(WechatMerchant $merchant): string
|
||||
{
|
||||
$mode = (string)($merchant->mode ?? 'direct');
|
||||
$appId = (string)($merchant->app_id ?? '');
|
||||
$subAppId = (string)($merchant->sub_app_id ?? '');
|
||||
|
||||
if ($mode === 'direct') {
|
||||
if ($appId === '') throw new \RuntimeException('商户 AppID 未配置');
|
||||
return $appId;
|
||||
}
|
||||
if ($mode === 'service_provider') {
|
||||
if ($subAppId !== '') return $subAppId;
|
||||
if ($appId === '') throw new \RuntimeException('服务商 AppID 未配置');
|
||||
return $appId;
|
||||
}
|
||||
throw new \RuntimeException('该商户类型暂不支持 JSAPI');
|
||||
}
|
||||
}
|
||||
276
app/common/service/WechatPayV3Client.php
Normal file
276
app/common/service/WechatPayV3Client.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\model\WechatMerchant;
|
||||
|
||||
class WechatPayV3Client
|
||||
{
|
||||
private WechatMerchant $merchant;
|
||||
|
||||
public function __construct(WechatMerchant $merchant)
|
||||
{
|
||||
$this->merchant = $merchant;
|
||||
}
|
||||
|
||||
public function createNativeTransaction(string $outTradeNo, string $description, int $amountFen, string $notifyUrl): array
|
||||
{
|
||||
$body = $this->buildPayBody($outTradeNo, $description, $amountFen, $notifyUrl);
|
||||
$resp = $this->request('POST', '/v3/pay/transactions/native', $body);
|
||||
|
||||
$data = json_decode($resp['body'], true) ?: [];
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300) {
|
||||
return $data;
|
||||
}
|
||||
$message = $data['message'] ?? ('微信支付下单失败 HTTP ' . $resp['status']);
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
public function createJsapiTransaction(string $outTradeNo, string $description, int $amountFen, string $notifyUrl, string $openid): array
|
||||
{
|
||||
$body = $this->buildJsapiPayBody($outTradeNo, $description, $amountFen, $notifyUrl, $openid);
|
||||
$resp = $this->request('POST', '/v3/pay/transactions/jsapi', $body);
|
||||
|
||||
$data = json_decode($resp['body'], true) ?: [];
|
||||
if ($resp['status'] >= 200 && $resp['status'] < 300) {
|
||||
return $data;
|
||||
}
|
||||
$message = $data['message'] ?? ('微信支付下单失败 HTTP ' . $resp['status']);
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
public function buildJsapiPayParams(string $appId, string $prepayId): array
|
||||
{
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$serialNo = (string)($this->merchant->serial_no ?? '');
|
||||
$privateKeyPem = (string)($this->merchant->private_key_pem ?? '');
|
||||
if ($privateKeyPem === '') {
|
||||
$privateKeyPem = $this->loadKeyPemFromFile();
|
||||
}
|
||||
if ($mchId === '' || $serialNo === '' || $privateKeyPem === '') {
|
||||
throw new \RuntimeException('微信支付密钥未配置(mch_id/serial_no/private_key_pem)');
|
||||
}
|
||||
|
||||
$timeStamp = (string)time();
|
||||
$nonceStr = bin2hex(random_bytes(16));
|
||||
$package = 'prepay_id=' . $prepayId;
|
||||
|
||||
$signStr = $appId . "\n" . $timeStamp . "\n" . $nonceStr . "\n" . $package . "\n";
|
||||
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
||||
if (!$privateKey) {
|
||||
throw new \RuntimeException('私钥格式错误');
|
||||
}
|
||||
openssl_sign($signStr, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
|
||||
return [
|
||||
'appId' => $appId,
|
||||
'timeStamp' => $timeStamp,
|
||||
'nonceStr' => $nonceStr,
|
||||
'package' => $package,
|
||||
'signType' => 'RSA',
|
||||
'paySign' => base64_encode($signature),
|
||||
];
|
||||
}
|
||||
|
||||
public function verifyPlatformSignature(string $timestamp, string $nonce, string $body, string $signature): bool
|
||||
{
|
||||
$certPath = getenv('WECHATPAY_PLATFORM_CERT_PATH') ?: '';
|
||||
if ($certPath === '' || !file_exists($certPath)) {
|
||||
throw new \RuntimeException('平台证书未配置');
|
||||
}
|
||||
$cert = file_get_contents($certPath);
|
||||
$publicKey = openssl_pkey_get_public($cert);
|
||||
if (!$publicKey) {
|
||||
throw new \RuntimeException('平台证书读取失败');
|
||||
}
|
||||
$message = $timestamp . "\n" . $nonce . "\n" . $body . "\n";
|
||||
$ok = openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256);
|
||||
return $ok === 1;
|
||||
}
|
||||
|
||||
public function decryptNotifyResource(array $resource, string $apiV3Key): array
|
||||
{
|
||||
$ciphertext = (string)($resource['ciphertext'] ?? '');
|
||||
$nonce = (string)($resource['nonce'] ?? '');
|
||||
$aad = (string)($resource['associated_data'] ?? '');
|
||||
if ($ciphertext === '' || $nonce === '') {
|
||||
throw new \RuntimeException('回调报文不完整');
|
||||
}
|
||||
|
||||
$cipherRaw = base64_decode($ciphertext);
|
||||
$tag = substr($cipherRaw, -16);
|
||||
$data = substr($cipherRaw, 0, -16);
|
||||
$plain = openssl_decrypt($data, 'aes-256-gcm', $apiV3Key, OPENSSL_RAW_DATA, $nonce, $tag, $aad);
|
||||
if ($plain === false) {
|
||||
throw new \RuntimeException('回调报文解密失败');
|
||||
}
|
||||
return json_decode($plain, true) ?: [];
|
||||
}
|
||||
|
||||
private function buildPayBody(string $outTradeNo, string $description, int $amountFen, string $notifyUrl): array
|
||||
{
|
||||
$mode = (string)($this->merchant->mode ?? 'direct');
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$appId = (string)($this->merchant->app_id ?? '');
|
||||
$subMchId = (string)($this->merchant->sub_mch_id ?? '');
|
||||
$subAppId = (string)($this->merchant->sub_app_id ?? '');
|
||||
|
||||
if ($mode === 'direct') {
|
||||
if ($appId === '') {
|
||||
throw new \RuntimeException('商户 AppID 未配置');
|
||||
}
|
||||
return [
|
||||
'appid' => $appId,
|
||||
'mchid' => $mchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
];
|
||||
}
|
||||
if ($mode === 'service_provider') {
|
||||
if ($appId === '' || $subMchId === '') {
|
||||
throw new \RuntimeException('服务商模式配置不完整');
|
||||
}
|
||||
$body = [
|
||||
'sp_appid' => $appId,
|
||||
'sp_mchid' => $mchId,
|
||||
'sub_mchid' => $subMchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
];
|
||||
if ($subAppId !== '') {
|
||||
$body['sub_appid'] = $subAppId;
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
throw new \RuntimeException('第三方支付模式未配置下单方式');
|
||||
}
|
||||
|
||||
private function buildJsapiPayBody(string $outTradeNo, string $description, int $amountFen, string $notifyUrl, string $openid): array
|
||||
{
|
||||
$openid = trim($openid);
|
||||
if ($openid === '') {
|
||||
throw new \RuntimeException('openid 不能为空');
|
||||
}
|
||||
|
||||
$mode = (string)($this->merchant->mode ?? 'direct');
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$appId = (string)($this->merchant->app_id ?? '');
|
||||
$subMchId = (string)($this->merchant->sub_mch_id ?? '');
|
||||
$subAppId = (string)($this->merchant->sub_app_id ?? '');
|
||||
|
||||
if ($mode === 'direct') {
|
||||
if ($appId === '') {
|
||||
throw new \RuntimeException('商户 AppID 未配置');
|
||||
}
|
||||
return [
|
||||
'appid' => $appId,
|
||||
'mchid' => $mchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
'payer' => ['openid' => $openid],
|
||||
];
|
||||
}
|
||||
|
||||
if ($mode === 'service_provider') {
|
||||
if ($appId === '' || $subMchId === '') {
|
||||
throw new \RuntimeException('服务商模式配置不完整');
|
||||
}
|
||||
$body = [
|
||||
'sp_appid' => $appId,
|
||||
'sp_mchid' => $mchId,
|
||||
'sub_mchid' => $subMchId,
|
||||
'description' => $description,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => ['total' => $amountFen, 'currency' => 'CNY'],
|
||||
'payer' => [],
|
||||
];
|
||||
if ($subAppId !== '') {
|
||||
$body['sub_appid'] = $subAppId;
|
||||
$body['payer']['sub_openid'] = $openid;
|
||||
} else {
|
||||
$body['payer']['sp_openid'] = $openid;
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
throw new \RuntimeException('第三方支付模式未配置下单方式');
|
||||
}
|
||||
|
||||
private function request(string $method, string $path, array $body): array
|
||||
{
|
||||
$mchId = (string)$this->merchant->mch_id;
|
||||
$serialNo = (string)($this->merchant->serial_no ?? '');
|
||||
$privateKeyPem = (string)($this->merchant->private_key_pem ?? '');
|
||||
if ($privateKeyPem === '') {
|
||||
$privateKeyPem = $this->loadKeyPemFromFile();
|
||||
}
|
||||
|
||||
if ($mchId === '' || $serialNo === '' || $privateKeyPem === '') {
|
||||
throw new \RuntimeException('微信支付密钥未配置(mch_id/serial_no/private_key_pem)');
|
||||
}
|
||||
|
||||
$bodyJson = json_encode($body, JSON_UNESCAPED_UNICODE);
|
||||
$timestamp = (string)time();
|
||||
$nonceStr = bin2hex(random_bytes(16));
|
||||
|
||||
$signStr = $method . "\n" . $path . "\n" . $timestamp . "\n" . $nonceStr . "\n" . $bodyJson . "\n";
|
||||
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
||||
if (!$privateKey) {
|
||||
throw new \RuntimeException('私钥格式错误');
|
||||
}
|
||||
openssl_sign($signStr, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
$signature = base64_encode($signature);
|
||||
|
||||
$authorization = sprintf(
|
||||
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"',
|
||||
$mchId,
|
||||
$nonceStr,
|
||||
$timestamp,
|
||||
$serialNo,
|
||||
$signature
|
||||
);
|
||||
|
||||
$ch = curl_init('https://api.mch.weixin.qq.com' . $path);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/json',
|
||||
'Authorization: ' . $authorization,
|
||||
'User-Agent: anxinyan-webman',
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyJson);
|
||||
$respBody = curl_exec($ch);
|
||||
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($respBody === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new \RuntimeException('微信支付请求失败: ' . $err);
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'body' => $respBody,
|
||||
];
|
||||
}
|
||||
|
||||
private function loadKeyPemFromFile(): string
|
||||
{
|
||||
$path = (string)($this->merchant->apiclient_key_path ?? '');
|
||||
if ($path === '') return '';
|
||||
$real = realpath($path);
|
||||
$base = realpath(runtime_path() . '/wechatpay/merchants');
|
||||
if (!$real || !$base) return '';
|
||||
if (strpos($real, $base) !== 0) return '';
|
||||
if (!is_file($real)) return '';
|
||||
$pem = file_get_contents($real);
|
||||
return is_string($pem) ? $pem : '';
|
||||
}
|
||||
}
|
||||
42
app/controller/IndexController.php
Normal file
42
app/controller/IndexController.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
return <<<EOF
|
||||
<style>
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
<iframe
|
||||
src="https://www.workerman.net/wellcome"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allow="clipboard-write"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-downloads"
|
||||
></iframe>
|
||||
EOF;
|
||||
}
|
||||
|
||||
public function view(Request $request)
|
||||
{
|
||||
return view('index/view', ['name' => 'webman']);
|
||||
}
|
||||
|
||||
public function json(Request $request)
|
||||
{
|
||||
return json(['code' => 0, 'msg' => 'ok']);
|
||||
}
|
||||
|
||||
}
|
||||
4
app/functions.php
Normal file
4
app/functions.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
46
app/middleware/Cors.php
Normal file
46
app/middleware/Cors.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
|
||||
class Cors implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$origin = (string)$request->header('origin', '');
|
||||
$allow = trim((string)(getenv('CORS_ALLOW_ORIGINS') ?: '*'));
|
||||
$allowOrigin = '';
|
||||
|
||||
if ($allow === '*') {
|
||||
$allowOrigin = '*';
|
||||
} else {
|
||||
$allowList = array_values(array_filter(array_map('trim', explode(',', $allow))));
|
||||
if ($origin !== '' && in_array($origin, $allowList, true)) {
|
||||
$allowOrigin = $origin;
|
||||
}
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Access-Control-Allow-Methods' => 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With',
|
||||
'Access-Control-Max-Age' => '86400',
|
||||
];
|
||||
|
||||
if ($allowOrigin !== '') {
|
||||
$headers['Access-Control-Allow-Origin'] = $allowOrigin;
|
||||
if ($allowOrigin !== '*') {
|
||||
$headers['Access-Control-Allow-Credentials'] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
if (strtoupper($request->method()) === 'OPTIONS') {
|
||||
return response('', 204)->withHeaders($headers);
|
||||
}
|
||||
|
||||
$response = $handler($request);
|
||||
return $response->withHeaders($headers);
|
||||
}
|
||||
}
|
||||
|
||||
42
app/middleware/StaticFile.php
Normal file
42
app/middleware/StaticFile.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* Class StaticFile
|
||||
* @package app\middleware
|
||||
*/
|
||||
class StaticFile implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
// Access to files beginning with. Is prohibited
|
||||
if (strpos($request->path(), '/.') !== false) {
|
||||
return response('<h1>403 forbidden</h1>', 403);
|
||||
}
|
||||
/** @var Response $response */
|
||||
$response = $handler($request);
|
||||
// Add cross domain HTTP header
|
||||
/*$response->withHeaders([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
]);*/
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
29
app/model/Test.php
Normal file
29
app/model/Test.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Test extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'test';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
}
|
||||
10
app/process/Http.php
Normal file
10
app/process/Http.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use Webman\App;
|
||||
|
||||
class Http extends App
|
||||
{
|
||||
|
||||
}
|
||||
305
app/process/Monitor.php
Normal file
305
app/process/Monitor.php
Normal file
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class FileMonitor
|
||||
* @package process
|
||||
*/
|
||||
class Monitor
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $paths = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $extensions = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $loadedFiles = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $ppid = 0;
|
||||
|
||||
/**
|
||||
* Pause monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function pause(): void
|
||||
{
|
||||
file_put_contents(static::lockFile(), time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function resume(): void
|
||||
{
|
||||
clearstatcache();
|
||||
if (is_file(static::lockFile())) {
|
||||
unlink(static::lockFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether monitor is paused
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPaused(): bool
|
||||
{
|
||||
clearstatcache();
|
||||
return file_exists(static::lockFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock file
|
||||
* @return string
|
||||
*/
|
||||
protected static function lockFile(): string
|
||||
{
|
||||
return runtime_path('monitor.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* FileMonitor constructor.
|
||||
* @param $monitorDir
|
||||
* @param $monitorExtensions
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($monitorDir, $monitorExtensions, array $options = [])
|
||||
{
|
||||
$this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
|
||||
static::resume();
|
||||
$this->paths = (array)$monitorDir;
|
||||
$this->extensions = $monitorExtensions;
|
||||
foreach (get_included_files() as $index => $file) {
|
||||
$this->loadedFiles[$file] = $index;
|
||||
if (strpos($file, 'webman-framework/src/support/App.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Worker::getAllWorkers()) {
|
||||
return;
|
||||
}
|
||||
$disableFunctions = explode(',', ini_get('disable_functions'));
|
||||
if (in_array('exec', $disableFunctions, true)) {
|
||||
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
|
||||
} else {
|
||||
if ($options['enable_file_monitor'] ?? true) {
|
||||
Timer::add(1, function () {
|
||||
$this->checkAllFilesChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
|
||||
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
|
||||
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $monitorDir
|
||||
* @return bool
|
||||
*/
|
||||
public function checkFilesChange($monitorDir): bool
|
||||
{
|
||||
static $lastMtime, $tooManyFilesCheck;
|
||||
if (!$lastMtime) {
|
||||
$lastMtime = time();
|
||||
}
|
||||
clearstatcache();
|
||||
if (!is_dir($monitorDir)) {
|
||||
if (!is_file($monitorDir)) {
|
||||
return false;
|
||||
}
|
||||
$iterator = [new SplFileInfo($monitorDir)];
|
||||
} else {
|
||||
// recursive traversal directory
|
||||
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
|
||||
$iterator = new RecursiveIteratorIterator($dirIterator);
|
||||
}
|
||||
$count = 0;
|
||||
foreach ($iterator as $file) {
|
||||
$count ++;
|
||||
/** @var SplFileInfo $file */
|
||||
if (is_dir($file->getRealPath())) {
|
||||
continue;
|
||||
}
|
||||
// check mtime
|
||||
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
|
||||
$lastMtime = $file->getMTime();
|
||||
if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
|
||||
echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
|
||||
continue;
|
||||
}
|
||||
$var = 0;
|
||||
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
|
||||
if ($var) {
|
||||
continue;
|
||||
}
|
||||
// send SIGUSR1 signal to master process for reload
|
||||
if (DIRECTORY_SEPARATOR === '/') {
|
||||
if ($masterPid = $this->getMasterPid()) {
|
||||
echo $file . " updated and reload\n";
|
||||
posix_kill($masterPid, SIGUSR1);
|
||||
} else {
|
||||
echo "Master process has gone away and can not reload\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
echo $file . " updated and reload\n";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!$tooManyFilesCheck && $count > 1000) {
|
||||
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
|
||||
$tooManyFilesCheck = 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMasterPid(): int
|
||||
{
|
||||
if ($this->ppid === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
|
||||
echo "Master process has gone away\n";
|
||||
return $this->ppid = 0;
|
||||
}
|
||||
if (PHP_OS_FAMILY !== 'Linux') {
|
||||
return $this->ppid;
|
||||
}
|
||||
$cmdline = "/proc/$this->ppid/cmdline";
|
||||
if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
|
||||
// Process not exist
|
||||
$this->ppid = 0;
|
||||
}
|
||||
return $this->ppid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function checkAllFilesChange(): bool
|
||||
{
|
||||
if (static::isPaused()) {
|
||||
return false;
|
||||
}
|
||||
foreach ($this->paths as $path) {
|
||||
if ($this->checkFilesChange($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $memoryLimit
|
||||
* @return void
|
||||
*/
|
||||
public function checkMemory($memoryLimit): void
|
||||
{
|
||||
if (static::isPaused() || $memoryLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
$masterPid = $this->getMasterPid();
|
||||
if ($masterPid <= 0) {
|
||||
echo "Master process has gone away\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$childrenFile = "/proc/$masterPid/task/$masterPid/children";
|
||||
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
|
||||
return;
|
||||
}
|
||||
foreach (explode(' ', $children) as $pid) {
|
||||
$pid = (int)$pid;
|
||||
$statusFile = "/proc/$pid/status";
|
||||
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
|
||||
continue;
|
||||
}
|
||||
$mem = 0;
|
||||
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
|
||||
$mem = $match[1];
|
||||
}
|
||||
$mem = (int)($mem / 1024);
|
||||
if ($mem >= $memoryLimit) {
|
||||
posix_kill($pid, SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory limit
|
||||
* @param $memoryLimit
|
||||
* @return int
|
||||
*/
|
||||
protected function getMemoryLimit($memoryLimit): int
|
||||
{
|
||||
if ($memoryLimit === 0) {
|
||||
return 0;
|
||||
}
|
||||
$usePhpIni = false;
|
||||
if (!$memoryLimit) {
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
$usePhpIni = true;
|
||||
}
|
||||
|
||||
if ($memoryLimit == -1) {
|
||||
return 0;
|
||||
}
|
||||
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
|
||||
$memoryLimit = (int)$memoryLimit;
|
||||
if ($unit === 'g') {
|
||||
$memoryLimit = 1024 * $memoryLimit;
|
||||
} else if ($unit === 'k') {
|
||||
$memoryLimit = ($memoryLimit / 1024);
|
||||
} else if ($unit === 'm') {
|
||||
$memoryLimit = (int)($memoryLimit);
|
||||
} else if ($unit === 't') {
|
||||
$memoryLimit = (1024 * 1024 * $memoryLimit);
|
||||
} else {
|
||||
$memoryLimit = ($memoryLimit / (1024 * 1024));
|
||||
}
|
||||
if ($memoryLimit < 50) {
|
||||
$memoryLimit = 50;
|
||||
}
|
||||
if ($usePhpIni) {
|
||||
$memoryLimit = (0.8 * $memoryLimit);
|
||||
}
|
||||
return (int)$memoryLimit;
|
||||
}
|
||||
|
||||
}
|
||||
14
app/view/index/view.html
Normal file
14
app/view/index/view.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/favicon.ico"/>
|
||||
<title>webman</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
hello <?=htmlspecialchars($name)?>
|
||||
</body>
|
||||
</html>
|
||||
70
composer.json
Normal file
70
composer.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "workerman/webman",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"high performance",
|
||||
"http service"
|
||||
],
|
||||
"homepage": "https://www.workerman.net",
|
||||
"license": "MIT",
|
||||
"description": "High performance HTTP Service Framework.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "walkor",
|
||||
"email": "walkor@workerman.net",
|
||||
"homepage": "https://www.workerman.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"email": "walkor@workerman.net",
|
||||
"issues": "https://github.com/walkor/webman/issues",
|
||||
"forum": "https://wenda.workerman.net/",
|
||||
"wiki": "https://workerman.net/doc/webman",
|
||||
"source": "https://github.com/walkor/webman"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"workerman/webman-framework": "^2.1",
|
||||
"monolog/monolog": "^2.0",
|
||||
"illuminate/database": "^10.49",
|
||||
"illuminate/pagination": "^10.49",
|
||||
"illuminate/events": "^10.49",
|
||||
"webman/redis-queue": "^2.1",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"": "./",
|
||||
"app\\": "./app",
|
||||
"app\\common\\": "./app/common",
|
||||
"app\\api\\": "./app/api",
|
||||
"app\\admin\\": "./app/admin"
|
||||
},
|
||||
"files": [
|
||||
"./support/helpers.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"post-package-install": [
|
||||
"support\\Plugin::install"
|
||||
],
|
||||
"post-package-update": [
|
||||
"support\\Plugin::install"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"support\\Plugin::uninstall"
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"support\\Setup::run"
|
||||
],
|
||||
"setup-webman": [
|
||||
"support\\Setup::run"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
1934
composer.lock
generated
Normal file
1934
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
config/app.php
Normal file
26
config/app.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\Request;
|
||||
|
||||
return [
|
||||
'debug' => true,
|
||||
'error_reporting' => E_ALL,
|
||||
'default_timezone' => 'Asia/Shanghai',
|
||||
'request_class' => Request::class,
|
||||
'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
|
||||
'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
|
||||
'controller_suffix' => 'Controller',
|
||||
'controller_reuse' => false,
|
||||
];
|
||||
21
config/autoload.php
Normal file
21
config/autoload.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'files' => [
|
||||
base_path() . '/app/functions.php',
|
||||
base_path() . '/support/Request.php',
|
||||
base_path() . '/support/Response.php',
|
||||
]
|
||||
];
|
||||
18
config/bootstrap.php
Normal file
18
config/bootstrap.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
support\bootstrap\Database::class,
|
||||
support\bootstrap\Session::class,
|
||||
];
|
||||
15
config/container.php
Normal file
15
config/container.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return new Webman\Container;
|
||||
23
config/database.php
Normal file
23
config/database.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => 'mysql',
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => getenv('DB_HOST') ?: '127.0.0.1',
|
||||
'port' => getenv('DB_PORT') ?: '3306',
|
||||
'database' => getenv('DB_DATABASE') ?: '',
|
||||
'username' => getenv('DB_USERNAME') ?: '',
|
||||
'password' => getenv('DB_PASSWORD') ?: '',
|
||||
'unix_socket' => '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => [
|
||||
\PDO::ATTR_TIMEOUT => 3
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
15
config/dependence.php
Normal file
15
config/dependence.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [];
|
||||
4
config/exception.php
Normal file
4
config/exception.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'' => support\ExceptionHandler::class,
|
||||
];
|
||||
32
config/log.php
Normal file
32
config/log.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'handlers' => [
|
||||
[
|
||||
'class' => Monolog\Handler\RotatingFileHandler::class,
|
||||
'constructor' => [
|
||||
runtime_path() . '/logs/webman.log',
|
||||
7, //$maxFiles
|
||||
Monolog\Logger::DEBUG,
|
||||
],
|
||||
'formatter' => [
|
||||
'class' => Monolog\Formatter\LineFormatter::class,
|
||||
'constructor' => [null, 'Y-m-d H:i:s', true],
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
19
config/middleware.php
Normal file
19
config/middleware.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'@' => [
|
||||
app\middleware\Cors::class,
|
||||
],
|
||||
];
|
||||
4
config/plugin/webman/redis-queue/app.php
Normal file
4
config/plugin/webman/redis-queue/app.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
];
|
||||
7
config/plugin/webman/redis-queue/command.php
Normal file
7
config/plugin/webman/redis-queue/command.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\RedisQueue\Command\MakeConsumerCommand;
|
||||
|
||||
return [
|
||||
MakeConsumerCommand::class
|
||||
];
|
||||
32
config/plugin/webman/redis-queue/log.php
Normal file
32
config/plugin/webman/redis-queue/log.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'handlers' => [
|
||||
[
|
||||
'class' => Monolog\Handler\RotatingFileHandler::class,
|
||||
'constructor' => [
|
||||
runtime_path() . '/logs/redis-queue/queue.log',
|
||||
7, //$maxFiles
|
||||
Monolog\Logger::DEBUG,
|
||||
],
|
||||
'formatter' => [
|
||||
'class' => Monolog\Formatter\LineFormatter::class,
|
||||
'constructor' => [null, 'Y-m-d H:i:s', true],
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
11
config/plugin/webman/redis-queue/process.php
Normal file
11
config/plugin/webman/redis-queue/process.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
'consumer' => [
|
||||
'handler' => Webman\RedisQueue\Process\Consumer::class,
|
||||
'count' => 8, // 可以设置多进程同时消费
|
||||
'constructor' => [
|
||||
// 消费者类目录
|
||||
'consumer_dir' => app_path() . '/queue/redis'
|
||||
]
|
||||
]
|
||||
];
|
||||
21
config/plugin/webman/redis-queue/redis.php
Normal file
21
config/plugin/webman/redis-queue/redis.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => [
|
||||
'host' => 'redis://127.0.0.1:6379',
|
||||
'options' => [
|
||||
'auth' => null,
|
||||
'db' => 0,
|
||||
'prefix' => '',
|
||||
'max_attempts' => 5,
|
||||
'retry_seconds' => 5,
|
||||
],
|
||||
// Connection pool, supports only Swoole or Swow drivers.
|
||||
'pool' => [
|
||||
'max_connections' => 5,
|
||||
'min_connections' => 1,
|
||||
'wait_timeout' => 3,
|
||||
'idle_timeout' => 60,
|
||||
'heartbeat_interval' => 50,
|
||||
]
|
||||
],
|
||||
];
|
||||
62
config/process.php
Normal file
62
config/process.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
use app\process\Http;
|
||||
|
||||
global $argv;
|
||||
|
||||
return [
|
||||
'webman' => [
|
||||
'handler' => Http::class,
|
||||
'listen' => 'http://0.0.0.0:8787',
|
||||
'count' => 4,
|
||||
'user' => '',
|
||||
'group' => '',
|
||||
'reusePort' => false,
|
||||
'eventLoop' => '',
|
||||
'context' => [],
|
||||
'constructor' => [
|
||||
'requestClass' => Request::class,
|
||||
'logger' => Log::channel('default'),
|
||||
'appPath' => app_path(),
|
||||
'publicPath' => public_path()
|
||||
]
|
||||
],
|
||||
// File update detection and automatic reload
|
||||
'monitor' => [
|
||||
'handler' => app\process\Monitor::class,
|
||||
'reloadable' => false,
|
||||
'constructor' => [
|
||||
// Monitor these directories
|
||||
'monitorDir' => array_merge([
|
||||
app_path(),
|
||||
config_path(),
|
||||
base_path() . '/process',
|
||||
base_path() . '/support',
|
||||
base_path() . '/resource',
|
||||
base_path() . '/.env',
|
||||
], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
|
||||
// Files with these suffixes will be monitored
|
||||
'monitorExtensions' => [
|
||||
'php', 'html', 'htm', 'env'
|
||||
],
|
||||
'options' => [
|
||||
'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
|
||||
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
109
config/route.php
Normal file
109
config/route.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
use Webman\Route;
|
||||
|
||||
Route::group('/api', function () {
|
||||
Route::post('/auth/login', [app\api\controller\AuthController::class, 'login'])->name('api.auth.login');
|
||||
Route::get('/wechat/app_list', [app\api\controller\WechatAuthController::class, 'appList'])->name('api.wechat.app_list');
|
||||
Route::post('/wechat/mini_login', [app\api\controller\WechatAuthController::class, 'miniLogin'])->name('api.wechat.mini_login');
|
||||
Route::post('/wechat/h5_login', [app\api\controller\WechatAuthController::class, 'h5Login'])->name('api.wechat.h5_login');
|
||||
Route::get('/wechat/h5_authorize_url', [app\api\controller\WechatAuthController::class, 'h5AuthorizeUrl'])->name('api.wechat.h5_authorize_url');
|
||||
|
||||
// 公开验证防伪接口
|
||||
Route::get('/report/verify', [app\api\controller\ReportController::class, 'verify'])->name('api.report.verify');
|
||||
Route::post('/pay/wechat/notify', [app\api\controller\PayController::class, 'wechatNotify'])->name('api.pay.wechat.notify');
|
||||
|
||||
Route::group('', function () {
|
||||
Route::get('/auth/me', [app\api\controller\AuthController::class, 'me'])->name('api.auth.me');
|
||||
Route::post('/auth/logout', [app\api\controller\AuthController::class, 'logout'])->name('api.auth.logout');
|
||||
|
||||
Route::post('/upload/image', [app\api\controller\UploadController::class, 'image'])->name('api.upload.image');
|
||||
|
||||
Route::group('/order', function () {
|
||||
Route::post('/create', [app\api\controller\OrderController::class, 'create']);
|
||||
Route::post('/pay', [app\api\controller\OrderController::class, 'pay']);
|
||||
Route::post('/ship', [app\api\controller\OrderController::class, 'ship']);
|
||||
Route::get('/list', [app\api\controller\OrderController::class, 'list']);
|
||||
Route::get('/detail/{id}', [app\api\controller\OrderController::class, 'detail']);
|
||||
});
|
||||
|
||||
Route::group('/report', function () {
|
||||
Route::get('/detail', [app\api\controller\ReportController::class, 'detail']);
|
||||
});
|
||||
|
||||
Route::group('/user', function () {
|
||||
Route::get('/stat', [app\api\controller\UserController::class, 'stat']);
|
||||
Route::post('/update_info', [app\api\controller\UserController::class, 'updateInfo']);
|
||||
});
|
||||
|
||||
})->middleware(app\api\middleware\AuthMiddleware::class);
|
||||
});
|
||||
|
||||
Route::group('/admin', function () {
|
||||
Route::post('/auth/login', [app\admin\controller\AuthController::class, 'login'])->name('admin.auth.login');
|
||||
Route::get('/auth/me', [app\admin\controller\AuthController::class, 'me'])
|
||||
->middleware(app\admin\middleware\AuthMiddleware::class)
|
||||
->name('admin.auth.me');
|
||||
Route::post('/auth/logout', [app\admin\controller\AuthController::class, 'logout'])
|
||||
->middleware(app\admin\middleware\AuthMiddleware::class)
|
||||
->name('admin.auth.logout');
|
||||
|
||||
Route::post('/upload/image', [app\admin\controller\UploadController::class, 'image'])->name('admin.upload.image');
|
||||
|
||||
Route::group('', function () {
|
||||
// Dashboard
|
||||
Route::get('/dashboard/stat', [app\admin\controller\DashboardController::class, 'stat'])->name('admin.dashboard.stat');
|
||||
|
||||
// Order
|
||||
Route::get('/order/list', [app\admin\controller\OrderController::class, 'list'])->name('admin.order.list');
|
||||
Route::get('/order/detail', [app\admin\controller\OrderController::class, 'detail'])->name('admin.order.detail');
|
||||
Route::post('/order/receive', [app\admin\controller\OrderController::class, 'receive'])->name('admin.order.receive');
|
||||
Route::post('/order/return_ship', [app\admin\controller\OrderController::class, 'returnShip'])->name('admin.order.return_ship');
|
||||
|
||||
// Report
|
||||
Route::get('/report/list', [app\admin\controller\ReportController::class, 'list'])->name('admin.report.list');
|
||||
Route::get('/report/detail', [app\admin\controller\ReportController::class, 'detail'])->name('admin.report.detail');
|
||||
Route::post('/report/create', [app\admin\controller\ReportController::class, 'create'])->name('admin.report.create');
|
||||
|
||||
// Admin User
|
||||
Route::get('/admin_user/list', [app\admin\controller\AdminUserController::class, 'list'])->name('admin.admin_user.list');
|
||||
Route::post('/admin_user/create', [app\admin\controller\AdminUserController::class, 'create'])->name('admin.admin_user.create');
|
||||
Route::post('/admin_user/update', [app\admin\controller\AdminUserController::class, 'update'])->name('admin.admin_user.update');
|
||||
Route::post('/admin_user/delete', [app\admin\controller\AdminUserController::class, 'delete'])->name('admin.admin_user.delete');
|
||||
|
||||
// C-User
|
||||
Route::get('/user/list', [app\admin\controller\UserController::class, 'list'])->name('admin.user.list');
|
||||
Route::post('/user/update_status', [app\admin\controller\UserController::class, 'updateStatus'])->name('admin.user.update_status');
|
||||
|
||||
// Role
|
||||
Route::get('/role/list', [app\admin\controller\RoleController::class, 'list'])->name('admin.role.list');
|
||||
Route::get('/role/all', [app\admin\controller\RoleController::class, 'all'])->name('admin.role.all');
|
||||
Route::post('/role/create', [app\admin\controller\RoleController::class, 'create'])->name('admin.role.create');
|
||||
Route::post('/role/update', [app\admin\controller\RoleController::class, 'update'])->name('admin.role.update');
|
||||
Route::post('/role/delete', [app\admin\controller\RoleController::class, 'delete'])->name('admin.role.delete');
|
||||
|
||||
// Permission
|
||||
Route::get('/permission/list', [app\admin\controller\PermissionController::class, 'list'])->name('admin.permission.list');
|
||||
Route::post('/permission/create', [app\admin\controller\PermissionController::class, 'create'])->name('admin.permission.create');
|
||||
Route::post('/permission/update', [app\admin\controller\PermissionController::class, 'update'])->name('admin.permission.update');
|
||||
Route::post('/permission/delete', [app\admin\controller\PermissionController::class, 'delete'])->name('admin.permission.delete');
|
||||
|
||||
// Wechat Merchant
|
||||
Route::get('/wechat_merchant/list', [app\admin\controller\WechatMerchantController::class, 'list'])->name('admin.wechat_merchant.list');
|
||||
Route::post('/wechat_merchant/create', [app\admin\controller\WechatMerchantController::class, 'create'])->name('admin.wechat_merchant.create');
|
||||
Route::post('/wechat_merchant/update', [app\admin\controller\WechatMerchantController::class, 'update'])->name('admin.wechat_merchant.update');
|
||||
Route::post('/wechat_merchant/delete', [app\admin\controller\WechatMerchantController::class, 'delete'])->name('admin.wechat_merchant.delete');
|
||||
Route::post('/wechat_merchant/upload_apiclient_cert', [app\admin\controller\WechatMerchantController::class, 'uploadApiclientCert'])->name('admin.wechat_merchant.upload_apiclient_cert');
|
||||
Route::post('/wechat_merchant/upload_apiclient_key', [app\admin\controller\WechatMerchantController::class, 'uploadApiclientKey'])->name('admin.wechat_merchant.upload_apiclient_key');
|
||||
Route::post('/wechat_merchant/upload_api_v3_key', [app\admin\controller\WechatMerchantController::class, 'uploadApiV3Key'])->name('admin.wechat_merchant.upload_api_v3_key');
|
||||
|
||||
// Wechat App
|
||||
Route::get('/wechat_app/list', [app\admin\controller\WechatAppController::class, 'list'])->name('admin.wechat_app.list');
|
||||
Route::post('/wechat_app/create', [app\admin\controller\WechatAppController::class, 'create'])->name('admin.wechat_app.create');
|
||||
Route::post('/wechat_app/update', [app\admin\controller\WechatAppController::class, 'update'])->name('admin.wechat_app.update');
|
||||
Route::post('/wechat_app/delete', [app\admin\controller\WechatAppController::class, 'delete'])->name('admin.wechat_app.delete');
|
||||
|
||||
})->middleware([
|
||||
app\admin\middleware\AuthMiddleware::class,
|
||||
app\admin\middleware\PermissionMiddleware::class,
|
||||
]);
|
||||
});
|
||||
23
config/server.php
Normal file
23
config/server.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'event_loop' => '',
|
||||
'stop_timeout' => 2,
|
||||
'pid_file' => runtime_path() . '/webman.pid',
|
||||
'status_file' => runtime_path() . '/webman.status',
|
||||
'stdout_file' => runtime_path() . '/logs/stdout.log',
|
||||
'log_file' => runtime_path() . '/logs/workerman.log',
|
||||
'max_package_size' => 10 * 1024 * 1024
|
||||
];
|
||||
65
config/session.php
Normal file
65
config/session.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use Webman\Session\FileSessionHandler;
|
||||
use Webman\Session\RedisSessionHandler;
|
||||
use Webman\Session\RedisClusterSessionHandler;
|
||||
|
||||
return [
|
||||
|
||||
'type' => 'file', // or redis or redis_cluster
|
||||
|
||||
'handler' => FileSessionHandler::class,
|
||||
|
||||
'config' => [
|
||||
'file' => [
|
||||
'save_path' => runtime_path() . '/sessions',
|
||||
],
|
||||
'redis' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'auth' => '',
|
||||
'timeout' => 2,
|
||||
'database' => '',
|
||||
'prefix' => 'redis_session_',
|
||||
],
|
||||
'redis_cluster' => [
|
||||
'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
|
||||
'timeout' => 2,
|
||||
'auth' => '',
|
||||
'prefix' => 'redis_session_',
|
||||
]
|
||||
],
|
||||
|
||||
'session_name' => 'PHPSID',
|
||||
|
||||
'auto_update_timestamp' => false,
|
||||
|
||||
'lifetime' => 7*24*60*60,
|
||||
|
||||
'cookie_lifetime' => 365*24*60*60,
|
||||
|
||||
'cookie_path' => '/',
|
||||
|
||||
'domain' => '',
|
||||
|
||||
'http_only' => true,
|
||||
|
||||
'secure' => false,
|
||||
|
||||
'same_site' => '',
|
||||
|
||||
'gc_probability' => [1, 1000],
|
||||
|
||||
];
|
||||
23
config/static.php
Normal file
23
config/static.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Static file settings
|
||||
*/
|
||||
return [
|
||||
'enable' => true,
|
||||
'middleware' => [ // Static file Middleware
|
||||
//app\middleware\StaticFile::class,
|
||||
],
|
||||
];
|
||||
25
config/translation.php
Normal file
25
config/translation.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Multilingual configuration
|
||||
*/
|
||||
return [
|
||||
// Default language
|
||||
'locale' => 'zh_CN',
|
||||
// Fallback language
|
||||
'fallback_locale' => ['zh_CN', 'en'],
|
||||
// Folder where language files are stored
|
||||
'path' => base_path() . '/resource/translations',
|
||||
];
|
||||
22
config/view.php
Normal file
22
config/view.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\view\Raw;
|
||||
use support\view\Twig;
|
||||
use support\view\Blade;
|
||||
use support\view\ThinkPHP;
|
||||
|
||||
return [
|
||||
'handler' => Raw::class
|
||||
];
|
||||
0
database.sqlite
Normal file
0
database.sqlite
Normal file
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
webman:
|
||||
build: .
|
||||
container_name: docker-webman
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- "./:/app"
|
||||
ports:
|
||||
- "8787:8787"
|
||||
command: ["php", "start.php", "start" ]
|
||||
185
migrate.php
Normal file
185
migrate.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
// 加载环境变量
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
$capsule = new Capsule;
|
||||
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
'port' => $_ENV['DB_PORT'] ?? '3306',
|
||||
'database' => $_ENV['DB_DATABASE'] ?? '',
|
||||
'username' => $_ENV['DB_USERNAME'] ?? '',
|
||||
'password' => $_ENV['DB_PASSWORD'] ?? '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
// 1. 创建 users 表
|
||||
if (!Capsule::schema()->hasTable('users')) {
|
||||
Capsule::schema()->create('users', function ($table) {
|
||||
$table->id();
|
||||
$table->string('openid', 64)->nullable()->unique();
|
||||
$table->string('mobile', 20)->nullable()->unique();
|
||||
$table->string('nickname', 50)->nullable();
|
||||
$table->string('avatar', 255)->nullable();
|
||||
$table->tinyInteger('status')->default(1)->comment('1:正常 0:禁用');
|
||||
$table->timestamps();
|
||||
});
|
||||
echo "Table 'users' created successfully.\n";
|
||||
}
|
||||
|
||||
// 2. 创建 orders 表
|
||||
if (!Capsule::schema()->hasTable('orders')) {
|
||||
Capsule::schema()->create('orders', function ($table) {
|
||||
$table->id();
|
||||
$table->string('order_no', 32)->unique();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->string('category', 32);
|
||||
$table->string('service_type', 64);
|
||||
$table->string('brand', 64);
|
||||
$table->string('model', 128)->nullable();
|
||||
$table->string('remark', 500)->nullable();
|
||||
$table->tinyInteger('is_fast')->default(0);
|
||||
$table->decimal('total_price', 10, 2)->default(0.00);
|
||||
$table->string('status', 20)->default('wait_pay')->index();
|
||||
$table->string('express_company', 32)->nullable();
|
||||
$table->string('express_no', 64)->nullable();
|
||||
$table->timestamp('pay_time')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
echo "Table 'orders' created successfully.\n";
|
||||
}
|
||||
|
||||
// 3. 创建 order_logs 表
|
||||
if (!Capsule::schema()->hasTable('order_logs')) {
|
||||
Capsule::schema()->create('order_logs', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('order_id');
|
||||
$table->string('action_type', 32);
|
||||
$table->string('title', 64);
|
||||
$table->string('description', 500)->nullable();
|
||||
$table->string('operator_type', 20)->default('system');
|
||||
$table->bigInteger('operator_id')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['order_id', 'created_at']);
|
||||
});
|
||||
echo "Table 'order_logs' created successfully.\n";
|
||||
}
|
||||
|
||||
// 4. 创建 reports 表
|
||||
if (!Capsule::schema()->hasTable('reports')) {
|
||||
Capsule::schema()->create('reports', function ($table) {
|
||||
$table->id();
|
||||
$table->string('report_no', 32)->unique();
|
||||
$table->unsignedBigInteger('order_id')->unique();
|
||||
$table->string('conclusion', 20);
|
||||
$table->string('level', 20)->nullable();
|
||||
$table->json('flaws_json')->nullable();
|
||||
$table->json('images_json');
|
||||
$table->bigInteger('inspector_id');
|
||||
$table->string('verify_code', 32)->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
echo "Table 'reports' created successfully.\n";
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('admin_users')) {
|
||||
Capsule::schema()->create('admin_users', function ($table) {
|
||||
$table->id();
|
||||
$table->string('username', 50)->unique();
|
||||
$table->string('password_hash', 255);
|
||||
$table->string('nickname', 50)->nullable();
|
||||
$table->tinyInteger('is_super')->default(0);
|
||||
$table->tinyInteger('status')->default(1);
|
||||
$table->timestamps();
|
||||
});
|
||||
echo "Table 'admin_users' created successfully.\n";
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('roles')) {
|
||||
Capsule::schema()->create('roles', function ($table) {
|
||||
$table->id();
|
||||
$table->string('name', 50);
|
||||
$table->string('code', 50)->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
echo "Table 'roles' created successfully.\n";
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('permissions')) {
|
||||
Capsule::schema()->create('permissions', function ($table) {
|
||||
$table->id();
|
||||
$table->string('name', 50);
|
||||
$table->string('code', 80)->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
echo "Table 'permissions' created successfully.\n";
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('admin_roles')) {
|
||||
Capsule::schema()->create('admin_roles', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('admin_id');
|
||||
$table->unsignedBigInteger('role_id');
|
||||
$table->timestamps();
|
||||
$table->unique(['admin_id', 'role_id']);
|
||||
$table->index(['admin_id']);
|
||||
$table->index(['role_id']);
|
||||
});
|
||||
echo "Table 'admin_roles' created successfully.\n";
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('role_permissions')) {
|
||||
Capsule::schema()->create('role_permissions', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('role_id');
|
||||
$table->unsignedBigInteger('permission_id');
|
||||
$table->timestamps();
|
||||
$table->unique(['role_id', 'permission_id']);
|
||||
$table->index(['role_id']);
|
||||
$table->index(['permission_id']);
|
||||
});
|
||||
echo "Table 'role_permissions' created successfully.\n";
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('user_tokens')) {
|
||||
Capsule::schema()->create('user_tokens', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->string('token_hash', 64)->unique();
|
||||
$table->timestamp('expired_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['user_id']);
|
||||
$table->index(['expired_at']);
|
||||
});
|
||||
echo "Table 'user_tokens' created successfully.\n";
|
||||
}
|
||||
|
||||
if (!Capsule::schema()->hasTable('admin_tokens')) {
|
||||
Capsule::schema()->create('admin_tokens', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('admin_id');
|
||||
$table->string('token_hash', 64)->unique();
|
||||
$table->timestamp('expired_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['admin_id']);
|
||||
$table->index(['expired_at']);
|
||||
});
|
||||
echo "Table 'admin_tokens' created successfully.\n";
|
||||
}
|
||||
|
||||
echo "All migrations completed.\n";
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
126
seed.php
Normal file
126
seed.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use app\common\model\AdminUser;
|
||||
use app\common\model\Role;
|
||||
use app\common\model\Permission;
|
||||
|
||||
if (class_exists('Dotenv\Dotenv') && file_exists(__DIR__ . '/.env')) {
|
||||
if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) {
|
||||
Dotenv\Dotenv::createUnsafeImmutable(__DIR__)->load();
|
||||
} else {
|
||||
Dotenv\Dotenv::createMutable(__DIR__)->load();
|
||||
}
|
||||
}
|
||||
|
||||
$capsule = new Capsule();
|
||||
$capsule->addConnection([
|
||||
'driver' => 'mysql',
|
||||
'host' => getenv('DB_HOST') ?: '127.0.0.1',
|
||||
'port' => getenv('DB_PORT') ?: '3306',
|
||||
'database' => getenv('DB_DATABASE') ?: '',
|
||||
'username' => getenv('DB_USERNAME') ?: '',
|
||||
'password' => getenv('DB_PASSWORD') ?: '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
$permissionNodes = [
|
||||
['code' => 'admin.menu.dashboard', 'name' => '工作台', 'parent_code' => null, 'type' => 1, 'sort' => 10],
|
||||
['code' => 'admin.dashboard.stat', 'name' => '工作台统计', 'parent_code' => 'admin.menu.dashboard', 'type' => 1, 'sort' => 10],
|
||||
|
||||
['code' => 'admin.menu.order', 'name' => '订单管理', 'parent_code' => null, 'type' => 1, 'sort' => 20],
|
||||
['code' => 'admin.order.list', 'name' => '订单列表', 'parent_code' => 'admin.menu.order', 'type' => 1, 'sort' => 10],
|
||||
['code' => 'admin.order.detail', 'name' => '订单详情', 'parent_code' => 'admin.order.list', 'type' => 2, 'sort' => 20],
|
||||
['code' => 'admin.order.receive', 'name' => '确认收件', 'parent_code' => 'admin.order.list', 'type' => 2, 'sort' => 30],
|
||||
['code' => 'admin.order.return_ship', 'name' => '回寄物流填写', 'parent_code' => 'admin.order.list', 'type' => 2, 'sort' => 40],
|
||||
|
||||
['code' => 'admin.menu.report', 'name' => '报告管理', 'parent_code' => null, 'type' => 1, 'sort' => 30],
|
||||
['code' => 'admin.report.list', 'name' => '报告列表', 'parent_code' => 'admin.menu.report', 'type' => 1, 'sort' => 10],
|
||||
['code' => 'admin.report.detail', 'name' => '报告详情', 'parent_code' => 'admin.report.list', 'type' => 2, 'sort' => 20],
|
||||
['code' => 'admin.report.create', 'name' => '出具报告', 'parent_code' => 'admin.report.list', 'type' => 2, 'sort' => 30],
|
||||
|
||||
['code' => 'admin.menu.user', 'name' => '用户管理', 'parent_code' => null, 'type' => 1, 'sort' => 40],
|
||||
['code' => 'admin.user.list', 'name' => 'C端用户列表', 'parent_code' => 'admin.menu.user', 'type' => 1, 'sort' => 10],
|
||||
['code' => 'admin.user.update_status', 'name' => '用户状态修改', 'parent_code' => 'admin.user.list', 'type' => 2, 'sort' => 20],
|
||||
|
||||
['code' => 'admin.menu.system', 'name' => '系统设置', 'parent_code' => null, 'type' => 1, 'sort' => 50],
|
||||
|
||||
['code' => 'admin.admin_user.list', 'name' => '管理员管理', 'parent_code' => 'admin.menu.system', 'type' => 1, 'sort' => 10],
|
||||
['code' => 'admin.admin_user.create', 'name' => '管理员创建', 'parent_code' => 'admin.admin_user.list', 'type' => 2, 'sort' => 20],
|
||||
['code' => 'admin.admin_user.update', 'name' => '管理员更新', 'parent_code' => 'admin.admin_user.list', 'type' => 2, 'sort' => 30],
|
||||
['code' => 'admin.admin_user.delete', 'name' => '管理员删除', 'parent_code' => 'admin.admin_user.list', 'type' => 2, 'sort' => 40],
|
||||
|
||||
['code' => 'admin.role.list', 'name' => '角色管理', 'parent_code' => 'admin.menu.system', 'type' => 1, 'sort' => 20],
|
||||
['code' => 'admin.role.all', 'name' => '角色全部列表', 'parent_code' => 'admin.role.list', 'type' => 2, 'sort' => 30],
|
||||
['code' => 'admin.role.create', 'name' => '角色创建', 'parent_code' => 'admin.role.list', 'type' => 2, 'sort' => 40],
|
||||
['code' => 'admin.role.update', 'name' => '角色更新', 'parent_code' => 'admin.role.list', 'type' => 2, 'sort' => 50],
|
||||
['code' => 'admin.role.delete', 'name' => '角色删除', 'parent_code' => 'admin.role.list', 'type' => 2, 'sort' => 60],
|
||||
|
||||
['code' => 'admin.permission.list', 'name' => '权限管理', 'parent_code' => 'admin.menu.system', 'type' => 1, 'sort' => 30],
|
||||
['code' => 'admin.permission.create', 'name' => '权限创建', 'parent_code' => 'admin.permission.list', 'type' => 2, 'sort' => 40],
|
||||
['code' => 'admin.permission.update', 'name' => '权限更新', 'parent_code' => 'admin.permission.list', 'type' => 2, 'sort' => 50],
|
||||
['code' => 'admin.permission.delete', 'name' => '权限删除', 'parent_code' => 'admin.permission.list', 'type' => 2, 'sort' => 60],
|
||||
|
||||
['code' => 'admin.wechat_merchant.list', 'name' => '微信商户号', 'parent_code' => 'admin.menu.system', 'type' => 1, 'sort' => 40],
|
||||
['code' => 'admin.wechat_merchant.create', 'name' => '微信商户号创建', 'parent_code' => 'admin.wechat_merchant.list', 'type' => 2, 'sort' => 20],
|
||||
['code' => 'admin.wechat_merchant.update', 'name' => '微信商户号更新', 'parent_code' => 'admin.wechat_merchant.list', 'type' => 2, 'sort' => 30],
|
||||
['code' => 'admin.wechat_merchant.delete', 'name' => '微信商户号删除', 'parent_code' => 'admin.wechat_merchant.list', 'type' => 2, 'sort' => 40],
|
||||
['code' => 'admin.wechat_merchant.upload_apiclient_cert', 'name' => '商户证书上传', 'parent_code' => 'admin.wechat_merchant.list', 'type' => 2, 'sort' => 50],
|
||||
['code' => 'admin.wechat_merchant.upload_apiclient_key', 'name' => '商户私钥上传', 'parent_code' => 'admin.wechat_merchant.list', 'type' => 2, 'sort' => 60],
|
||||
['code' => 'admin.wechat_merchant.upload_api_v3_key', 'name' => 'APIv3密钥上传', 'parent_code' => 'admin.wechat_merchant.list', 'type' => 2, 'sort' => 70],
|
||||
|
||||
['code' => 'admin.wechat_app.list', 'name' => '微信应用', 'parent_code' => 'admin.menu.system', 'type' => 1, 'sort' => 35],
|
||||
['code' => 'admin.wechat_app.create', 'name' => '微信应用创建', 'parent_code' => 'admin.wechat_app.list', 'type' => 2, 'sort' => 20],
|
||||
['code' => 'admin.wechat_app.update', 'name' => '微信应用更新', 'parent_code' => 'admin.wechat_app.list', 'type' => 2, 'sort' => 30],
|
||||
['code' => 'admin.wechat_app.delete', 'name' => '微信应用删除', 'parent_code' => 'admin.wechat_app.list', 'type' => 2, 'sort' => 40],
|
||||
];
|
||||
|
||||
$codeToId = [];
|
||||
foreach ($permissionNodes as $node) {
|
||||
$permission = Permission::updateOrCreate(['code' => $node['code']], [
|
||||
'name' => $node['name'],
|
||||
'parent_id' => 0,
|
||||
'type' => $node['type'],
|
||||
'sort' => $node['sort'],
|
||||
]);
|
||||
$codeToId[$node['code']] = $permission->id;
|
||||
}
|
||||
|
||||
foreach ($permissionNodes as $node) {
|
||||
$parentCode = $node['parent_code'];
|
||||
$parentId = $parentCode && isset($codeToId[$parentCode]) ? $codeToId[$parentCode] : 0;
|
||||
Permission::where('code', $node['code'])->update(['parent_id' => $parentId]);
|
||||
}
|
||||
|
||||
$role = Role::firstOrCreate(['code' => 'super_admin'], ['name' => '超级管理员']);
|
||||
$permissionCodes = array_map(function ($n) { return $n['code']; }, $permissionNodes);
|
||||
$permissionIds = Permission::whereIn('code', $permissionCodes)->pluck('id')->toArray();
|
||||
$role->permissions()->syncWithoutDetaching($permissionIds);
|
||||
|
||||
$username = getenv('ADMIN_INIT_USERNAME') ?: 'admin';
|
||||
$password = getenv('ADMIN_INIT_PASSWORD') ?: '';
|
||||
if ($password === '') {
|
||||
echo "ADMIN_INIT_PASSWORD 为空,请先在 .env 中设置\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$admin = AdminUser::where('username', $username)->first();
|
||||
if (!$admin) {
|
||||
$admin = AdminUser::create([
|
||||
'username' => $username,
|
||||
'password_hash' => password_hash($password, PASSWORD_BCRYPT),
|
||||
'nickname' => '管理员',
|
||||
'is_super' => 1,
|
||||
'status' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$admin->roles()->syncWithoutDetaching([$role->id]);
|
||||
|
||||
echo "seed ok\n";
|
||||
6
start.php
Executable file
6
start.php
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
chdir(__DIR__);
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
support\App::run();
|
||||
35
support/ExceptionHandler.php
Normal file
35
support/ExceptionHandler.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace support;
|
||||
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Exception\ExceptionHandler as BaseExceptionHandler;
|
||||
use app\common\exception\BusinessException;
|
||||
|
||||
class ExceptionHandler extends BaseExceptionHandler
|
||||
{
|
||||
public function render(Request $request, Throwable $exception): Response
|
||||
{
|
||||
$code = $exception->getCode();
|
||||
$message = $exception->getMessage();
|
||||
$data = null;
|
||||
|
||||
if ($exception instanceof BusinessException) {
|
||||
$code = $code ?: 400;
|
||||
$data = $exception->getData();
|
||||
} else {
|
||||
$code = 500;
|
||||
$message = 'Server internal error';
|
||||
if (config('app.debug')) {
|
||||
$message = $exception->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $message,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
24
support/Request.php
Normal file
24
support/Request.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace support;
|
||||
|
||||
/**
|
||||
* Class Request
|
||||
* @package support
|
||||
*/
|
||||
class Request extends \Webman\Http\Request
|
||||
{
|
||||
|
||||
}
|
||||
24
support/Response.php
Normal file
24
support/Response.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace support;
|
||||
|
||||
/**
|
||||
* Class Response
|
||||
* @package support
|
||||
*/
|
||||
class Response extends \Webman\Http\Response
|
||||
{
|
||||
|
||||
}
|
||||
1558
support/Setup.php
Normal file
1558
support/Setup.php
Normal file
File diff suppressed because it is too large
Load Diff
3
support/bootstrap.php
Normal file
3
support/bootstrap.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/workerman/webman-framework/src/support/bootstrap.php';
|
||||
20
support/bootstrap/Database.php
Normal file
20
support/bootstrap/Database.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace support\bootstrap;
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
class Database
|
||||
{
|
||||
public static function start($worker)
|
||||
{
|
||||
$config = config('database');
|
||||
$default = $config['default'] ?? 'mysql';
|
||||
$connection = $config['connections'][$default] ?? [];
|
||||
|
||||
$capsule = new Capsule();
|
||||
$capsule->addConnection($connection);
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
}
|
||||
}
|
||||
|
||||
25
support/helpers.php
Normal file
25
support/helpers.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
if (!function_exists('jsonResponse')) {
|
||||
function jsonResponse($data = [], $msg = 'success', $code = 200)
|
||||
{
|
||||
return json([
|
||||
'code' => $code,
|
||||
'msg' => $msg,
|
||||
'data' => $data
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('generateToken')) {
|
||||
function generateToken(): string
|
||||
{
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('hashToken')) {
|
||||
function hashToken(string $token): string
|
||||
{
|
||||
return hash('sha256', $token);
|
||||
}
|
||||
}
|
||||
3
windows.bat
Normal file
3
windows.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
CHCP 65001
|
||||
php windows.php
|
||||
pause
|
||||
136
windows.php
Normal file
136
windows.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* Start file for windows
|
||||
*/
|
||||
chdir(__DIR__);
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use support\App;
|
||||
use Workerman\Worker;
|
||||
|
||||
ini_set('display_errors', 'on');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (class_exists('Dotenv\Dotenv') && file_exists(base_path() . '/.env')) {
|
||||
if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) {
|
||||
Dotenv::createUnsafeImmutable(base_path())->load();
|
||||
} else {
|
||||
Dotenv::createMutable(base_path())->load();
|
||||
}
|
||||
}
|
||||
|
||||
App::loadAllConfig(['route']);
|
||||
|
||||
$errorReporting = config('app.error_reporting');
|
||||
if (isset($errorReporting)) {
|
||||
error_reporting($errorReporting);
|
||||
}
|
||||
|
||||
$runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows';
|
||||
$paths = [
|
||||
$runtimeProcessPath,
|
||||
runtime_path('logs'),
|
||||
runtime_path('views')
|
||||
];
|
||||
foreach ($paths as $path) {
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
$processFiles = [];
|
||||
if (config('server.listen')) {
|
||||
$processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php';
|
||||
}
|
||||
foreach (config('process', []) as $processName => $config) {
|
||||
$processFiles[] = write_process_file($runtimeProcessPath, $processName, '');
|
||||
}
|
||||
|
||||
foreach (config('plugin', []) as $firm => $projects) {
|
||||
foreach ($projects as $name => $project) {
|
||||
if (!is_array($project)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($project['process'] ?? [] as $processName => $config) {
|
||||
$processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name");
|
||||
}
|
||||
}
|
||||
foreach ($projects['process'] ?? [] as $processName => $config) {
|
||||
$processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm);
|
||||
}
|
||||
}
|
||||
|
||||
function write_process_file($runtimeProcessPath, $processName, $firm): string
|
||||
{
|
||||
$processParam = $firm ? "plugin.$firm.$processName" : $processName;
|
||||
$configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']";
|
||||
$fileContent = <<<EOF
|
||||
<?php
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use Workerman\Worker;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Webman\Config;
|
||||
use support\App;
|
||||
|
||||
ini_set('display_errors', 'on');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (is_callable('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
|
||||
if (!\$appConfigFile = config_path('app.php')) {
|
||||
throw new RuntimeException('Config file not found: app.php');
|
||||
}
|
||||
\$appConfig = require \$appConfigFile;
|
||||
if (\$timezone = \$appConfig['default_timezone'] ?? '') {
|
||||
date_default_timezone_set(\$timezone);
|
||||
}
|
||||
|
||||
App::loadAllConfig(['route']);
|
||||
|
||||
worker_start('$processParam', $configParam);
|
||||
|
||||
if (DIRECTORY_SEPARATOR != "/") {
|
||||
Worker::\$logFile = config('server')['log_file'] ?? Worker::\$logFile;
|
||||
TcpConnection::\$defaultMaxPackageSize = config('server')['max_package_size'] ?? 10*1024*1024;
|
||||
}
|
||||
|
||||
Worker::runAll();
|
||||
|
||||
EOF;
|
||||
$processFile = $runtimeProcessPath . DIRECTORY_SEPARATOR . "start_$processParam.php";
|
||||
file_put_contents($processFile, $fileContent);
|
||||
return $processFile;
|
||||
}
|
||||
|
||||
if ($monitorConfig = config('process.monitor.constructor')) {
|
||||
$monitorHandler = config('process.monitor.handler');
|
||||
$monitor = new $monitorHandler(...array_values($monitorConfig));
|
||||
}
|
||||
|
||||
function popen_processes($processFiles)
|
||||
{
|
||||
$cmd = '"' . PHP_BINARY . '" ' . implode(' ', $processFiles);
|
||||
$descriptorspec = [STDIN, STDOUT, STDOUT];
|
||||
$resource = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
|
||||
if (!$resource) {
|
||||
exit("Can not execute $cmd\r\n");
|
||||
}
|
||||
return $resource;
|
||||
}
|
||||
|
||||
$resource = popen_processes($processFiles);
|
||||
echo "\r\n";
|
||||
while (1) {
|
||||
sleep(1);
|
||||
if (!empty($monitor) && $monitor->checkAllFilesChange()) {
|
||||
$status = proc_get_status($resource);
|
||||
$pid = $status['pid'];
|
||||
shell_exec("taskkill /F /T /PID $pid");
|
||||
proc_close($resource);
|
||||
$resource = popen_processes($processFiles);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user