Files
xianyan/docs/toolsapi/application/api/controller/UserSecurity.php
Developer 283950ea07 chore: 批量代码优化与功能迭代更新
本次提交包含大量代码优化、功能新增与服务端配置更新:
1. 修复分析报告统计数据,调整CMake策略设置
2. 优化APP权限配置、编辑器与聊天界面组件
3. 更新依赖库版本与pubspec配置
4. 新增文件传输服务端、信令服务器相关配置与脚本
5. 完善用户注销功能与数据库迁移脚本
6. 优化多处动画效果、代码风格与日志输出
7. 新增多种调试与部署脚本,修复已知BUG
2026-05-12 06:28:04 +08:00

1033 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\api\controller;
use app\common\controller\Api;
use app\common\library\Receipt;
use app\common\library\Ems;
use app\common\library\Sms;
use fast\Random;
use think\Config;
use think\Validate;
/**
* 用户安全接口
* @time 2026-04-29
* @name UserSecurity
* @description 用户安全相关API含注册/登录/改密/改邮箱/重置密码/回执登录/二维码登录等
* @lastUpdate v9.0.0 新增qrcodeLogin二维码登录; login/receiptLogin记录设备信息和在线状态
*/
class UserSecurity extends Api
{
protected $noNeedLogin = ['login', 'mobilelogin', 'register', 'resetpwd', 'tokenLogin', 'receiptLogin', 'sendEms', 'checkEms', 'qrcodeGenerate', 'qrcodePoll', 'qrcodeCancel'];
protected $noNeedRight = '*';
private static $rateLimitKey = 'api_rate_limit:';
private static $rateLimits = [
'login' => ['max' => 50, 'window' => 300],
'register' => ['max' => 30, 'window' => 3600],
'resetpwd' => ['max' => 20, 'window' => 3600],
'changepwd' => ['max' => 30, 'window' => 3600],
'changeemail' => ['max' => 30, 'window' => 3600],
'changemobile' => ['max' => 30, 'window' => 3600],
'sendEms' => ['max' => 30, 'window' => 300],
'tokenLogin' => ['max' => 80, 'window' => 300],
'receiptLogin' => ['max' => 50, 'window' => 300],
'requestDeletion' => ['max' => 5, 'window' => 3600],
'cancelDeletion' => ['max' => 10, 'window' => 3600],
'deletionStatus' => ['max' => 60, 'window' => 60],
];
private static $testMode = false;
public function _initialize()
{
parent::_initialize();
if (!Config::get('fastadmin.usercenter')) {
$this->error(__('User center already closed'));
}
self::$testMode = Config::get('test_mode') ?: false;
}
private function checkRateLimit($action)
{
if (!isset(self::$rateLimits[$action])) {
return true;
}
$config = self::$rateLimits[$action];
$key = self::$rateLimitKey . $action . ':' . $this->request->ip();
$cache = cache($key);
$count = $cache ? intval($cache) : 0;
if ($count >= $config['max']) {
$this->error('请求过于频繁,请稍后再试', null, ['retry_after' => $config['window']]);
}
cache($key, $count + 1, $config['window']);
return true;
}
private function sanitizeString($value, $maxLength = 255)
{
if (!is_string($value)) {
return '';
}
$value = trim($value);
$value = strip_tags($value);
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
if (mb_strlen($value) > $maxLength) {
$value = mb_substr($value, 0, $maxLength);
}
return $value;
}
private function validateLength($value, $field, $min = 1, $max = 255)
{
$len = mb_strlen($value);
if ($len < $min || $len > $max) {
$this->error("{$field}长度须在{$min}-{$max}之间");
}
return true;
}
private function detectMaliciousInput($value)
{
$patterns = [
'/<script\b[^>]*>/i',
'/javascript\s*:/i',
'/on\w+\s*=/i',
'/union\s+select/i',
'/insert\s+into/i',
'/delete\s+from/i',
'/update\s+\w+\s+set/i',
'/drop\s+table/i',
'/\.\.[\/\\\\]/i',
'/eval\s*\(/i',
'/exec\s*\(/i',
'/system\s*\(/i',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $value)) {
$this->error('输入内容包含非法字符');
}
}
return true;
}
private function isTestUser($userId)
{
if (!self::$testMode) {
return false;
}
$user = db('user')->where('id', $userId)->find();
return $user && isset($user['is_test']) && $user['is_test'] == 1;
}
private function verifyReceipt($action, $payloadStr)
{
$receipt = $this->request->post('receipt', '', 'trim');
$sig = $this->request->post('sig', '', 'trim');
$result = Receipt::verify($receipt, $sig, $action, $payloadStr);
if (!$result['valid']) {
$this->error($result['msg']);
}
return true;
}
/**
* @name 账号密码登录
* @desc 支持用户名/邮箱+密码登录,无需回执,登录后记录设备信息
* @lastUpdate v9.0.0 登录后记录设备信息和在线状态
*/
public function login()
{
$this->checkRateLimit('login');
$account = $this->request->post('account', '', 'trim');
$password = $this->request->post('password', '', 'trim');
if (!$account || !$password) {
$this->error(__('Invalid parameters'));
}
$this->validateLength($account, '账号', 2, 50);
$this->validateLength($password, '密码', 6, 30);
$this->detectMaliciousInput($account);
$ret = $this->auth->login($account, $password);
if ($ret) {
$this->recordLoginDevice($this->auth->id);
$this->updateOnlineStatus($this->auth->id);
$token = $this->auth->getToken();
$data = ['userinfo' => $this->auth->getUserinfo(), 'token' => $token];
$this->success(__('Logged in successful'), $data);
} else {
$this->error($this->auth->getError());
}
}
/**
* @name 回执登录
* @desc 客户端验证邮箱/手机后,用回执替代密码登录
* @lastUpdate v9.0.0 登录后记录设备信息和在线状态
*/
public function receiptLogin()
{
$this->checkRateLimit('receiptLogin');
$account = $this->request->post('account', '', 'trim');
$receipt = $this->request->post('receipt', '', 'trim');
$sig = $this->request->post('sig', '', 'trim');
if (!$account || !$receipt || !$sig) {
$this->error(__('Invalid parameters'));
}
$this->validateLength($account, '账号', 2, 100);
$this->detectMaliciousInput($account);
$result = Receipt::verifyFlexible($receipt, $sig, 'receipt_login', $account);
if (!$result['valid']) {
$this->error($result['msg']);
}
$user = null;
if (Validate::is($account, "email")) {
$user = \app\common\model\User::getByEmail($account);
} elseif (Validate::regex($account, "^1\d{10}$")) {
$user = \app\common\model\User::getByMobile($account);
} else {
$user = \app\common\model\User::getByUsername($account);
}
if (!$user) {
$this->error(__('User not found'));
}
if ($user->status != 'normal') {
$this->error(__('Account is locked'));
}
$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 {
$this->error($this->auth->getError());
}
}
/**
* @name 手机号登录
* @desc 手机号+短信验证码登录,未注册自动注册
*/
public function mobilelogin()
{
$this->checkRateLimit('login');
$mobile = $this->request->post('mobile', '', 'trim');
$captcha = $this->request->post('captcha', '', 'trim');
if (!$mobile || !$captcha) {
$this->error(__('Invalid parameters'));
}
if (!Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
$this->validateLength($captcha, '验证码', 4, 6);
if (self::$testMode && $captcha === '888888') {
$verified = true;
} else {
$verified = false;
try {
$verified = Sms::check($mobile, $captcha, 'mobilelogin');
} catch (\Exception $e) {
$this->error('短信验证服务暂不可用,请使用邮箱登录');
}
}
if (!$verified) {
$this->error(__('Captcha is incorrect'));
}
$user = \app\common\model\User::getByMobile($mobile);
if ($user) {
if ($user->status != 'normal') {
$this->error(__('Account is locked'));
}
$ret = $this->auth->direct($user->id);
} else {
$ret = $this->auth->register($mobile, Random::alnum(), '', $mobile, []);
}
if ($ret) {
try { Sms::flush($mobile, 'mobilelogin'); } catch (\Exception $e) {}
$data = ['userinfo' => $this->auth->getUserinfo()];
$this->success(__('Logged in successful'), $data);
} else {
$this->error($this->auth->getError());
}
}
/**
* @name Token令牌登录
* @desc 使用已有Token直接登录
*/
public function tokenLogin()
{
$this->checkRateLimit('tokenLogin');
$token = $this->request->post('token', '', 'trim');
if (!$token) {
$this->error(__('Invalid parameters'));
}
$this->validateLength($token, 'Token', 10, 128);
$tokenInfo = \app\common\library\Token::get($token);
if (!$tokenInfo) {
$this->error('Token无效或已过期');
}
$ret = $this->auth->direct($tokenInfo['user_id']);
if ($ret) {
$data = ['userinfo' => $this->auth->getUserinfo()];
$this->success(__('Logged in successful'), $data);
} else {
$this->error($this->auth->getError());
}
}
/**
* @name 用户注册
* @desc 注册新用户,需回执验证(客户端已验证邮箱),不再需要邮箱验证码
*/
public function register()
{
$this->checkRateLimit('register');
$username = $this->request->post('username', '', 'trim');
$password = $this->request->post('password', '', 'trim');
$email = $this->request->post('email', '', 'trim');
$mobile = $this->request->post('mobile', '', 'trim');
$mobileCode = $this->request->post('mobile_code', '', 'trim');
if (!$username || !$password || !$email) {
$this->error('用户名、密码和邮箱为必填项');
}
$this->validateLength($username, '用户名', 3, 30);
$this->validateLength($password, '密码', 6, 30);
$this->detectMaliciousInput($username);
if (!preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u', $username)) {
$this->error('用户名只能包含字母、数字、下划线和中文');
}
if (!Validate::is($email, "email")) {
$this->error(__('Email is incorrect'));
}
$this->validateLength($email, '邮箱', 5, 100);
if (\app\common\model\User::getByEmail($email)) {
$this->error('该邮箱已被注册');
}
$this->verifyReceipt('register', $email);
if ($mobile) {
if (!Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
if ($mobileCode) {
if (!Sms::check($mobile, $mobileCode, 'register')) {
$this->error(__('Captcha is incorrect'));
}
}
}
$ret = $this->auth->register($username, $password, $email, $mobile, []);
if ($ret) {
$userId = $this->auth->id;
$verification = db('user')->where('id', $userId)->value('verification');
$verification = $verification ? json_decode($verification, true) : [];
$verification['email'] = 1;
db('user')->where('id', $userId)->update(['verification' => json_encode($verification)]);
$token = $this->auth->getToken();
$data = ['userinfo' => $this->auth->getUserinfo(), 'token' => $token];
$this->success(__('Sign up successful'), $data);
} else {
$this->error($this->auth->getError());
}
}
/**
* @name 退出登录
* @desc 清除Token退出登录状态
*/
public function logout()
{
if (!$this->request->isPost()) {
$this->error(__('Invalid parameters'));
}
$token = $this->request->header('token', '');
if ($token) {
\app\common\library\Token::delete($token);
}
$userId = $this->auth->id;
if ($userId) {
\app\common\library\Token::clear($userId);
}
$this->success(__('Logout successful'));
}
/**
* @name 修改密码
* @desc 修改密码需验证旧密码+回执验证(客户端已验证邮箱)
*/
public function changepwd()
{
$this->checkRateLimit('changepwd');
$user = $this->auth->getUser();
$oldpassword = $this->request->post('oldpassword', '', 'trim');
$newpassword = $this->request->post('newpassword', '', 'trim');
if (!$oldpassword || !$newpassword) {
$this->error(__('Invalid parameters'));
}
$this->validateLength($oldpassword, '旧密码', 6, 30);
$this->validateLength($newpassword, '新密码', 6, 30);
if (!$this->isTestUser($user->id)) {
$email = $user->email;
if (!$email) {
$this->error('请先绑定邮箱后再修改密码');
}
$this->verifyReceipt('changepwd', $user->username);
}
$ret = $this->auth->changepwd($newpassword, $oldpassword);
if ($ret) {
$this->success(__('Change password successful'));
} else {
$this->error($this->auth->getError());
}
}
/**
* @name 重置密码
* @desc 通过回执验证重置密码(无需登录),客户端已验证邮箱/手机
*/
public function resetpwd()
{
$this->checkRateLimit('resetpwd');
$type = $this->request->post("type", "email", 'trim');
$mobile = $this->request->post("mobile", '', 'trim');
$email = $this->request->post("email", '', 'trim');
$newpassword = $this->request->post("newpassword", '', 'trim');
if (!$newpassword) {
$this->error(__('Invalid parameters'));
}
$this->validateLength($newpassword, '新密码', 6, 30);
if ($type == 'mobile') {
if (!Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
$user = \app\common\model\User::getByMobile($mobile);
if (!$user) {
$this->error(__('User not found'));
}
$this->verifyReceipt('resetpwd', $mobile);
} else {
if (!Validate::is($email, "email")) {
$this->error(__('Email is incorrect'));
}
$user = \app\common\model\User::getByEmail($email);
if (!$user) {
$this->error(__('User not found'));
}
$this->verifyReceipt('resetpwd', $email);
}
$this->auth->direct($user->id);
$ret = $this->auth->changepwd($newpassword, '', true);
if ($ret) {
$this->success(__('Reset password successful'));
} else {
$this->error($this->auth->getError());
}
}
/**
* @name 修改邮箱
* @desc 修改邮箱需回执验证(客户端已验证新邮箱)
*/
public function changeemail()
{
$this->checkRateLimit('changeemail');
$user = $this->auth->getUser();
$email = $this->request->post('email', '', 'trim');
if (!$email) {
$this->error(__('Invalid parameters'));
}
if (!Validate::is($email, "email")) {
$this->error(__('Email is incorrect'));
}
$this->validateLength($email, '邮箱', 5, 100);
if (\app\common\model\User::where('email', $email)->where('id', '<>', $user->id)->find()) {
$this->error(__('Email already exists'));
}
$this->verifyReceipt('changeemail', $email);
$verification = $user->verification;
$verification->email = 1;
$user->verification = $verification;
$user->email = $email;
$user->save();
$this->success();
}
/**
* @name 修改手机号
* @desc 修改手机号需回执验证(客户端已验证新手机号)
*/
public function changemobile()
{
$this->checkRateLimit('changemobile');
$user = $this->auth->getUser();
$mobile = $this->request->post('mobile', '', 'trim');
if (!$mobile) {
$this->error(__('Invalid parameters'));
}
if (!Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
if (\app\common\model\User::where('mobile', $mobile)->where('id', '<>', $user->id)->find()) {
$this->error(__('Mobile already exists'));
}
$this->verifyReceipt('changemobile', $mobile);
$verification = $user->verification;
$verification->mobile = 1;
$user->verification = $verification;
$user->mobile = $mobile;
$user->save();
$this->success();
}
/**
* @name 发送邮箱验证码(保留兼容)
* @desc 保留接口兼容旧客户端,新客户端建议使用回执验证
*/
public function sendEms()
{
$this->checkRateLimit('sendEms');
$email = $this->request->post('email', '', 'trim');
$event = $this->request->post('event', 'register', 'trim');
if (!$email) {
$this->error('邮箱不能为空');
}
if (!Validate::is($email, "email")) {
$this->error(__('Email is incorrect'));
}
$allowedEvents = ['register', 'changepwd', 'resetpwd', 'changeemail'];
if (!in_array($event, $allowedEvents)) {
$this->error('无效的事件类型');
}
$last = Ems::get($email, $event);
if ($last && time() - $last['createtime'] < 60) {
$this->error(__('发送频繁'));
}
$userinfo = \app\common\model\User::getByEmail($email);
if ($event == 'register' && $userinfo) {
$this->error(__('已被注册'));
} elseif ($event == 'changeemail' && $userinfo) {
$this->error(__('已被占用'));
} elseif (in_array($event, ['changepwd', 'resetpwd']) && !$userinfo) {
$this->error(__('未注册'));
}
$ret = Ems::send($email, null, $event);
if ($ret) {
$this->success(__('发送成功'));
} else {
$this->error(__('发送失败'));
}
}
/**
* @name 校验邮箱验证码(保留兼容)
* @desc 保留接口兼容旧客户端
*/
public function checkEms()
{
$email = $this->request->post('email', '', 'trim');
$captcha = $this->request->post('captcha', '', 'trim');
$event = $this->request->post('event', 'register', 'trim');
if (!$email || !$captcha) {
$this->error(__('Invalid parameters'));
}
if (self::$testMode && $captcha === '888888') {
$this->success(__('验证码正确'));
}
$ret = Ems::check($email, $captcha, $event);
if ($ret) {
$this->success(__('验证码正确'));
} else {
$this->error(__('验证码不正确'));
}
}
/**
* @name 第三方登录
* @desc 第三方平台登录
*/
public function third()
{
$this->checkRateLimit('login');
$url = url('user/index');
$platform = $this->request->post("platform", '', 'trim');
$code = $this->request->post("code", '', 'trim');
$this->detectMaliciousInput($platform);
$this->detectMaliciousInput($code);
$config = get_addon_config('third');
if (!$config || !isset($config[$platform])) {
$this->error(__('Invalid parameters'));
}
$app = new \addons\third\library\Application($config);
$result = $app->{$platform}->getUserInfo(['code' => $code]);
if ($result) {
$loginret = \addons\third\library\Service::connect($platform, $result);
if ($loginret) {
$data = [
'userinfo' => $this->auth->getUserinfo(),
'thirdinfo' => $result
];
$this->success(__('Logged in successful'), $data);
}
}
$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 用户提交注销申请,需回执验证(action=delete_account)3天审核期
* @lastUpdate v9.2.0 新增
*/
public function requestDeletion()
{
$this->checkRateLimit('requestDeletion');
$user = $this->auth->getUser();
$reason = $this->request->post('reason', '', 'trim');
$this->verifyReceipt('delete_account', strval($user->id));
if ($reason) {
$reason = $this->sanitizeString($reason, 500);
}
$existing = db('user_deletion')
->where('user_id', $user->id)
->where('status', 0)
->find();
if ($existing) {
$statusMap = [0 => '待审核', 1 => '已通过(已注销)', 2 => '已拒绝', 3 => '已自动注销'];
$remain = $existing['auto_delete_time'] - time();
$countdown = '';
if ($remain > 0) {
$days = floor($remain / 86400);
$hours = floor(($remain % 86400) / 3600);
$countdown = "{$days}{$hours}小时后自动注销";
} else {
$countdown = '即将自动注销';
}
$this->error('已存在待审核的注销申请', [
'id' => $existing['id'],
'status_text' => $statusMap[$existing['status']] ?? '未知',
'countdown' => $countdown,
]);
}
$threeDaysLater = time() + 3 * 24 * 3600;
$now = time();
$data = [
'user_id' => $user->id,
'username' => $user->username ?? $user->nickname ?? '',
'reason' => $reason ?: '用户主动申请注销',
'status' => 0,
'admin_id' => 0,
'admin_remark' => '',
'auto_delete_time' => $threeDaysLater,
'createtime' => $now,
'updatetime' => $now,
'deletetime' => null,
];
$result = db('user_deletion')->insertGetId($data);
if ($result) {
$remain = $threeDaysLater - time();
$days = floor($remain / 86400);
$hours = floor(($remain % 86400) / 3600);
$countdown = "{$days}{$hours}小时后自动注销";
$this->success('注销申请已提交', [
'id' => $result,
'user_id' => $user->id,
'status' => 0,
'status_text' => '待审核',
'auto_delete_time' => $threeDaysLater,
'auto_delete_time_text' => date('Y-m-d H:i:s', $threeDaysLater),
'countdown' => $countdown,
'createtime_text' => date('Y-m-d H:i:s', $now),
]);
} else {
$this->error('提交失败,请稍后再试');
}
}
/**
* @name 查询注销状态
* @desc 查询当前用户的注销申请状态
* @lastUpdate v9.2.0 新增
*/
public function deletionStatus()
{
$this->checkRateLimit('deletionStatus');
$user = $this->auth->getUser();
$record = db('user_deletion')
->where('user_id', $user->id)
->order('createtime desc')
->find();
if (!$record) {
$this->success('', ['has_pending' => false]);
}
$statusMap = [0 => '待审核', 1 => '已通过(已注销)', 2 => '已拒绝', 3 => '已自动注销'];
$record['status_text'] = $statusMap[$record['status']] ?? '未知';
$record['createtime_text'] = date('Y-m-d H:i:s', $record['createtime']);
$record['auto_delete_time_text'] = date('Y-m-d H:i:s', $record['auto_delete_time']);
$remain = $record['auto_delete_time'] - time();
if ($record['status'] == 0 && $remain > 0) {
$days = floor($remain / 86400);
$hours = floor(($remain % 86400) / 3600);
$record['countdown'] = "{$days}{$hours}小时后自动注销";
} elseif ($record['status'] == 0) {
$record['countdown'] = '已超时,待自动注销';
} else {
$record['countdown'] = '-';
}
$this->success('', [
'has_pending' => $record['status'] == 0,
'id' => $record['id'],
'status' => $record['status'],
'status_text' => $record['status_text'],
'reason' => $record['reason'],
'auto_delete_time_text' => $record['auto_delete_time_text'],
'countdown' => $record['countdown'],
'createtime_text' => $record['createtime_text'],
]);
}
/**
* @name 取消注销申请
* @desc 用户取消待审核的注销申请
* @lastUpdate v9.2.0 新增
*/
public function cancelDeletion()
{
$this->checkRateLimit('cancelDeletion');
$user = $this->auth->getUser();
$record = db('user_deletion')
->where('user_id', $user->id)
->where('status', 0)
->find();
if (!$record) {
$this->error('没有待审核的注销申请');
}
$result = db('user_deletion')
->where('id', $record['id'])
->update([
'status' => 2,
'admin_remark' => '用户主动取消',
'updatetime' => time(),
]);
if ($result) {
$this->success('注销申请已取消');
} else {
$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();
$ipLocation = $this->queryIpLocation($ip);
if ($deviceId) {
$exists = db('user_device')->where('user_id', $userId)->where('device_id', $deviceId)->find();
if ($exists) {
$updateData = [
'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,
];
if ($ipLocation) {
if (!empty($ipLocation['city'])) $updateData['ip_city'] = $ipLocation['city'];
if (!empty($ipLocation['fw'])) $updateData['ip_range'] = $ipLocation['fw'];
}
db('user_device')->where('id', $exists['id'])->update($updateData);
return;
}
}
$insertData = [
'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,
];
if ($ipLocation) {
if (!empty($ipLocation['city'])) $insertData['ip_city'] = $ipLocation['city'];
if (!empty($ipLocation['fw'])) $insertData['ip_range'] = $ipLocation['fw'];
}
db('user_device')->insert($insertData);
}
/**
* @name 查询IP归属地
* @desc 使用纯真IP数据库查询IP归属地返回city和fw
* @lastUpdate v9.2.0 新增解决登录时ip_city为空问题
*/
private function queryIpLocation($ip)
{
if (!$ip || $ip === '127.0.0.1' || $ip === '0.0.0.0') {
return null;
}
try {
$ips = new \Net\Ips('./qqwry.dat');
$resolved = gethostbyname($ip);
if (!$resolved || $resolved === $ip) {
$resolved = $ip;
}
$fwq = $ips->Getlocation($resolved);
$city = strToUTF8($fwq['country'] . ' ' . $fwq['area']);
$fw = '';
$domain = preg_replace('/(\d+)\..*/', '\\1', $resolved);
if ('1' <= $domain && $domain <= '126') {
$fw = '1.0.0.1 - 126.155.255.254';
} elseif ('128' <= $domain && $domain <= '191') {
$fw = '128.0.0.1 - 191.255.255.254';
} elseif ('192' <= $domain && $domain <= '223') {
$fw = '192.0.0.1 - 223.255.255.254';
}
return ['city' => $city, 'fw' => $fw];
} catch (\Exception $e) {
return null;
}
}
/**
* @name 更新在线状态
* @desc 更新用户最后活跃时间和在线状态
* @lastUpdate v9.0.0 新增
*/
private function updateOnlineStatus($userId)
{
db('user')->where('id', $userId)->update([
'last_active_time' => time(),
'is_online' => 1,
]);
}
}