重构 monorepo 并完善网页端订阅与首页体验

- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-08 18:17:39 +08:00
parent 5814b7bc0e
commit 88c6ce8ccc
511 changed files with 189528 additions and 22804 deletions
+93
View File
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/bootstrap.php';
use Soon\Api\Admin\Controllers\AuditsController;
use Soon\Api\Admin\Controllers\OrdersController;
use Soon\Api\Admin\Controllers\PaymentController;
use Soon\Api\Admin\Controllers\PlansController;
use Soon\Api\Admin\Controllers\SettingsController;
use Soon\Api\Admin\Controllers\StatsController;
use Soon\Api\Admin\Controllers\UsersController;
use Soon\Api\Core\Config;
use Soon\Api\Core\Json;
use Soon\Api\Core\Router;
use Soon\Api\Middleware\AdminAuth;
use Soon\Api\Middleware\RateLimit;
use Soon\Api\Services\AdminPermission;
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowed = (array)Config::get('app.cors_origins', []);
if ($origin !== '' && in_array($origin, $allowed, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
}
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer');
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
http_response_code(204);
exit;
}
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
Router::get('/api/admin/stats', [StatsController::class, 'summary']);
Router::get('/api/admin/users', [UsersController::class, 'list']);
Router::post('/api/admin/users', [UsersController::class, 'create']);
Router::get('/api/admin/users/{id}', [UsersController::class, 'detail']);
Router::put('/api/admin/users/{id}', [UsersController::class, 'update']);
Router::delete('/api/admin/users/{id}', [UsersController::class, 'delete']);
Router::post('/api/admin/users/{id}/password', [UsersController::class, 'setPassword']);
Router::post('/api/admin/users/{id}/subscription', [UsersController::class, 'setSubscription']);
Router::post('/api/admin/users/{id}/status', [UsersController::class, 'setStatus']);
Router::get('/api/admin/orders', [OrdersController::class, 'list']);
Router::get('/api/admin/orders/{id}', [OrdersController::class, 'detail']);
Router::post('/api/admin/orders/{id}/cancel', [OrdersController::class, 'cancel']);
Router::post('/api/admin/orders/{id}/refund/approve', [OrdersController::class, 'refundApprove']);
Router::post('/api/admin/orders/{id}/refund/reject', [OrdersController::class, 'refundReject']);
Router::get('/api/admin/plans', [PlansController::class, 'list']);
Router::post('/api/admin/plans', [PlansController::class, 'upsert']);
Router::get('/api/admin/settings', [SettingsController::class, 'list']);
Router::post('/api/admin/settings', [SettingsController::class, 'set']);
Router::post('/api/admin/settings/batch', [SettingsController::class, 'setBatch']);
Router::get('/api/admin/audits', [AuditsController::class, 'list']);
Router::post('/api/admin/payment/upload', [PaymentController::class, 'upload']);
Router::get('/api/admin/payment/status', [PaymentController::class, 'status']);
if (str_starts_with($path, '/api/admin/')) {
$adminId = AdminAuth::require();
AdminPermission::assertPath($adminId, $path);
RateLimit::check('admin/*', $method, $path, $adminId);
}
$match = Router::dispatch($method, $path);
if ($match === null) {
Json::fail('not_found', '管理接口不存在', 404);
}
[$handler, $params] = $match;
foreach ($params as $k => $v) {
if ($k === 'id' && is_string($v) && ctype_digit($v)) {
$params[$k] = (int)$v;
}
}
if (is_array($handler)) {
[$class, $action] = $handler;
$instance = new $class();
$args = array_values($params);
$instance->$action($adminId, ...$args);
} else {
($handler)($params);
}
+101
View File
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/bootstrap.php';
use Soon\Api\Controllers\AuthController;
use Soon\Api\Controllers\FileController;
use Soon\Api\Controllers\PayController;
use Soon\Api\Controllers\PlanController;
use Soon\Api\Controllers\SettingsController;
use Soon\Api\Controllers\SoonModelController;
use Soon\Api\Core\Config;
use Soon\Api\Core\Json;
use Soon\Api\Core\Router;
use Soon\Api\Middleware\Auth;
use Soon\Api\Middleware\RateLimit;
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowed = (array)Config::get('app.cors_origins', []);
if ($origin !== '' && in_array($origin, $allowed, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, Range');
header('Access-Control-Expose-Headers: ETag, Content-Range, Content-Length');
}
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
http_response_code(204);
exit;
}
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
Router::post('/api/v1/auth/register', [AuthController::class, 'register']);
Router::post('/api/v1/auth/login', [AuthController::class, 'login']);
Router::post('/api/v1/auth/refresh', [AuthController::class, 'refresh']);
Router::get('/api/v1/auth/me', [AuthController::class, 'me']);
Router::get('/api/v1/files', [FileController::class, 'index']);
Router::post('/api/v1/files', [FileController::class, 'create']);
Router::put('/api/v1/files/{id}', [FileController::class, 'update']);
Router::delete('/api/v1/files/{id}', [FileController::class, 'delete']);
Router::get('/api/v1/files/{id}/download', [FileController::class, 'download']);
Router::get('/api/v1/plans', [PlanController::class, 'index']);
Router::get('/api/v1/plans/me', [PlanController::class, 'myPlan']);
Router::get('/api/v1/pay/orders', [PayController::class, 'listMyOrders']);
Router::post('/api/v1/pay/orders', [PayController::class, 'createOrder']);
Router::get('/api/v1/pay/orders/{order_no}', [PayController::class, 'showOrder']);
Router::post('/api/v1/pay/orders/{order_no}/checkout', [PayController::class, 'checkoutOrder']);
Router::post('/api/v1/pay/orders/{order_no}/cancel', [PayController::class, 'cancelOrder']);
Router::post('/api/v1/pay/orders/{order_no}/refund-request', [PayController::class, 'requestRefund']);
Router::post('/api/v1/pay/alipay/notify', [PayController::class, 'alipayNotify']);
Router::post('/api/v1/pay/wechat/notify', [PayController::class, 'wechatNotify']);
Router::get('/api/v1/soon-models', [SoonModelController::class, 'index']);
Router::get('/api/v1/settings', [SettingsController::class, 'publicSettings']);
if (str_starts_with($path, '/api/v1/')) {
$routeKey = $method . ' ' . $path;
if (str_starts_with($path, '/api/v1/auth/login') || str_starts_with($path, '/api/v1/auth/register') || str_starts_with($path, '/api/v1/auth/refresh')) {
RateLimit::check($routeKey, $method, $path, null);
} elseif (str_starts_with($path, '/api/v1/pay/') && str_contains($path, '/notify')) {
// 通知不进限速
} else {
$uid = null;
$tok = Auth::bearerFromGlobals();
if ($tok !== '') {
$payload = \Soon\Api\Core\Jwt::decode($tok);
if ($payload !== null) $uid = (int)($payload['sub'] ?? 0) ?: null;
}
RateLimit::check($routeKey, $method, $path, $uid);
}
}
$match = Router::dispatch($method, $path);
if ($match === null) {
Json::fail('not_found', '接口不存在', 404);
}
[$handler, $params] = $match;
foreach ($params as $k => $v) {
if ($k === 'id' && is_string($v) && ctype_digit($v)) {
$params[$k] = (int)$v;
}
}
if (is_array($handler)) {
[$class, $action] = $handler;
$instance = new $class();
$args = array_values($params);
$instance->$action(...$args);
} else {
($handler)($params);
}