feat: 新增文件传输助手功能及相关组件

新增文件传输助手功能,包含设备发现、配对、传输等核心模块。主要变更包括:
1. 新增局域网、蓝牙、NFC等多种设备发现方式
2. 实现基于WebRTC、TCP、USB等多种传输协议
3. 添加相关权限管理及状态监控
4. 完善UI界面及交互流程
5. 更新依赖库及版本号至4.19.0

同时优化部分现有功能:
1. 聊天会话增加隐藏功能
2. 完善本地通知权限处理
3. 修复部分已知问题
This commit is contained in:
Developer
2026-05-10 02:48:52 +08:00
parent 41a60b0288
commit 72f64f9ca9
458 changed files with 56803 additions and 72785 deletions

View File

@@ -14,12 +14,12 @@ use think\Validate;
* 用户安全接口
* @time 2026-04-29
* @name UserSecurity
* @description 用户安全相关API含注册/登录/改密/改邮箱/重置密码/回执登录等
* @lastUpdate v8.0.0 去除邮箱验证码依赖引入HMAC-SHA256回执验证机制新增receiptLogin
* @description 用户安全相关API含注册/登录/改密/改邮箱/重置密码/回执登录/二维码登录
* @lastUpdate v9.0.0 新增qrcodeLogin二维码登录; login/receiptLogin记录设备信息和在线状态
*/
class UserSecurity extends Api
{
protected $noNeedLogin = ['login', 'mobilelogin', 'register', 'resetpwd', 'tokenLogin', 'receiptLogin', 'sendEms', 'checkEms'];
protected $noNeedLogin = ['login', 'mobilelogin', 'register', 'resetpwd', 'tokenLogin', 'receiptLogin', 'sendEms', 'checkEms', 'qrcodeGenerate', 'qrcodePoll', 'qrcodeCancel'];
protected $noNeedRight = '*';
private static $rateLimitKey = 'api_rate_limit:';
@@ -131,7 +131,8 @@ class UserSecurity extends Api
/**
* @name 账号密码登录
* @desc 支持用户名/邮箱+密码登录,无需回执
* @desc 支持用户名/邮箱+密码登录,无需回执,登录后记录设备信息
* @lastUpdate v9.0.0 登录后记录设备信息和在线状态
*/
public function login()
{
@@ -146,6 +147,8 @@ class UserSecurity extends Api
$this->detectMaliciousInput($account);
$ret = $this->auth->login($account, $password);
if ($ret) {
$this->recordLoginDevice($this->auth->id);
$this->updateOnlineStatus($this->auth->id);
$data = ['userinfo' => $this->auth->getUserinfo()];
$this->success(__('Logged in successful'), $data);
} else {
@@ -156,7 +159,7 @@ class UserSecurity extends Api
/**
* @name 回执登录
* @desc 客户端验证邮箱/手机后,用回执替代密码登录
* @since v8.0.0
* @lastUpdate v9.0.0 登录后记录设备信息和在线状态
*/
public function receiptLogin()
{
@@ -194,6 +197,8 @@ class UserSecurity extends Api
$ret = $this->auth->direct($user->id);
if ($ret) {
$this->recordLoginDevice($user->id);
$this->updateOnlineStatus($user->id);
$data = ['userinfo' => $this->auth->getUserinfo()];
$this->success(__('Logged in successful'), $data);
} else {
@@ -593,4 +598,227 @@ class UserSecurity extends Api
}
$this->error(__('Operation failed'), $url);
}
/**
* @name 生成二维码
* @desc 生成二维码登录标识返回code用于轮询
* @lastUpdate v9.0.0 新增
*/
public function qrcodeGenerate()
{
$code = md5(uniqid('qr', true) . mt_rand() . time());
$expireTime = time() + 300;
db('qrcode_login')->insert([
'code' => $code,
'status' => 'pending',
'user_id' => 0,
'token' => '',
'ip' => $this->request->ip(),
'expire_time' => $expireTime,
'createtime' => time(),
'updatetime' => time(),
]);
$this->success('二维码已生成', [
'code' => $code,
'expire_time' => $expireTime,
'expire_seconds' => 300,
'qrcode_url' => $this->request->domain() . '/qrcode?code=' . $code,
]);
}
/**
* @name 扫码确认
* @desc 已登录用户扫码后确认登录,需登录
* @lastUpdate v9.0.0 新增
*/
public function qrcodeConfirm()
{
$code = $this->request->post('code', '', 'trim');
if (!$code) {
$this->error('code必填');
}
$qr = db('qrcode_login')->where('code', $code)->find();
if (!$qr) {
$this->error('二维码不存在');
}
if ($qr['status'] !== 'pending' && $qr['status'] !== 'scanned') {
$this->error('二维码状态无效');
}
if ($qr['expire_time'] < time()) {
db('qrcode_login')->where('code', $code)->update(['status' => 'expired', 'updatetime' => time()]);
$this->error('二维码已过期');
}
$userId = $this->auth->id;
$deviceInfo = json_encode([
'ip' => $this->request->ip(),
'platform' => $this->request->post('platform', '', 'trim'),
'device_name' => $this->request->post('device_name', '', 'trim'),
'app_name' => $this->request->post('app_name', '', 'trim'),
]);
db('qrcode_login')->where('code', $code)->update([
'status' => 'confirmed',
'user_id' => $userId,
'device_info' => $deviceInfo,
'updatetime' => time(),
]);
$this->success('确认登录成功');
}
/**
* @name 轮询二维码状态
* @desc 客户端轮询二维码状态confirmed时返回Token
* @lastUpdate v9.0.0 新增
*/
public function qrcodePoll()
{
$code = $this->request->param('code', '', 'trim');
if (!$code) {
$this->error('code必填');
}
$qr = db('qrcode_login')->where('code', $code)->find();
if (!$qr) {
$this->error('二维码不存在');
}
if ($qr['expire_time'] < time() && $qr['status'] !== 'confirmed') {
db('qrcode_login')->where('code', $code)->update(['status' => 'expired', 'updatetime' => time()]);
$this->success('', ['status' => 'expired', 'message' => '二维码已过期']);
}
if ($qr['status'] === 'pending') {
$this->success('', ['status' => 'pending', 'message' => '等待扫码']);
}
if ($qr['status'] === 'scanned') {
$this->success('', ['status' => 'scanned', 'message' => '已扫码,等待确认']);
}
if ($qr['status'] === 'expired') {
$this->success('', ['status' => 'expired', 'message' => '二维码已过期']);
}
if ($qr['status'] === 'cancelled') {
$this->success('', ['status' => 'cancelled', 'message' => '已取消']);
}
if ($qr['status'] === 'confirmed') {
$user = \app\common\model\User::get($qr['user_id']);
if (!$user || $user->status != 'normal') {
$this->error('用户异常');
}
$ret = $this->auth->direct($user->id);
if ($ret) {
$token = $this->auth->getToken();
db('qrcode_login')->where('code', $code)->update([
'token' => $token,
'updatetime' => time(),
]);
$this->recordLoginDevice($user->id);
$this->updateOnlineStatus($user->id);
$this->success('登录成功', [
'status' => 'confirmed',
'token' => $token,
'userinfo' => $this->auth->getUserinfo(),
]);
} else {
$this->error('登录失败');
}
}
$this->error('未知状态');
}
/**
* @name 取消二维码
* @desc 取消二维码登录
* @lastUpdate v9.0.0 新增
*/
public function qrcodeCancel()
{
$code = $this->request->post('code', '', 'trim');
if (!$code) {
$this->error('code必填');
}
$qr = db('qrcode_login')->where('code', $code)->find();
if (!$qr) {
$this->error('二维码不存在');
}
db('qrcode_login')->where('code', $code)->update(['status' => 'cancelled', 'updatetime' => time()]);
$this->success('已取消');
}
/**
* @name 记录登录设备
* @desc 登录成功后记录设备信息到user_device表
* @lastUpdate v9.0.0 新增
*/
private function recordLoginDevice($userId)
{
$deviceName = $this->request->post('device_name', '', 'trim');
$deviceModel = $this->request->post('device_model', '', 'trim');
$platform = $this->request->post('platform', '', 'trim');
$appName = $this->request->post('app_name', '', 'trim');
$deviceId = $this->request->post('device_id', '', 'trim');
$userAgent = $this->request->header('user-agent', '');
$ip = $this->request->ip();
$now = time();
if ($deviceId) {
$exists = db('user_device')->where('user_id', $userId)->where('device_id', $deviceId)->find();
if ($exists) {
db('user_device')->where('id', $exists['id'])->update([
'device_name' => $deviceName ?: $exists['device_name'],
'device_model' => $deviceModel ?: $exists['device_model'],
'platform' => $platform ?: $exists['platform'],
'app_name' => $appName ?: $exists['app_name'],
'ip' => $ip,
'last_active_time' => $now,
'is_online' => 1,
'user_agent' => substr($userAgent, 0, 500),
'updatetime' => $now,
]);
return;
}
}
db('user_device')->insert([
'user_id' => $userId,
'device_name' => $deviceName,
'device_model' => $deviceModel,
'platform' => $platform,
'app_name' => $appName,
'ip' => $ip,
'device_id' => $deviceId ?: md5($userId . $ip . $userAgent . $now),
'last_active_time' => $now,
'is_online' => 1,
'user_agent' => substr($userAgent, 0, 500),
'createtime' => $now,
'updatetime' => $now,
]);
}
/**
* @name 更新在线状态
* @desc 更新用户最后活跃时间和在线状态
* @lastUpdate v9.0.0 新增
*/
private function updateOnlineStatus($userId)
{
db('user')->where('id', $userId)->update([
'last_active_time' => time(),
'is_online' => 1,
]);
}
}