first commit

This commit is contained in:
wushumin
2026-04-16 11:17:18 +08:00
commit 5b9c398e68
98 changed files with 8701 additions and 0 deletions

12
.env.example Normal file
View 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
View File

@@ -0,0 +1,8 @@
/runtime
/.idea
/.vscode
/vendor
*.log
.env
/tests/tmp
/tests/.phpunit.result.cache

18
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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";

View 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
View 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";

View 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";

View 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";

View 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";

View 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);
}
}
}

View 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, '已退出登录');
}
}

View 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);
}
}

View 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);
}
}
}

View 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, '删除成功');
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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(),
], '上传成功');
}
}

View 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, '更新状态成功');
}
}

View 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, '删除成功');
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View 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, '已退出登录');
}
}

View 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);
}
}
}

View 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']);
}
}

View 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,
]
], '验证成功,该报告真实有效');
}
}

View 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(),
], '上传成功');
}
}

View 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, '更新成功');
}
}

View 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]
);
}
}

View 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);
}
}

View 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;
}
}

View 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',
];
}

View 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');
}
}

View 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');
}
}

View 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 = [];
}

View 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',
];
}

View 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'];
}

View 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
View 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
View 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'];
}

View 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',
];
}

View 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'];
}

View 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'];
}

View 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'];
}

View 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();
}
}

View 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
]);
}
}

View 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');
}
}

View 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 : '';
}
}

View 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
View File

@@ -0,0 +1,4 @@
<?php
/**
* Here is your custom functions.
*/

46
app/middleware/Cors.php Normal file
View 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);
}
}

View 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
View 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
View File

@@ -0,0 +1,10 @@
<?php
namespace app\process;
use Webman\App;
class Http extends App
{
}

305
app/process/Monitor.php Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

26
config/app.php Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,4 @@
<?php
return [
'' => support\ExceptionHandler::class,
];

32
config/log.php Normal file
View 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
View 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,
],
];

View File

@@ -0,0 +1,4 @@
<?php
return [
'enable' => true,
];

View File

@@ -0,0 +1,7 @@
<?php
use Webman\RedisQueue\Command\MakeConsumerCommand;
return [
MakeConsumerCommand::class
];

View 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],
],
]
],
]
];

View File

@@ -0,0 +1,11 @@
<?php
return [
'consumer' => [
'handler' => Webman\RedisQueue\Process\Consumer::class,
'count' => 8, // 可以设置多进程同时消费
'constructor' => [
// 消费者类目录
'consumer_dir' => app_path() . '/queue/redis'
]
]
];

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

11
docker-compose.yml Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

126
seed.php Normal file
View 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
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env php
<?php
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
support\App::run();

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

3
support/bootstrap.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
require_once __DIR__ . '/../vendor/workerman/webman-framework/src/support/bootstrap.php';

View 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
View 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
View File

@@ -0,0 +1,3 @@
CHCP 65001
php windows.php
pause

136
windows.php Normal file
View 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);
}
}