本次提交包含大量代码优化、功能新增与服务端配置更新: 1. 修复分析报告统计数据,调整CMake策略设置 2. 优化APP权限配置、编辑器与聊天界面组件 3. 更新依赖库版本与pubspec配置 4. 新增文件传输服务端、信令服务器相关配置与脚本 5. 完善用户注销功能与数据库迁移脚本 6. 优化多处动画效果、代码风格与日志输出 7. 新增多种调试与部署脚本,修复已知BUG
754 lines
26 KiB
PHP
754 lines
26 KiB
PHP
<?php
|
|
|
|
namespace app\api\controller;
|
|
|
|
use app\common\controller\Api;
|
|
use think\Config;
|
|
use think\Cache;
|
|
use think\Db;
|
|
|
|
/**
|
|
* 文件传输助手 — 服务端API
|
|
* 创建时间: 2026-05-10
|
|
* 更新时间: 2026-05-10
|
|
* 作用: TURN凭据发放 + 设备配对管理 + 信令服务状态 + 房间管理 + 速率限制
|
|
* 兼容: LocalSend v2.1 协议 + 闲言 xianyan-v1 协议
|
|
* 参考: github.com/MatrixSeven/file-transfer-go (房间模型)
|
|
* 上次更新: 升级数据库持久化+速率限制+房间管理API
|
|
*/
|
|
class FileTransfer extends Api
|
|
{
|
|
protected $noNeedLogin = ['*'];
|
|
protected $noNeedRight = ['*'];
|
|
|
|
private $turnSecret = 'xianyan-turn-secret-2026';
|
|
private $turnRealm = 'tools.wktyl.com';
|
|
private $turnHost = '123.207.67.197';
|
|
private $turnPort = 3478;
|
|
private $turnTtl = 86400;
|
|
private $signalingUrl = 'ws://127.0.0.1:9443';
|
|
|
|
private $rateLimitMax = 60;
|
|
private $rateLimitWindow = 60;
|
|
|
|
public function _initialize()
|
|
{
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Device-Id, X-Protocol');
|
|
if ($this->request->method() === 'OPTIONS') {
|
|
http_response_code(204);
|
|
exit;
|
|
}
|
|
parent::_initialize();
|
|
$this->checkRateLimit();
|
|
}
|
|
|
|
// ─── 速率限制 ─────────────────────────────────────────
|
|
|
|
private function checkRateLimit()
|
|
{
|
|
$ip = $this->request->ip();
|
|
$key = 'rate_limit_ft_' . md5($ip);
|
|
$now = time();
|
|
$requests = Cache::get($key, []);
|
|
|
|
$requests = array_filter($requests, function ($t) use ($now) {
|
|
return ($now - $t) < $this->rateLimitWindow;
|
|
});
|
|
|
|
if (count($requests) >= $this->rateLimitMax) {
|
|
$this->error('请求过于频繁,请稍后再试', null, 429);
|
|
}
|
|
|
|
$requests[] = $now;
|
|
Cache::set($key, $requests, $this->rateLimitWindow);
|
|
}
|
|
|
|
// ─── TURN凭据 ─────────────────────────────────────────
|
|
|
|
/**
|
|
* 获取TURN凭据
|
|
* POST /api/file_transfer/turn_credentials
|
|
*/
|
|
public function turn_credentials()
|
|
{
|
|
$deviceId = $this->request->header('X-Device-Id', '');
|
|
$fingerprint = $this->request->post('fingerprint', '');
|
|
|
|
if (empty($deviceId) && empty($fingerprint)) {
|
|
$this->error('deviceId or fingerprint required', null, 400);
|
|
}
|
|
|
|
$ident = $deviceId ?: $fingerprint;
|
|
$timestamp = time() + $this->turnTtl;
|
|
$username = $timestamp . ':' . substr($ident, 0, 16);
|
|
$password = base64_encode(hash_hmac('sha1', $username, $this->turnSecret, true));
|
|
|
|
$stunServers = [
|
|
'stun:stun.l.google.com:19302',
|
|
'stun:stun1.l.google.com:19302',
|
|
];
|
|
|
|
$turnUrls = [];
|
|
if (!empty($this->turnHost)) {
|
|
$turnUrls = [
|
|
"turn:{$this->turnHost}:{$this->turnPort}?transport=udp",
|
|
"turn:{$this->turnHost}:{$this->turnPort}?transport=tcp",
|
|
];
|
|
}
|
|
|
|
$iceServers = [];
|
|
foreach ($stunServers as $url) {
|
|
$iceServers[] = ['urls' => $url];
|
|
}
|
|
foreach ($turnUrls as $url) {
|
|
$iceServers[] = [
|
|
'urls' => $url,
|
|
'username' => $username,
|
|
'credential' => $password,
|
|
];
|
|
}
|
|
|
|
$this->success('ok', [
|
|
'username' => $username,
|
|
'password' => $password,
|
|
'urls' => $turnUrls,
|
|
'ttl' => $this->turnTtl,
|
|
'iceServers' => $iceServers,
|
|
]);
|
|
}
|
|
|
|
// ─── 信令服务状态 ──────────────────────────────────────
|
|
|
|
/**
|
|
* 获取信令服务信息
|
|
* GET /api/file_transfer/signaling_info
|
|
*/
|
|
public function signaling_info()
|
|
{
|
|
$protocol = $this->request->header('X-Protocol', 'xianyan-v1');
|
|
$host = $this->request->host(true);
|
|
|
|
$this->success('ok', [
|
|
'signalingUrl' => "wss://{$host}:9443",
|
|
'signalingHttp' => "https://{$host}/api/file_transfer/",
|
|
'protocols' => ['xianyan-v1', 'localsend-v2'],
|
|
'defaultProtocol'=> 'xianyan-v1',
|
|
'serverTime' => time() * 1000,
|
|
'features' => [
|
|
'webrtc' => true,
|
|
'localsend' => true,
|
|
'tcpSocket' => true,
|
|
'usbTether' => true,
|
|
'bluetooth' => true,
|
|
'nfc' => true,
|
|
'qrCode' => true,
|
|
'hotspot' => true,
|
|
'relay' => true,
|
|
'roomCode' => true,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 信令服务健康检查
|
|
* GET /api/file_transfer/health
|
|
*/
|
|
public function health()
|
|
{
|
|
$signalingAlive = false;
|
|
$fp = @fsockopen('127.0.0.1', 9443, $errno, $errstr, 2);
|
|
if ($fp) {
|
|
$signalingAlive = true;
|
|
fclose($fp);
|
|
}
|
|
|
|
$dbAlive = false;
|
|
try {
|
|
Db::query('SELECT 1');
|
|
$dbAlive = true;
|
|
} catch (\Exception $e) {
|
|
$dbAlive = false;
|
|
}
|
|
|
|
$this->success('ok', [
|
|
'status' => ($signalingAlive && $dbAlive) ? 'healthy' : 'degraded',
|
|
'signalingAlive' => $signalingAlive,
|
|
'databaseAlive' => $dbAlive,
|
|
'serverTime' => time() * 1000,
|
|
'version' => '2.0.0',
|
|
]);
|
|
}
|
|
|
|
// ─── 配对管理 (数据库持久化) ────────────────────────────
|
|
|
|
/**
|
|
* 创建配对请求
|
|
* POST /api/file_transfer/pair_request
|
|
*/
|
|
public function pair_request()
|
|
{
|
|
$fromId = $this->request->post('fromId', '');
|
|
$toId = $this->request->post('toId', '');
|
|
$fingerprint = $this->request->post('fingerprint', '');
|
|
$alias = $this->request->post('alias', 'Unknown');
|
|
$deviceType = $this->request->post('deviceType', 'mobile');
|
|
|
|
if (empty($fromId) || empty($toId)) {
|
|
$this->error('fromId and toId required', null, 400);
|
|
}
|
|
|
|
$requestId = bin2hex(random_bytes(16));
|
|
$expiresAt = time() + 300;
|
|
|
|
try {
|
|
Db::name('file_transfer_pair_request')->insert([
|
|
'request_id' => $requestId,
|
|
'from_id' => $fromId,
|
|
'to_id' => $toId,
|
|
'fingerprint' => $fingerprint,
|
|
'alias' => $alias,
|
|
'device_type' => $deviceType,
|
|
'status' => 'pending',
|
|
'created_at' => time(),
|
|
'expires_at' => $expiresAt,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$this->fallbackPairRequest($requestId, $fromId, $toId, $fingerprint, $alias, $deviceType, $expiresAt);
|
|
}
|
|
|
|
$this->success('ok', [
|
|
'requestId' => $requestId,
|
|
'expiresAt' => $expiresAt * 1000,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 接受配对请求
|
|
* POST /api/file_transfer/pair_accept
|
|
*/
|
|
public function pair_accept()
|
|
{
|
|
$requestId = $this->request->post('requestId', '');
|
|
$deviceId = $this->request->post('deviceId', '');
|
|
|
|
if (empty($requestId) || empty($deviceId)) {
|
|
$this->error('requestId and deviceId required', null, 400);
|
|
}
|
|
|
|
try {
|
|
$req = Db::name('file_transfer_pair_request')
|
|
->where('request_id', $requestId)
|
|
->where('status', 'pending')
|
|
->find();
|
|
|
|
if (!$req) {
|
|
$this->error('request not found or expired', null, 404);
|
|
}
|
|
|
|
if (time() > $req['expires_at']) {
|
|
Db::name('file_transfer_pair_request')
|
|
->where('request_id', $requestId)
|
|
->update(['status' => 'expired']);
|
|
$this->error('request expired', null, 410);
|
|
}
|
|
|
|
Db::name('file_transfer_pair_request')
|
|
->where('request_id', $requestId)
|
|
->update(['status' => 'accepted']);
|
|
|
|
$fromId = $req['from_id'];
|
|
$toId = $req['to_id'];
|
|
$now = time();
|
|
|
|
Db::name('file_transfer_pair_record')->insertAll([
|
|
[
|
|
'device_id' => $fromId,
|
|
'peer_device_id' => $toId,
|
|
'fingerprint' => $req['fingerprint'],
|
|
'alias' => $req['alias'],
|
|
'paired_at' => $now,
|
|
'last_verified_at'=> $now,
|
|
'is_trusted' => 1,
|
|
],
|
|
[
|
|
'device_id' => $toId,
|
|
'peer_device_id' => $fromId,
|
|
'fingerprint' => $req['fingerprint'],
|
|
'alias' => $req['alias'],
|
|
'paired_at' => $now,
|
|
'last_verified_at'=> $now,
|
|
'is_trusted' => 1,
|
|
],
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
$this->fallbackPairAccept($requestId, $deviceId);
|
|
return;
|
|
}
|
|
|
|
$this->success('ok', ['paired' => true]);
|
|
}
|
|
|
|
/**
|
|
* 拒绝配对请求
|
|
* POST /api/file_transfer/pair_reject
|
|
*/
|
|
public function pair_reject()
|
|
{
|
|
$requestId = $this->request->post('requestId', '');
|
|
|
|
try {
|
|
Db::name('file_transfer_pair_request')
|
|
->where('request_id', $requestId)
|
|
->update(['status' => 'rejected']);
|
|
} catch (\Exception $e) {
|
|
$this->fallbackPairReject($requestId);
|
|
}
|
|
|
|
$this->success('ok');
|
|
}
|
|
|
|
/**
|
|
* 获取已配对设备列表
|
|
* GET /api/file_transfer/paired_devices
|
|
*/
|
|
public function paired_devices()
|
|
{
|
|
$deviceId = $this->request->param('deviceId', '');
|
|
if (empty($deviceId)) {
|
|
$this->error('deviceId required', null, 400);
|
|
}
|
|
|
|
$paired = [];
|
|
try {
|
|
$paired = Db::name('file_transfer_pair_record')
|
|
->where('device_id', $deviceId)
|
|
->select();
|
|
} catch (\Exception $e) {
|
|
$paired = $this->fallbackPairedDevices($deviceId);
|
|
}
|
|
|
|
$this->success('ok', ['devices' => $paired]);
|
|
}
|
|
|
|
/**
|
|
* 删除配对
|
|
* POST /api/file_transfer/pair_delete
|
|
*/
|
|
public function pair_delete()
|
|
{
|
|
$deviceId = $this->request->post('deviceId', '');
|
|
$peerDeviceId = $this->request->post('peerDeviceId', '');
|
|
|
|
if (empty($deviceId) || empty($peerDeviceId)) {
|
|
$this->error('deviceId and peerDeviceId required', null, 400);
|
|
}
|
|
|
|
try {
|
|
Db::name('file_transfer_pair_record')
|
|
->where('device_id', $deviceId)
|
|
->where('peer_device_id', $peerDeviceId)
|
|
->delete();
|
|
Db::name('file_transfer_pair_record')
|
|
->where('device_id', $peerDeviceId)
|
|
->where('peer_device_id', $deviceId)
|
|
->delete();
|
|
} catch (\Exception $e) {
|
|
$this->fallbackPairDelete($deviceId, $peerDeviceId);
|
|
}
|
|
|
|
$this->success('ok');
|
|
}
|
|
|
|
// ─── 房间管理 (参考 file-transfer-go) ──────────────────
|
|
|
|
/**
|
|
* 创建房间
|
|
* POST /api/file_transfer/create_room
|
|
*/
|
|
public function create_room()
|
|
{
|
|
$deviceId = $this->request->post('deviceId', '');
|
|
$alias = $this->request->post('alias', 'Unknown');
|
|
$deviceType = $this->request->post('deviceType', 'mobile');
|
|
|
|
$code = $this->generateRoomCode();
|
|
$expiresAt = time() + 3600;
|
|
|
|
try {
|
|
Db::name('file_transfer_room')->insert([
|
|
'room_code' => $code,
|
|
'creator_id' => $deviceId,
|
|
'alias' => $alias,
|
|
'device_type' => $deviceType,
|
|
'status' => 'waiting',
|
|
'created_at' => time(),
|
|
'expires_at' => $expiresAt,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$file = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'rooms.json';
|
|
$rooms = $this->readJsonFile($file);
|
|
$rooms[$code] = [
|
|
'room_code' => $code,
|
|
'creator_id' => $deviceId,
|
|
'alias' => $alias,
|
|
'device_type' => $deviceType,
|
|
'status' => 'waiting',
|
|
'created_at' => time(),
|
|
'expires_at' => $expiresAt,
|
|
];
|
|
$this->writeJsonFile($file, $rooms);
|
|
}
|
|
|
|
$this->success('ok', [
|
|
'code' => $code,
|
|
'expiresAt' => $expiresAt * 1000,
|
|
'createdAt' => time() * 1000,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 查询房间状态
|
|
* GET /api/file_transfer/room_status
|
|
*/
|
|
public function room_status()
|
|
{
|
|
$code = strtoupper($this->request->param('code', ''));
|
|
if (empty($code)) {
|
|
$this->error('code required', null, 400);
|
|
}
|
|
|
|
$room = null;
|
|
try {
|
|
$room = Db::name('file_transfer_room')
|
|
->where('room_code', $code)
|
|
->find();
|
|
} catch (\Exception $e) {
|
|
$file = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'rooms.json';
|
|
$rooms = $this->readJsonFile($file);
|
|
$room = $rooms[$code] ?? null;
|
|
}
|
|
|
|
if (!$room) {
|
|
$this->success('ok', [
|
|
'exists' => false,
|
|
'message' => '房间不存在或已过期',
|
|
]);
|
|
return;
|
|
}
|
|
|
|
if (time() > ($room['expires_at'] ?? 0)) {
|
|
$this->success('ok', [
|
|
'exists' => false,
|
|
'message' => '房间已过期',
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$this->success('ok', [
|
|
'exists' => true,
|
|
'status' => $room['status'] ?? 'waiting',
|
|
'senderOnline' => in_array($room['status'], ['waiting', 'connected']),
|
|
'receiverOnline' => $room['status'] === 'connected',
|
|
'isFull' => $room['status'] === 'connected',
|
|
'createdAt' => ($room['created_at'] ?? 0) * 1000,
|
|
'expiresAt' => ($room['expires_at'] ?? 0) * 1000,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 加入房间
|
|
* POST /api/file_transfer/join_room
|
|
*/
|
|
public function join_room()
|
|
{
|
|
$code = strtoupper($this->request->post('code', ''));
|
|
$role = $this->request->post('role', '');
|
|
$deviceId = $this->request->post('deviceId', '');
|
|
|
|
if (empty($code) || !in_array($role, ['sender', 'receiver'])) {
|
|
$this->error('code and role(sender/receiver) required', null, 400);
|
|
}
|
|
|
|
$room = null;
|
|
try {
|
|
$room = Db::name('file_transfer_room')
|
|
->where('room_code', $code)
|
|
->find();
|
|
} catch (\Exception $e) {
|
|
$file = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'rooms.json';
|
|
$rooms = $this->readJsonFile($file);
|
|
$room = $rooms[$code] ?? null;
|
|
}
|
|
|
|
if (!$room) {
|
|
$this->error('房间不存在或已过期', null, 404);
|
|
}
|
|
|
|
if (time() > ($room['expires_at'] ?? 0)) {
|
|
$this->error('房间已过期', null, 410);
|
|
}
|
|
|
|
if (($room['status'] ?? '') === 'connected') {
|
|
$this->error('房间已满,正在传输中无法加入', null, 403);
|
|
}
|
|
|
|
try {
|
|
if ($role === 'sender') {
|
|
Db::name('file_transfer_room')
|
|
->where('room_code', $code)
|
|
->update(['sender_id' => $deviceId, 'status' => 'waiting']);
|
|
} else {
|
|
Db::name('file_transfer_room')
|
|
->where('room_code', $code)
|
|
->update(['receiver_id' => $deviceId, 'status' => 'connected']);
|
|
}
|
|
} catch (\Exception $e) {
|
|
$file = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'rooms.json';
|
|
$rooms = $this->readJsonFile($file);
|
|
if (isset($rooms[$code])) {
|
|
if ($role === 'sender') {
|
|
$rooms[$code]['sender_id'] = $deviceId;
|
|
$rooms[$code]['status'] = 'waiting';
|
|
} else {
|
|
$rooms[$code]['receiver_id'] = $deviceId;
|
|
$rooms[$code]['status'] = 'connected';
|
|
}
|
|
$this->writeJsonFile($file, $rooms);
|
|
}
|
|
}
|
|
|
|
$this->success('ok', [
|
|
'joined' => true,
|
|
'code' => $code,
|
|
'role' => $role,
|
|
]);
|
|
}
|
|
|
|
// ─── LocalSend兼容端点 ─────────────────────────────────
|
|
|
|
/**
|
|
* LocalSend设备信息 (兼容)
|
|
* GET /api/file_transfer/localsend_info
|
|
*/
|
|
public function localsend_info()
|
|
{
|
|
$this->success('ok', [
|
|
'alias' => '闲言传输服务',
|
|
'version' => '2.1',
|
|
'deviceModel' => 'Server',
|
|
'deviceType' => 'server',
|
|
'fingerprint' => hash('sha256', 'xianyan-server-' . $this->turnSecret),
|
|
'download' => false,
|
|
'protocol' => 'https',
|
|
'port' => 53317,
|
|
]);
|
|
}
|
|
|
|
// ─── 数据库安装 ────────────────────────────────────────
|
|
|
|
/**
|
|
* 安装/升级数据库表
|
|
* POST /api/file_transfer/install
|
|
*/
|
|
public function install()
|
|
{
|
|
$prefix = Config::get('database.prefix');
|
|
|
|
$sqls = [];
|
|
|
|
$sqls[] = "CREATE TABLE IF NOT EXISTS `{$prefix}file_transfer_pair_request` (
|
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
|
`request_id` varchar(64) NOT NULL DEFAULT '' COMMENT '请求ID',
|
|
`from_id` varchar(128) NOT NULL DEFAULT '' COMMENT '发起方设备ID',
|
|
`to_id` varchar(128) NOT NULL DEFAULT '' COMMENT '目标设备ID',
|
|
`fingerprint` varchar(256) NOT NULL DEFAULT '' COMMENT '设备指纹',
|
|
`alias` varchar(128) NOT NULL DEFAULT 'Unknown' COMMENT '设备别名',
|
|
`device_type` varchar(32) NOT NULL DEFAULT 'mobile' COMMENT '设备类型',
|
|
`status` enum('pending','accepted','rejected','expired') NOT NULL DEFAULT 'pending',
|
|
`created_at` int(11) unsigned NOT NULL DEFAULT 0,
|
|
`expires_at` int(11) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `uk_request_id` (`request_id`),
|
|
KEY `idx_from_id` (`from_id`),
|
|
KEY `idx_to_id` (`to_id`),
|
|
KEY `idx_status` (`status`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件传输-配对请求'";
|
|
|
|
$sqls[] = "CREATE TABLE IF NOT EXISTS `{$prefix}file_transfer_pair_record` (
|
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
|
`device_id` varchar(128) NOT NULL DEFAULT '' COMMENT '设备ID',
|
|
`peer_device_id` varchar(128) NOT NULL DEFAULT '' COMMENT '对端设备ID',
|
|
`fingerprint` varchar(256) NOT NULL DEFAULT '' COMMENT '对端指纹',
|
|
`alias` varchar(128) NOT NULL DEFAULT '' COMMENT '对端别名',
|
|
`paired_at` int(11) unsigned NOT NULL DEFAULT 0,
|
|
`last_verified_at` int(11) unsigned NOT NULL DEFAULT 0,
|
|
`is_trusted` tinyint(1) NOT NULL DEFAULT 1,
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `uk_pair` (`device_id`, `peer_device_id`),
|
|
KEY `idx_device_id` (`device_id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件传输-配对记录'";
|
|
|
|
$sqls[] = "CREATE TABLE IF NOT EXISTS `{$prefix}file_transfer_room` (
|
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
|
`room_code` varchar(16) NOT NULL DEFAULT '' COMMENT '房间码',
|
|
`creator_id` varchar(128) NOT NULL DEFAULT '' COMMENT '创建者设备ID',
|
|
`sender_id` varchar(128) NOT NULL DEFAULT '' COMMENT '发送方设备ID',
|
|
`receiver_id` varchar(128) NOT NULL DEFAULT '' COMMENT '接收方设备ID',
|
|
`alias` varchar(128) NOT NULL DEFAULT '' COMMENT '创建者别名',
|
|
`device_type` varchar(32) NOT NULL DEFAULT 'mobile' COMMENT '设备类型',
|
|
`status` enum('waiting','connected','completed','expired') NOT NULL DEFAULT 'waiting',
|
|
`created_at` int(11) unsigned NOT NULL DEFAULT 0,
|
|
`expires_at` int(11) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `uk_room_code` (`room_code`),
|
|
KEY `idx_creator` (`creator_id`),
|
|
KEY `idx_status` (`status`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件传输-房间'";
|
|
|
|
$results = [];
|
|
foreach ($sqls as $sql) {
|
|
try {
|
|
Db::execute($sql);
|
|
$results[] = 'ok';
|
|
} catch (\Exception $e) {
|
|
$results[] = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
$this->success('ok', [
|
|
'tables' => count($sqls),
|
|
'results' => $results,
|
|
]);
|
|
}
|
|
|
|
// ─── 文件缓存降级方案 ──────────────────────────────────
|
|
|
|
private function fallbackPairRequest($requestId, $fromId, $toId, $fingerprint, $alias, $deviceType, $expiresAt)
|
|
{
|
|
$file = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'pair_requests.json';
|
|
$requests = $this->readJsonFile($file);
|
|
$requests[$requestId] = [
|
|
'id' => $requestId,
|
|
'fromId' => $fromId,
|
|
'toId' => $toId,
|
|
'fingerprint' => $fingerprint,
|
|
'alias' => $alias,
|
|
'deviceType' => $deviceType,
|
|
'createdAt' => time(),
|
|
'expiresAt' => $expiresAt,
|
|
];
|
|
$this->writeJsonFile($file, $requests);
|
|
}
|
|
|
|
private function fallbackPairAccept($requestId, $deviceId)
|
|
{
|
|
$reqFile = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'pair_requests.json';
|
|
$requests = $this->readJsonFile($reqFile);
|
|
if (!isset($requests[$requestId])) {
|
|
$this->error('request not found or expired', null, 404);
|
|
}
|
|
$req = $requests[$requestId];
|
|
if (time() > $req['expiresAt']) {
|
|
unset($requests[$requestId]);
|
|
$this->writeJsonFile($reqFile, $requests);
|
|
$this->error('request expired', null, 410);
|
|
}
|
|
unset($requests[$requestId]);
|
|
$this->writeJsonFile($reqFile, $requests);
|
|
|
|
$pairFile = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'pair_records.json';
|
|
$records = $this->readJsonFile($pairFile);
|
|
$fromId = $req['fromId'];
|
|
$toId = $req['toId'];
|
|
$now = time();
|
|
$records[$fromId . ':' . $toId] = [
|
|
'deviceId' => $fromId,
|
|
'peerDeviceId' => $toId,
|
|
'fingerprint' => $req['fingerprint'],
|
|
'pairedAt' => $now,
|
|
'lastVerifiedAt' => $now,
|
|
'isTrusted' => true,
|
|
];
|
|
$records[$toId . ':' . $fromId] = [
|
|
'deviceId' => $toId,
|
|
'peerDeviceId' => $fromId,
|
|
'fingerprint' => $req['fingerprint'],
|
|
'pairedAt' => $now,
|
|
'lastVerifiedAt' => $now,
|
|
'isTrusted' => true,
|
|
];
|
|
$this->writeJsonFile($pairFile, $records);
|
|
$this->success('ok', ['paired' => true]);
|
|
}
|
|
|
|
private function fallbackPairReject($requestId)
|
|
{
|
|
$reqFile = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'pair_requests.json';
|
|
$requests = $this->readJsonFile($reqFile);
|
|
if (isset($requests[$requestId])) {
|
|
unset($requests[$requestId]);
|
|
$this->writeJsonFile($reqFile, $requests);
|
|
}
|
|
}
|
|
|
|
private function fallbackPairedDevices($deviceId)
|
|
{
|
|
$pairFile = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'pair_records.json';
|
|
$records = $this->readJsonFile($pairFile);
|
|
$paired = [];
|
|
foreach ($records as $key => $rec) {
|
|
if ($rec['deviceId'] === $deviceId) {
|
|
$paired[] = $rec;
|
|
}
|
|
}
|
|
return $paired;
|
|
}
|
|
|
|
private function fallbackPairDelete($deviceId, $peerDeviceId)
|
|
{
|
|
$pairFile = RUNTIME_PATH . 'file_transfer' . DIRECTORY_SEPARATOR . 'pair_records.json';
|
|
$records = $this->readJsonFile($pairFile);
|
|
unset($records[$deviceId . ':' . $peerDeviceId]);
|
|
unset($records[$peerDeviceId . ':' . $deviceId]);
|
|
$this->writeJsonFile($pairFile, $records);
|
|
}
|
|
|
|
// ─── 工具方法 ──────────────────────────────────────────
|
|
|
|
private function generateRoomCode()
|
|
{
|
|
$chars = '123456789ABCDEFGHIJKLMNPQRSTUVWXYZ';
|
|
$code = '';
|
|
for ($i = 0; $i < 6; $i++) {
|
|
$code .= $chars[random_int(0, strlen($chars) - 1)];
|
|
}
|
|
return $code;
|
|
}
|
|
|
|
private function readJsonFile(string $path): array
|
|
{
|
|
if (!is_file($path)) {
|
|
return [];
|
|
}
|
|
$content = file_get_contents($path);
|
|
$data = json_decode($content, true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
private function writeJsonFile(string $path, array $data): void
|
|
{
|
|
$dir = dirname($path);
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
file_put_contents(
|
|
$path,
|
|
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
|
|
LOCK_EX
|
|
);
|
|
}
|
|
}
|