04a2034975
- storefront 全站改为极简黑白配色:直角、细线分隔、卡片 hover 浅底、深色模式(跟随系统+手动切换) - 抽出 storefront/partials/card 复用卡片 - 一并提交此前暂存的客户端注册 API、站点设置、后台页面与测试 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
198 lines
6.8 KiB
PHP
198 lines
6.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\StoreClient;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ClientController extends Controller
|
|
{
|
|
public function register(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'site_url' => ['required', 'url', 'max:512'],
|
|
'site_name' => ['nullable', 'string', 'max:255'],
|
|
'typecho_version' => ['nullable', 'string', 'max:32'],
|
|
'php_version' => ['nullable', 'string', 'max:32'],
|
|
'plugin_version' => ['nullable', 'string', 'max:32'],
|
|
'user_count' => ['nullable', 'integer', 'min:0'],
|
|
]);
|
|
|
|
$siteUrl = $this->normalizeSiteUrl((string) $validated['site_url']);
|
|
$now = now();
|
|
|
|
$client = StoreClient::query()->firstOrNew([
|
|
'site_url' => $siteUrl,
|
|
]);
|
|
|
|
if (!$client->exists) {
|
|
$client->registered_at = $now;
|
|
}
|
|
|
|
$client->site_name = $this->truncate((string) ($validated['site_name'] ?? ''), 255);
|
|
$client->access_token = $this->issueAccessToken();
|
|
$client->status = 'online';
|
|
$client->user_count = max((int) ($validated['user_count'] ?? 0), 0);
|
|
$client->typecho_version = $this->truncate((string) ($validated['typecho_version'] ?? ''), 32);
|
|
$client->php_version = $this->truncate((string) ($validated['php_version'] ?? ''), 32);
|
|
$client->plugin_version = $this->truncate((string) ($validated['plugin_version'] ?? ''), 32);
|
|
$client->last_ip = $this->truncate((string) ($request->ip() ?? ''), 45);
|
|
$client->last_user_agent = $this->truncate((string) ($request->userAgent() ?? ''), 512);
|
|
$client->last_seen_at = $now;
|
|
$client->save();
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'message' => 'ok',
|
|
'data' => $this->clientPayload($client, true),
|
|
]);
|
|
}
|
|
|
|
public function heartbeat(Request $request): JsonResponse
|
|
{
|
|
$client = $this->resolveClient($request);
|
|
if (!$client) {
|
|
return $this->unauthorizedResponse();
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'site_name' => ['nullable', 'string', 'max:255'],
|
|
'typecho_version' => ['nullable', 'string', 'max:32'],
|
|
'php_version' => ['nullable', 'string', 'max:32'],
|
|
'plugin_version' => ['nullable', 'string', 'max:32'],
|
|
'user_count' => ['nullable', 'integer', 'min:0'],
|
|
]);
|
|
|
|
if (array_key_exists('site_name', $validated)) {
|
|
$client->site_name = $this->truncate((string) $validated['site_name'], 255);
|
|
}
|
|
if (array_key_exists('typecho_version', $validated)) {
|
|
$client->typecho_version = $this->truncate((string) $validated['typecho_version'], 32);
|
|
}
|
|
if (array_key_exists('php_version', $validated)) {
|
|
$client->php_version = $this->truncate((string) $validated['php_version'], 32);
|
|
}
|
|
if (array_key_exists('plugin_version', $validated)) {
|
|
$client->plugin_version = $this->truncate((string) $validated['plugin_version'], 32);
|
|
}
|
|
if (array_key_exists('user_count', $validated)) {
|
|
$client->user_count = max((int) $validated['user_count'], 0);
|
|
}
|
|
|
|
$client->status = 'online';
|
|
$client->last_ip = $this->truncate((string) ($request->ip() ?? ''), 45);
|
|
$client->last_user_agent = $this->truncate((string) ($request->userAgent() ?? ''), 512);
|
|
$client->last_seen_at = now();
|
|
$client->save();
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'message' => 'ok',
|
|
'data' => $this->clientPayload($client, false),
|
|
]);
|
|
}
|
|
|
|
public function status(Request $request): JsonResponse
|
|
{
|
|
$client = $this->resolveClient($request);
|
|
if (!$client) {
|
|
return $this->unauthorizedResponse();
|
|
}
|
|
|
|
$client->status = 'online';
|
|
$client->last_ip = $this->truncate((string) ($request->ip() ?? ''), 45);
|
|
$client->last_user_agent = $this->truncate((string) ($request->userAgent() ?? ''), 512);
|
|
$client->last_seen_at = now();
|
|
$client->save();
|
|
|
|
return response()->json([
|
|
'code' => 0,
|
|
'message' => 'ok',
|
|
'data' => $this->clientPayload($client, false),
|
|
]);
|
|
}
|
|
|
|
private function resolveClient(Request $request): ?StoreClient
|
|
{
|
|
$token = $this->extractToken($request);
|
|
if ($token === '') {
|
|
return null;
|
|
}
|
|
|
|
return StoreClient::query()->where('access_token', $token)->first();
|
|
}
|
|
|
|
private function extractToken(Request $request): string
|
|
{
|
|
return trim((string) (
|
|
$request->bearerToken()
|
|
?: $request->header('X-Store-Plugin-Token')
|
|
?: $request->query('access_token', '')
|
|
));
|
|
}
|
|
|
|
private function unauthorizedResponse(): JsonResponse
|
|
{
|
|
return response()->json([
|
|
'code' => 401,
|
|
'message' => 'client unauthorized',
|
|
'data' => null,
|
|
], 401);
|
|
}
|
|
|
|
private function issueAccessToken(): string
|
|
{
|
|
do {
|
|
$token = Str::random(96);
|
|
} while (StoreClient::query()->where('access_token', $token)->exists());
|
|
|
|
return $token;
|
|
}
|
|
|
|
private function normalizeSiteUrl(string $url): string
|
|
{
|
|
$url = trim($url);
|
|
if ($url === '') {
|
|
return '';
|
|
}
|
|
|
|
return rtrim($url, '/');
|
|
}
|
|
|
|
private function truncate(string $value, int $length): string
|
|
{
|
|
return mb_substr(trim($value), 0, $length);
|
|
}
|
|
|
|
private function clientPayload(StoreClient $client, bool $includeToken): array
|
|
{
|
|
$payload = [
|
|
'client_id' => $client->id,
|
|
'site_url' => $client->site_url,
|
|
'site_name' => $client->site_name,
|
|
'status' => $client->status,
|
|
'user_count' => (int) $client->user_count,
|
|
'typecho_version' => $client->typecho_version,
|
|
'php_version' => $client->php_version,
|
|
'plugin_version' => $client->plugin_version,
|
|
'registered_at' => optional($client->registered_at)->toAtomString(),
|
|
'last_seen_at' => optional($client->last_seen_at)->toAtomString(),
|
|
'server_time' => now()->toAtomString(),
|
|
'stats' => [
|
|
'registered_sites' => StoreClient::query()->count(),
|
|
'online_sites' => StoreClient::query()->online()->count(),
|
|
'tracked_users' => (int) StoreClient::query()->sum('user_count'),
|
|
],
|
|
];
|
|
|
|
if ($includeToken) {
|
|
$payload['access_token'] = $client->access_token;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
}
|