Files
xianyan/docs/toolsapi/application/api/controller/UserCenter.php
Developer 355191aaf6 feat(leisure): 新增闲情逸致模块与多项功能优化
本次提交完成多项核心更新:
1. 新增闲情逸致功能模块,包含时间线、收藏标注、季节主题等基础框架
2. 替换hive为社区维护的hive_ce包,修复依赖兼容问题
3. 统一替换"开发中"提示为"当前设备不支持",优化用户提示文案
4. 新增多项功能开关与特性标志,统一管理不可用功能提示
5. 完善用户账户洞察系统,新增头像审核中状态检测
6. 优化TTS语音朗读服务,修复Android端引擎初始化问题
7. 重构知识图谱缩放手势逻辑,解决缩放不跟手问题
8. 新增精灵头像组件,替换默认聊天头像样式
9. 新增外部链接跳转确认弹窗,提升使用安全性
10. 升级后端API接口,新增签到配置获取与补签积分规则动态读取
11. 完善多语言翻译覆盖率限制,非中文语言仅显示最高50%进度
12. 新增HTTP缓存拦截器,优化网络请求性能
13. 新增恢复出厂设置选项,完善数据管理功能

同时修复了多处代码细节问题:简化字符串拼接、优化布局代码、移除多余代码等。
2026-05-27 08:06:54 +08:00

1513 lines
62 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 think\Config;
/**
* 用户中心接口
* @time 2026-04-29
* @name UserCenter控制器
* @description 用户中心API含个人信息/签到/收藏/笔记/点赞/互动/数据面板/设备管理/会员/云空间等
* @lastUpdate v10.0.0 registerDevice新增ip_city/ip_range参数; devices返回ip_city/ip_range; 新增myDevices接口
*/
class UserCenter extends Api
{
protected $noNeedLogin = ['public_profile', 'signin_config'];
protected $noNeedRight = '*';
private static $secQuestions = [
1 => '您母亲的姓名是?',
2 => '您的第一只宠物叫什么?',
3 => '您就读的小学名称是?',
4 => '您的出生地是?',
5 => '您最喜欢的电影是?',
6 => '您最好朋友的名字是?',
7 => '您父亲的姓名是?',
8 => '您的童年昵称是?',
];
private static $rateLimitKey = 'api_rate_limit:';
private static $rateLimits = [
'signin' => ['max' => 10, 'window' => 60],
'favorite' => ['max' => 60, 'window' => 60],
'note' => ['max' => 40, 'window' => 60],
'profile' => ['max' => 30, 'window' => 60],
'like' => ['max' => 60, 'window' => 60],
'interaction' => ['max' => 60, 'window' => 60],
'stats' => ['max' => 30, 'window' => 60],
'devices' => ['max' => 30, 'window' => 60],
];
public function _initialize()
{
parent::_initialize();
if (!Config::get('fastadmin.usercenter')) {
$this->error(__('User center already closed'));
}
}
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',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $value)) {
$this->error('输入内容包含非法字符');
}
}
return true;
}
private function validateInt($value, $field, $min = 0, $max = 2147483647)
{
$val = intval($value);
if ($val < $min || $val > $max) {
$this->error("{$field}数值无效");
}
return $val;
}
/**
* @name 获取用户信息
* @desc 获取当前登录用户信息不常用字段压缩到extra
* @lastUpdate v9.0.0 重构返回结构新增is_online/vip/cloud_space/devices字段avatar优先使用avatar_url
*/
public function index()
{
$user = $this->auth->getUser();
$titleInfo = null;
if ($user->title_id) {
$titleInfo = db('user_title')->where('id', $user->title_id)->field('id,name,icon,color')->find();
}
$avatarUrl = isset($user->avatar_url) ? trim($user->avatar_url) : '';
$avatar = $avatarUrl ?: ($user->avatar ? cdnurl($user->avatar, true) : '');
$vipStartTime = isset($user->vip_start_time) ? intval($user->vip_start_time) : 0;
$vipEndTime = isset($user->vip_end_time) ? intval($user->vip_end_time) : 0;
$isVip = $vipEndTime > time();
$cloudTotal = isset($user->cloud_space_total) ? intval($user->cloud_space_total) : 819200;
$cloudUsed = isset($user->cloud_space_used) ? intval($user->cloud_space_used) : 0;
$lastActiveTime = isset($user->last_active_time) ? intval($user->last_active_time) : 0;
$isOnline = (time() - $lastActiveTime < 300) ? 1 : 0;
$onlineDevices = db('user_device')
->where('user_id', $user->id)
->where('is_online', 1)
->field('id,device_name,device_model,platform,app_name,ip,last_active_time')
->order('last_active_time', 'desc')
->select();
$secQuestionId = isset($user->sec_question) ? intval($user->sec_question) : 0;
$secQuestionText = ($secQuestionId > 0 && isset(self::$secQuestions[$secQuestionId])) ? self::$secQuestions[$secQuestionId] : '';
$data = [
'id' => $user->id,
'username' => $user->username,
'nickname' => $user->nickname,
'avatar' => $avatar,
'email' => $user->email,
'mobile' => $user->mobile,
'score' => $user->score,
'level' => $user->level ?: 1,
'exp' => $user->exp ?: 0,
'exp_to_next' => $this->_calcExpToNext(intval($user->exp ?: 0)),
'exp_progress' => $this->_calcExpProgress(intval($user->exp ?: 0)),
'title' => $titleInfo,
'signin_days' => $user->signin_days ?: 0,
'article_count' => $user->article_count ?: 0,
'bio' => $user->bio,
'is_online' => $isOnline,
'vip' => [
'is_vip' => $isVip,
'start_time' => $vipStartTime,
'end_time' => $vipEndTime,
'start_date' => $vipStartTime ? date('Y-m-d', $vipStartTime) : '',
'end_date' => $vipEndTime ? date('Y-m-d', $vipEndTime) : '',
],
'cloud_space' => [
'total' => $cloudTotal,
'used' => $cloudUsed,
'free' => max(0, $cloudTotal - $cloudUsed),
'total_human' => $this->formatBytes($cloudTotal),
'used_human' => $this->formatBytes($cloudUsed),
'usage_percent'=> $cloudTotal > 0 ? round($cloudUsed / $cloudTotal * 100, 1) : 0,
],
'devices' => $onlineDevices,
'extra' => [
'money' => $user->money,
'level' => $user->level ?: 1,
'exp' => $user->exp ?: 0,
'exp_to_next' => $this->_calcExpToNext(intval($user->exp ?: 0)),
'exp_progress' => $this->_calcExpProgress(intval($user->exp ?: 0)),
'level_title' => $this->_getLevelTitle(intval($user->level ?: 1)),
'note_limit' => $user->note_limit ?: 50,
'verification' => $user->verification,
'last_signin_date'=> $user->last_signin_date,
'sec_question' => [
'question_id' => $secQuestionId,
'question_text' => $secQuestionText,
],
],
];
$this->success('', $data);
}
/**
* @name 计算升级所需剩余EXP
* @desc 基于阶梯表直接计算距离下一级还需多少EXP
* @lastUpdate v10.2.0 从二分查找+pow公式改为阶梯表
*/
private function _calcExpToNext($exp)
{
$lv = array(1 => 0, 2 => 30, 3 => 100, 4 => 300, 5 => 800, 6 => 2000, 7 => 5000, 8 => 12000, 9 => 30000, 10 => 80000);
$currentLevel = \app\common\model\User::nextlevelByExp($exp);
if ($currentLevel >= 10) return 0;
$nextLevel = $currentLevel + 1;
return $lv[$nextLevel] - $exp;
}
/**
* @name 计算EXP进度
* @desc 基于阶梯表直接计算当前等级的EXP进度(0~1)
* @lastUpdate v10.2.0 从二分查找+pow公式改为阶梯表
*/
private function _calcExpProgress($exp)
{
$lv = array(1 => 0, 2 => 30, 3 => 100, 4 => 300, 5 => 800, 6 => 2000, 7 => 5000, 8 => 12000, 9 => 30000, 10 => 80000);
$currentLevel = \app\common\model\User::nextlevelByExp($exp);
if ($currentLevel >= 10) return 1.0;
$currentLevelExp = $lv[$currentLevel];
$nextLevelExp = $lv[$currentLevel + 1];
if ($nextLevelExp <= $currentLevelExp) return 1.0;
return round(($exp - $currentLevelExp) / ($nextLevelExp - $currentLevelExp), 4);
}
/**
* @name 获取等级称号
* @desc 根据等级返回对应称号文本
*/
private function _getLevelTitle($level)
{
$titles = [
1 => '新手', 2 => '学徒', 3 => '初学者', 4 => '进阶者', 5 => '学者',
6 => '达人', 7 => '专家', 8 => '大师', 9 => '宗师', 10 => '传说',
];
return isset($titles[$level]) ? $titles[$level] : '新手';
}
/**
* @name 格式化字节
* @desc 将字节数转为人类可读格式
*/
private function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
/**
* @name 修改个人信息
* @desc 修改用户名/昵称/简介/头像URL
* @lastUpdate v9.0.0 avatar改为avatar_url(用户填写的URL优先)
*/
public function profile()
{
$this->checkRateLimit('profile');
$user = $this->auth->getUser();
$username = $this->request->post('username', '', 'trim,strip_tags');
$nickname = $this->request->post('nickname', '', 'trim,strip_tags');
$bio = $this->request->post('bio', '', 'trim');
$avatarUrl = $this->request->post('avatar_url', '', 'trim,strip_tags,htmlspecialchars');
if ($username) {
$this->validateLength($username, '用户名', 3, 30);
$this->detectMaliciousInput($username);
if (!preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u', $username)) {
$this->error('用户名只能包含字母、数字、下划线和中文');
}
$exists = \app\common\model\User::where('username', $username)->where('id', '<>', $this->auth->id)->find();
if ($exists) {
$this->error(__('Username already exists'));
}
$user->username = $username;
}
if ($nickname) {
$this->validateLength($nickname, '昵称', 1, 50);
$this->detectMaliciousInput($nickname);
$exists = \app\common\model\User::where('nickname', $nickname)->where('id', '<>', $this->auth->id)->find();
if ($exists) {
$this->error(__('Nickname already exists'));
}
$user->nickname = $nickname;
}
if ($bio !== null) {
$this->validateLength($bio, '简介', 0, 500);
$user->bio = $bio;
}
if ($avatarUrl !== null) {
$this->validateLength($avatarUrl, '头像URL', 0, 500);
if ($avatarUrl && !filter_var($avatarUrl, FILTER_VALIDATE_URL) && !preg_match('/^https?:\/\//i', $avatarUrl)) {
$this->error('头像URL格式不正确');
}
$user->avatar_url = $avatarUrl;
}
$user->save();
$this->success();
}
/**
* @name 每日签到
* @desc 签到获取积分,连续签到有额外奖励
*/
public function signin()
{
$this->checkRateLimit('signin');
$userId = $this->auth->id;
$todayStart = strtotime(date('Y-m-d'));
$today = db('user_signin')->where('user_id', $userId)->where('createtime', '>=', $todayStart)->find();
if ($today) {
$this->error('今日已签到', ['continuous' => $today['continuous'], 'today_signed' => true]);
}
$yesterdayStart = strtotime(date('Y-m-d', strtotime('-1 day')));
$yesterday = db('user_signin')->where('user_id', $userId)->where('createtime', 'between', [$yesterdayStart, $todayStart - 1])->find();
$continuous = $yesterday ? ($yesterday['continuous'] + 1) : 1;
$coinReward = min($continuous * 2, 20);
$rule = db('coin_rule')->where('action', 'signin')->where('status', 'normal')->find();
if ($rule) {
$coinReward = $rule['amount'];
if ($continuous >= 7 && $rule7 = db('coin_rule')->where('action', 'signin_continuous_7')->where('status', 'normal')->find()) {
$coinReward += $rule7['amount'];
}
if ($continuous >= 30 && $rule30 = db('coin_rule')->where('action', 'signin_continuous_30')->where('status', 'normal')->find()) {
$coinReward += $rule30['amount'];
}
}
$data = [
'user_id' => $userId,
'date' => date('Y-m-d'),
'coin_reward' => $coinReward,
'continuous' => $continuous,
'createtime' => time(),
];
db('user_signin')->insert($data);
db('user')->where('id', $userId)->setInc('score', $coinReward);
db('user')->where('id', $userId)->update(['signin_days' => $continuous, 'last_signin_date' => date('Y-m-d')]);
$logData = [
'user_id' => $userId,
'coin_type' => 'score',
'amount' => $coinReward,
'before' => $this->auth->score,
'after' => $this->auth->score + $coinReward,
'action' => 'signin',
'remark' => '每日签到奖励(连续' . $continuous . '天)',
'createtime'=> time(),
];
db('coin_log')->insert($logData);
$signinExp = 5 + $continuous * 2;
\app\common\model\User::exp($signinExp, $userId, 'signin', '每日签到EXP');
\app\api\controller\Achievement::checkBadges($userId);
$this->success('签到成功', ['continuous' => $continuous, 'coin_reward' => $coinReward, 'exp_reward' => $signinExp, 'today_signed' => true]);
}
/**
* @name 签到日历
* @desc 获取指定月份的签到记录
*/
public function signin_calendar()
{
$userId = $this->auth->id;
$month = trim($this->request->param('month', date('Y-m')));
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
$this->error('月份格式错误');
}
$startDate = strtotime($month . '-01');
$endDate = strtotime($month . '-01 +1 month');
$records = db('user_signin')
->where('user_id', $userId)
->where('createtime', 'between', [$startDate, $endDate - 1])
->field('id,date,coin_reward,continuous,createtime')
->order('date', 'asc')
->select();
$calendar = [];
foreach ($records as $r) {
$calendar[$r['date']] = $r;
}
$currentContinuous = 0;
$lastSignin = db('user_signin')->where('user_id', $userId)->order('date', 'desc')->find();
if ($lastSignin) {
$currentContinuous = $lastSignin['continuous'];
}
$this->success('', [
'month' => $month,
'calendar' => $calendar,
'current_continuous' => $currentContinuous,
'total_signins' => db('user_signin')->where('user_id', $userId)->count(),
]);
}
/**
* @name 签到配置
* @desc 获取补签消耗积分等配置信息
*/
public function signin_config()
{
$makeupRule = db('coin_rule')->where('action', 'signin_makeup')->where('status', 'normal')->find();
$makeupCost = $makeupRule ? intval($makeupRule['amount']) : 10;
$this->success('获取成功', ['makeup_cost' => $makeupCost]);
}
/**
* @name 补签
* @desc 消耗积分补签过去30天内的日期
*/
public function signin_makeup()
{
$userId = $this->auth->id;
$date = $this->request->post('date', '', 'trim');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$this->error('日期格式错误');
}
$dateTimestamp = strtotime($date);
if ($dateTimestamp >= strtotime(date('Y-m-d'))) {
$this->error('只能补签过去的日期');
}
if ($dateTimestamp < strtotime('-30 days')) {
$this->error('只能补签30天内的日期');
}
$exists = db('user_signin')->where('user_id', $userId)->where('date', $date)->find();
if ($exists) {
$this->error('该日期已签到');
}
$makeupRule = db('coin_rule')->where('action', 'signin_makeup')->where('status', 'normal')->find();
$makeupCost = $makeupRule ? intval($makeupRule['amount']) : 10;
$user = db('user')->where('id', $userId)->find();
if ($user['score'] < $makeupCost) {
$this->error('积分不足,补签需要' . $makeupCost . '积分');
}
$prevDate = date('Y-m-d', strtotime($date . ' -1 day'));
$prevSignin = db('user_signin')->where('user_id', $userId)->where('date', $prevDate)->find();
$continuous = $prevSignin ? ($prevSignin['continuous'] + 1) : 1;
db('user_signin')->insert([
'user_id' => $userId,
'date' => $date,
'coin_reward' => 0,
'continuous' => $continuous,
'createtime' => time(),
]);
db('user')->where('id', $userId)->setDec('score', $makeupCost);
try {
db('coin_log')->insert([
'user_id' => $userId,
'coin_type' => 'score',
'amount' => -$makeupCost,
'before' => $user['score'],
'after' => $user['score'] - $makeupCost,
'action' => 'signin_makeup',
'remark' => '补签扣除: ' . $date,
'createtime' => time(),
]);
} catch (\Exception $e) {}
$this->success('补签成功', ['date' => $date, 'cost' => $makeupCost]);
}
/**
* @name 收藏操作
* @desc 添加/移除/检查/列表/分组/移动收藏
*/
public function favorite()
{
$this->checkRateLimit('favorite');
$userId = $this->auth->id;
$action = trim($this->request->param('action', 'add'));
$targetId = $this->request->param('target_id/d', 0);
$targetType = trim($this->request->param('target_type', 'article'));
$remark = $this->sanitizeString($this->request->param('remark', ''), 100);
$groupName = $this->sanitizeString($this->request->param('group_name', '默认'), 30);
$newGroup = $this->sanitizeString($this->request->param('new_group', ''), 30);
$allowedTypes = ['article', 'tool', 'page', 'poetry', 'cy', 'story', 'wisdom', 'hanzi', 'chengyu', 'cidian', 'saying', 'joke'];
$allowedActions = ['add', 'remove', 'check', 'list', 'count', 'groups', 'move'];
if (!in_array($action, $allowedActions)) {
$this->error('无效的操作类型');
}
if (!in_array($targetType, $allowedTypes) && !in_array($action, ['groups', 'count'])) {
$this->error('无效的收藏类型');
}
if (!in_array($action, ['list', 'count', 'groups']) && !$targetId) {
$this->error('参数错误');
}
if ($targetId) {
$this->validateInt($targetId, '目标ID', 1);
}
$where = ['user_id' => $userId, 'target_id' => $targetId, 'target_type' => $targetType];
if ($action == 'add') {
$exists = db('user_favorite')->where($where)->find();
if ($exists) {
$this->error('已收藏');
}
$where['createtime'] = time();
$where['remark'] = $remark;
$where['group_name'] = $groupName;
$favId = db('user_favorite')->insertGetId($where);
$this->success('收藏成功', ['id' => $favId]);
} elseif ($action == 'remove') {
db('user_favorite')->where($where)->delete();
$this->success('取消收藏');
} elseif ($action == 'check') {
$exists = db('user_favorite')->where($where)->find();
$this->success('', ['is_favorited' => !empty($exists)]);
} elseif ($action == 'list') {
$page = $this->request->param('page', 1, 'intval');
$limit = $this->request->param('limit', 15, 'intval');
$limit = min(max($limit, 1), 50);
$favWhere = function() use ($userId, $targetType) {
$q = db('user_favorite')->where('user_id', $userId);
if ($targetType) $q->where('target_type', $targetType);
return $q;
};
$total = $favWhere()->count();
$list = $favWhere()->page($page, $limit)->order('createtime', 'desc')->select();
$this->success('', ['list' => $list, 'total' => $total]);
} elseif ($action == 'count') {
$total = db('user_favorite')->where('user_id', $userId)->count();
$byType = db('user_favorite')->where('user_id', $userId)->group('target_type')->field('target_type, count(*) as count')->select();
$this->success('', ['total' => $total, 'by_type' => $byType]);
} elseif ($action == 'groups') {
$groups = db('user_favorite')
->where('user_id', $userId)
->group('group_name')
->field('group_name, count(*) as count')
->select();
$this->success('', ['groups' => $groups]);
} elseif ($action == 'move') {
if (!$targetId || !$newGroup) {
$this->error('参数错误');
}
$exists = db('user_favorite')->where($where)->find();
if (!$exists) {
$this->error('收藏不存在');
}
db('user_favorite')->where($where)->update(['group_name' => $newGroup]);
$this->success('移动成功');
}
}
/**
* @name 笔记操作
* @desc 笔记的增删改查支持note_type区分笔记/清单/摘抄,支持关联内容
* @lastUpdate v7.9.0 新增source_type/source_id关联内容字段
*/
public function note()
{
$this->checkRateLimit('note');
$userId = $this->auth->id;
$action = trim($this->request->param('action', 'list'));
if ($action == 'list') {
$page = $this->request->param('page', 1, 'intval');
$limit = $this->request->param('limit', 15, 'intval');
$limit = min(max($limit, 1), 50);
$noteType = trim($this->request->param('note_type', ''));
$sourceType = trim($this->request->param('source_type', ''));
$sourceId = $this->request->param('source_id', 0, 'intval');
$noteWhere = function() use ($userId, $noteType, $sourceType, $sourceId) {
$q = db('user_note')->where('user_id', $userId)->where('status', 'normal');
if ($noteType) $q->where('note_type', $noteType);
if ($sourceType) $q->where('source_type', $sourceType);
if ($sourceId) $q->where('source_id', $sourceId);
return $q;
};
$total = $noteWhere()->count();
$list = $noteWhere()->page($page, $limit)->order('updatetime', 'desc')
->field('id,title,content,category,note_type,source_type,source_id,is_public,views,status,createtime,updatetime')
->select();
$this->success('', ['list' => $list, 'total' => $total]);
} elseif ($action == 'add') {
$title = $this->request->post('title', '', 'trim');
$content = $this->request->post('content', '', 'trim');
$category = $this->request->post('category', '', 'trim');
$isPublic = $this->request->post('is_public', 0, 'intval');
$noteType = $this->request->post('note_type', 'note', 'trim');
$sourceType = $this->request->post('source_type', '', 'trim');
$sourceId = $this->request->post('source_id', 0, 'intval');
if (!$title) {
$this->error('标题不能为空');
}
$this->validateLength($title, '标题', 1, 100);
$this->detectMaliciousInput($title);
$allowedNoteTypes = ['note', 'checklist', 'excerpt'];
if (!in_array($noteType, $allowedNoteTypes)) {
$noteType = 'note';
}
$noteLimit = db('user')->where('id', $userId)->value('note_limit') ?: 50;
$noteCount = db('user_note')->where('user_id', $userId)->where('status', 'normal')->count();
if ($noteCount >= $noteLimit) {
$this->error('笔记数量已达上限(' . $noteLimit . '条)');
}
$data = [
'user_id' => $userId,
'title' => $title,
'content' => $content,
'category' => $category ?: '默认',
'note_type' => $noteType,
'source_type' => $sourceType,
'source_id' => $sourceId,
'is_public' => $isPublic ? 1 : 0,
'status' => 'normal',
'createtime' => time(),
'updatetime' => time(),
];
$insertId = db('user_note')->insertGetId($data);
$this->success('添加成功', ['id' => $insertId]);
} elseif ($action == 'edit') {
$id = $this->request->post('id', 0, 'intval');
if (!$id) {
$this->error('参数错误');
}
$note = db('user_note')->where('id', $id)->where('user_id', $userId)->where('status', 'normal')->find();
if (!$note) {
$this->error('笔记不存在或已删除');
}
$update = ['updatetime' => time()];
$title = $this->request->post('title', '', 'trim');
$content = $this->request->post('content', '', 'trim');
$category = $this->request->post('category', '', 'trim');
$isPublic = $this->request->post('is_public');
$noteType = $this->request->post('note_type', '', 'trim');
$sourceType = $this->request->post('source_type', '', 'trim');
$sourceId = $this->request->post('source_id');
if ($title) {
$this->validateLength($title, '标题', 1, 100);
$this->detectMaliciousInput($title);
$update['title'] = $title;
}
if ($content !== null) {
$update['content'] = $content;
}
if ($category !== null) {
$update['category'] = $category;
}
if ($isPublic !== null) {
$update['is_public'] = $isPublic ? 1 : 0;
}
if ($noteType && in_array($noteType, ['note', 'checklist', 'excerpt'])) {
$update['note_type'] = $noteType;
}
if ($sourceType !== null) {
$update['source_type'] = $sourceType;
}
if ($sourceId !== null) {
$update['source_id'] = intval($sourceId);
}
db('user_note')->where('id', $id)->update($update);
$this->success('修改成功');
} elseif ($action == 'delete') {
$id = $this->request->post('id', 0, 'intval');
if (!$id) {
$this->error('参数错误');
}
db('user_note')->where('id', $id)->where('user_id', $userId)->update(['status' => 'hidden', 'updatetime' => time()]);
$this->success('删除成功');
} else {
$this->error('无效操作');
}
}
/**
* @name 通用互动操作
* @desc 支持所有互动类型零新增表扩展含IP记录
* @lastUpdate v7.9.0 增加ip记录新增search/bookmark/collect操作
*/
public function interaction()
{
$this->checkRateLimit('interaction');
$userId = $this->auth->id;
$action = trim($this->request->param('action', ''));
$targetId = $this->request->param('target_id/d', 0);
$targetType = trim($this->request->param('target_type', ''));
$extra = trim($this->request->param('extra', ''));
$clientIp = $this->request->ip();
$allowedActions = [
'like', 'dislike', 'readlater', 'share', 'block',
'view', 'rating', 'comment',
'notify', 'tag', 'progress', 'preference',
'search', 'bookmark', 'collect',
'check', 'history', 'counts'
];
if (!in_array($action, $allowedActions)) {
$this->error('无效的操作类型,支持: ' . implode('/', $allowedActions));
}
$allowedTypes = ['article', 'tool', 'poetry', 'cy', 'story', 'wisdom', 'hanzi', 'chengyu', 'cidian', 'saying', 'joke', 'feed', 'page', 'user'];
$noTargetRequired = ['history', 'counts', 'notify', 'preference', 'search'];
if (!in_array($targetType, $allowedTypes) && !in_array($action, $noTargetRequired)) {
$this->error('无效的内容类型');
}
$requireTarget = ['like', 'dislike', 'readlater', 'share', 'block', 'view', 'rating', 'comment', 'check', 'tag', 'progress', 'bookmark', 'collect'];
if (in_array($action, $requireTarget)) {
if (!$targetId || !$targetType) {
$this->error('参数错误: target_id和target_type必填');
}
}
if ($action === 'check') {
$interactions = db('feed_interaction')
->where('user_id', $userId)
->where('feed_id', $targetId)
->where('feed_type', $targetType)
->column('action');
$this->success('', ['actions' => $interactions]);
return;
}
if ($action === 'counts') {
if ($targetId && $targetType) {
$counts = db('feed_interaction')
->where('feed_id', $targetId)
->where('feed_type', $targetType)
->group('action')
->field('action, count(*) as count')
->select();
$this->success('', ['counts' => $counts]);
} else {
$myCounts = db('feed_interaction')
->where('user_id', $userId)
->group('action')
->field('action, count(*) as count')
->select();
$this->success('', ['my_counts' => $myCounts]);
}
return;
}
if ($action === 'history') {
$page = $this->request->param('page', 1, 'intval');
$limit = $this->request->param('limit', 15, 'intval');
$limit = min(max($limit, 1), 50);
$filterAction = trim($this->request->param('filter_action', ''));
$filterType = trim($this->request->param('filter_type', ''));
$historyWhere = function() use ($userId, $filterAction, $filterType) {
$q = db('feed_interaction')->where('user_id', $userId);
if ($filterAction) $q->where('action', $filterAction);
if ($filterType) $q->where('feed_type', $filterType);
return $q;
};
$total = $historyWhere()->count();
$list = $historyWhere()->page($page, $limit)->order('createtime', 'desc')->select();
$this->success('', ['list' => $list, 'total' => $total]);
return;
}
$toggleActions = ['like', 'dislike', 'readlater', 'share', 'block', 'bookmark', 'collect'];
if (in_array($action, $toggleActions)) {
$where = ['user_id' => $userId, 'feed_id' => $targetId, 'feed_type' => $targetType, 'action' => $action];
$exists = db('feed_interaction')->where($where)->find();
if ($exists) {
db('feed_interaction')->where($where)->delete();
$this->success('取消' . $this->actionLabel($action), ['status' => 'removed', 'id' => $exists['id']]);
} else {
$where['createtime'] = time();
$where['ip'] = $clientIp;
if ($extra) {
$where['extra'] = $extra;
}
$insertId = db('feed_interaction')->insertGetId($where);
$this->success($this->actionLabel($action) . '成功', ['status' => 'added', 'id' => $insertId]);
}
return;
}
if ($action === 'view') {
$where = ['user_id' => $userId, 'feed_id' => $targetId, 'feed_type' => $targetType, 'action' => 'view'];
$today = date('Y-m-d');
$exists = db('feed_interaction')->where($where)->whereTime('createtime', 'between', [strtotime($today), strtotime($today . ' 23:59:59')])->find();
if (!$exists) {
$where['createtime'] = time();
$where['ip'] = $clientIp;
if ($extra) {
$where['extra'] = $extra;
}
db('feed_interaction')->insert($where);
}
$tableMap = ['article' => 'article', 'poetry' => 'poetry', 'story' => 'story', 'wisdom' => 'wisdom'];
if (isset($tableMap[$targetType])) {
try { db($tableMap[$targetType])->where('id', $targetId)->setInc('views'); } catch (\Exception $e) {}
}
db('user')->where('id', $userId)->update(['last_active_time' => time()]);
$this->success('浏览记录', ['status' => 'recorded']);
return;
}
if ($action === 'rating') {
$score = intval($extra);
if ($score < 1 || $score > 5) {
$this->error('评分须在1-5之间');
}
$where = ['user_id' => $userId, 'feed_id' => $targetId, 'feed_type' => $targetType, 'action' => 'rating'];
$exists = db('feed_interaction')->where($where)->find();
if ($exists) {
db('feed_interaction')->where($where)->update(['extra' => strval($score), 'createtime' => time(), 'ip' => $clientIp]);
$this->success('评分已更新', ['score' => $score]);
} else {
$where['extra'] = strval($score);
$where['createtime'] = time();
$where['ip'] = $clientIp;
db('feed_interaction')->insert($where);
$this->success('评分成功', ['score' => $score]);
}
return;
}
if ($action === 'comment') {
if (!$extra) {
$this->error('评论内容不能为空');
}
$this->validateLength($extra, '评论', 1, 1000);
$this->detectMaliciousInput($extra);
$data = [
'user_id' => $userId,
'feed_id' => $targetId,
'feed_type' => $targetType,
'action' => 'comment',
'extra' => $this->sanitizeString($extra, 1000),
'ip' => $clientIp,
'createtime' => time(),
];
$commentId = db('feed_interaction')->insertGetId($data);
$this->success('评论成功', ['id' => $commentId]);
return;
}
if ($action === 'notify') {
$notifyType = trim($this->request->param('notify_type', 'system'));
$allowedNotifyTypes = ['system', 'interaction', 'custom'];
if (!in_array($notifyType, $allowedNotifyTypes)) {
$notifyType = 'system';
}
$data = [
'user_id' => $userId,
'feed_id' => $targetId ?: 0,
'feed_type' => $targetType ?: 'system',
'action' => 'notify',
'extra' => json_encode([
'notify_type' => $notifyType,
'content' => $this->sanitizeString($extra, 500),
'is_read' => 0,
]),
'ip' => $clientIp,
'createtime' => time(),
];
db('feed_interaction')->insert($data);
$this->success('通知已记录');
return;
}
if ($action === 'tag') {
if (!$extra) {
$this->error('标签名不能为空');
}
$this->validateLength($extra, '标签', 1, 30);
$tagName = $this->sanitizeString($extra, 30);
$where = ['user_id' => $userId, 'feed_id' => $targetId, 'feed_type' => $targetType, 'action' => 'tag', 'extra' => $tagName];
$exists = db('feed_interaction')->where($where)->find();
if ($exists) {
db('feed_interaction')->where($where)->delete();
$this->success('标签已移除', ['status' => 'removed']);
} else {
$where['createtime'] = time();
$where['ip'] = $clientIp;
db('feed_interaction')->insert($where);
$this->success('标签已添加', ['status' => 'added']);
}
return;
}
if ($action === 'progress') {
$progress = intval($extra);
if ($progress < 0 || $progress > 100) {
$this->error('进度须在0-100之间');
}
$where = ['user_id' => $userId, 'feed_id' => $targetId, 'feed_type' => $targetType, 'action' => 'progress'];
$exists = db('feed_interaction')->where($where)->find();
if ($exists) {
db('feed_interaction')->where($where)->update(['extra' => strval($progress), 'createtime' => time(), 'ip' => $clientIp]);
$this->success('进度已更新', ['progress' => $progress]);
} else {
$where['extra'] = strval($progress);
$where['createtime'] = time();
$where['ip'] = $clientIp;
db('feed_interaction')->insert($where);
$this->success('进度已记录', ['progress' => $progress]);
}
return;
}
if ($action === 'preference') {
if (!$extra) {
$this->error('偏好数据不能为空');
}
$prefData = $extra;
if (mb_strlen($prefData) > 2000) {
$this->error('偏好数据过长');
}
$where = ['user_id' => $userId, 'feed_id' => 0, 'feed_type' => 'user', 'action' => 'preference'];
$exists = db('feed_interaction')->where($where)->find();
if ($exists) {
db('feed_interaction')->where($where)->update(['extra' => $prefData, 'createtime' => time(), 'ip' => $clientIp]);
$this->success('偏好已更新');
} else {
$where['extra'] = $prefData;
$where['createtime'] = time();
$where['ip'] = $clientIp;
db('feed_interaction')->insert($where);
$this->success('偏好已保存');
}
return;
}
if ($action === 'search') {
if (!$extra) {
$this->error('搜索关键词不能为空');
}
$this->validateLength($extra, '搜索词', 1, 100);
$keyword = $this->sanitizeString($extra, 100);
$where = ['user_id' => $userId, 'action' => 'search', 'extra' => $keyword];
$exists = db('feed_interaction')->where($where)->whereTime('createtime', '>=', strtotime('-1 hour'))->find();
if (!$exists) {
db('feed_interaction')->insert([
'user_id' => $userId,
'feed_id' => 0,
'feed_type' => $targetType ?: 'all',
'action' => 'search',
'extra' => $keyword,
'ip' => $clientIp,
'createtime' => time(),
]);
}
$this->success('搜索已记录');
return;
}
}
private function actionLabel($action)
{
$labels = [
'like' => '点赞',
'dislike' => '不喜欢',
'readlater' => '稍后读',
'share' => '分享',
'block' => '屏蔽',
];
return $labels[$action] ?? $action;
}
/**
* @name 点赞操作
* @desc 添加/移除/检查点赞
*/
public function like()
{
$this->checkRateLimit('like');
$userId = $this->auth->id;
$action = trim($this->request->post('action', 'add'));
$targetId = $this->request->param('target_id/d', 0);
$targetType = trim($this->request->param('target_type', 'article'));
$allowedTypes = ['article', 'tool', 'poetry', 'cy', 'story', 'wisdom', 'hanzi', 'saying', 'feed'];
if (!in_array($targetType, $allowedTypes)) {
$this->error('无效的类型');
}
if (!$targetId) {
$this->error('参数错误');
}
$where = ['user_id' => $userId, 'feed_id' => $targetId, 'feed_type' => $targetType, 'action' => 'like'];
if ($action === 'add') {
$exists = db('feed_interaction')->where($where)->find();
if ($exists) {
$this->error('已点赞');
}
$where['createtime'] = time();
db('feed_interaction')->insert($where);
$tableMap = ['article' => 'article', 'poetry' => 'poetry', 'cy' => 'cy', 'story' => 'story', 'wisdom' => 'wisdom'];
if (isset($tableMap[$targetType])) {
try { db($tableMap[$targetType])->where('id', $targetId)->setInc('likes'); } catch (\Exception $e) {}
}
$this->success('点赞成功');
} elseif ($action === 'remove') {
db('feed_interaction')->where($where)->delete();
$tableMap = ['article' => 'article', 'poetry' => 'poetry', 'cy' => 'cy', 'story' => 'story', 'wisdom' => 'wisdom'];
if (isset($tableMap[$targetType])) {
try { db($tableMap[$targetType])->where('id', $targetId)->setDec('likes'); } catch (\Exception $e) {}
}
$this->success('取消点赞');
} elseif ($action === 'check') {
$exists = db('feed_interaction')->where($where)->find();
$this->success('', ['is_liked' => !empty($exists)]);
} else {
$this->error('无效操作');
}
}
/**
* @name 用户公开主页
* @desc 获取用户公开信息(无需登录)
* @lastUpdate v9.0.0 avatar优先使用avatar_url新增vip状态
*/
public function public_profile()
{
$uid = $this->request->param('uid', 0, 'intval');
if (!$uid) {
$uid = $this->request->param('user_id', 0, 'intval');
}
if (!$uid) {
$this->error('参数错误: uid或user_id必填');
}
$user = db('user')->where('id', $uid)->where('status', 'normal')->find();
if (!$user) {
$this->error('用户不存在');
}
$articleCount = db('article')->where('user_id', $uid)->where('status', 'normal')->count();
$signinDays = $user['signin_days'] ?: 0;
$favoriteCount = db('user_favorite')->where('user_id', $uid)->count();
$likeCount = db('feed_interaction')->where('user_id', $uid)->where('action', 'like')->count();
$avatarUrl = isset($user['avatar_url']) ? trim($user['avatar_url']) : '';
$avatar = $avatarUrl ?: ($user['avatar'] ? cdnurl($user['avatar'], true) : '');
$vipEndTime = isset($user['vip_end_time']) ? intval($user['vip_end_time']) : 0;
$data = [
'id' => $user['id'],
'username' => $user['username'],
'nickname' => $user['nickname'],
'avatar' => $avatar,
'bio' => $user['bio'] ?? '',
'score' => $user['score'],
'signin_days' => $signinDays,
'article_count' => $articleCount,
'favorite_count' => $favoriteCount,
'like_count' => $likeCount,
'is_vip' => $vipEndTime > time(),
'jointime' => $user['jointime'] ? date('Y-m-d', intval($user['jointime'])) : '-',
];
$this->success('成功', $data);
}
/**
* @name 数据面板
* @desc 用户个人数据可视化统计
*/
public function dashboard()
{
$userId = $this->auth->id;
$user = db('user')->where('id', $userId)->find();
$signinCount = db('user_signin')->where('user_id', $userId)->count();
$favoriteCount = db('user_favorite')->where('user_id', $userId)->count();
$noteCount = db('user_note')->where('user_id', $userId)->where('status', 'normal')->count();
$likeCount = db('feed_interaction')->where('user_id', $userId)->where('action', 'like')->count();
$articleCount = db('article')->where('user_id', $userId)->where('status', 'normal')->count();
$commentCount = db('feed_interaction')->where('user_id', $userId)->where('action', 'comment')->count();
$viewCount = db('feed_interaction')->where('user_id', $userId)->where('action', 'view')->count();
$readlaterCount = db('feed_interaction')->where('user_id', $userId)->where('action', 'readlater')->count();
$last7Days = [];
for ($i = 6; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-{$i} days"));
$signin = db('user_signin')->where('user_id', $userId)->where('date', $date)->find();
$last7Days[] = [
'date' => $date,
'signed' => !empty($signin),
'coin_reward' => $signin ? $signin['coin_reward'] : 0,
];
}
$data = [
'user_id' => $userId,
'score' => $user['score'],
'signin_days' => $user['signin_days'] ?: 0,
'signin_count' => $signinCount,
'favorite_count' => $favoriteCount,
'note_count' => $noteCount,
'like_count' => $likeCount,
'comment_count' => $commentCount,
'view_count' => $viewCount,
'readlater_count' => $readlaterCount,
'article_count' => $articleCount,
'last_7days' => $last7Days,
];
$this->success('', $data);
}
/**
* @name 活跃热力图
* @desc 用户活跃度热力图数据
*/
public function heatmap()
{
$userId = $this->auth->id;
$year = trim($this->request->param('year', date('Y')));
if (!preg_match('/^\d{4}$/', $year)) {
$this->error('年份格式错误');
}
$startDate = strtotime($year . '-01-01');
$endDate = strtotime($year . '-12-31 23:59:59');
$signins = db('user_signin')
->where('user_id', $userId)
->where('createtime', 'between', [$startDate, $endDate])
->column('date');
$favorites = db('user_favorite')
->where('user_id', $userId)
->where('createtime', 'between', [$startDate, $endDate])
->column("FROM_UNIXTIME(createtime, '%Y-%m-%d') as date");
$heatmap = [];
$activityMap = [];
foreach ($signins as $d) {
$activityMap[$d] = ($activityMap[$d] ?? 0) + 1;
}
foreach ($favorites as $d) {
$activityMap[$d] = ($activityMap[$d] ?? 0) + 1;
}
foreach ($activityMap as $date => $count) {
$heatmap[] = ['date' => $date, 'count' => $count];
}
$this->success('', ['year' => $year, 'heatmap' => $heatmap]);
}
/**
* @name 金币记录
* @desc 获取金币变动记录
*/
public function coin()
{
$userId = $this->auth->id;
$page = $this->request->param('page', 1, 'intval');
$limit = $this->request->param('limit', 15, 'intval');
$limit = min(max($limit, 1), 50);
$total = db('coin_log')->where('user_id', $userId)->count();
$list = db('coin_log')
->where('user_id', $userId)
->page($page, $limit)
->order('createtime', 'desc')
->field('id,coin_type,amount,before,after,action,remark,createtime')
->select();
$this->success('', ['list' => $list, 'total' => $total]);
}
/**
* @name 学习统计
* @desc 基于互动数据生成学习统计和用户画像
* @lastUpdate v7.8.0 新增
*/
public function stats()
{
$this->checkRateLimit('stats');
$userId = $this->auth->id;
$typeStats = db('feed_interaction')
->where('user_id', $userId)
->where('action', 'in', ['view', 'like', 'rating', 'comment', 'readlater', 'share'])
->group('feed_type')
->field('feed_type, count(*) as count')
->order('count', 'desc')
->select();
$actionStats = db('feed_interaction')
->where('user_id', $userId)
->group('action')
->field('action, count(*) as count')
->order('count', 'desc')
->select();
$topRated = db('feed_interaction')
->where('user_id', $userId)
->where('action', 'rating')
->order('extra', 'desc')
->limit(10)
->field('feed_id, feed_type, extra as score, createtime')
->select();
$recentViews = db('feed_interaction')
->where('user_id', $userId)
->where('action', 'view')
->order('createtime', 'desc')
->limit(10)
->field('feed_id, feed_type, createtime')
->select();
$totalViews = db('feed_interaction')->where('user_id', $userId)->where('action', 'view')->count();
$totalLikes = db('feed_interaction')->where('user_id', $userId)->where('action', 'like')->count();
$totalComments = db('feed_interaction')->where('user_id', $userId)->where('action', 'comment')->count();
$totalRatings = db('feed_interaction')->where('user_id', $userId)->where('action', 'rating')->count();
$avgRating = db('feed_interaction')
->where('user_id', $userId)
->where('action', 'rating')
->avg('extra');
$weekStart = strtotime('-7 days');
$weekViews = db('feed_interaction')->where('user_id', $userId)->where('action', 'view')->where('createtime', '>=', $weekStart)->count();
$weekLikes = db('feed_interaction')->where('user_id', $userId)->where('action', 'like')->where('createtime', '>=', $weekStart)->count();
$preference = db('feed_interaction')
->where('user_id', $userId)
->where('action', 'preference')
->value('extra');
$tags = db('feed_interaction')
->where('user_id', $userId)
->where('action', 'tag')
->group('extra')
->field('extra as tag, count(*) as count')
->order('count', 'desc')
->limit(20)
->select();
$data = [
'type_stats' => $typeStats,
'action_stats' => $actionStats,
'top_rated' => $topRated,
'recent_views' => $recentViews,
'summary' => [
'total_views' => $totalViews,
'total_likes' => $totalLikes,
'total_comments' => $totalComments,
'total_ratings' => $totalRatings,
'avg_rating' => round(floatval($avgRating), 1),
'week_views' => $weekViews,
'week_likes' => $weekLikes,
],
'preference' => $preference ? json_decode($preference, true) : null,
'tags' => $tags,
];
$this->success('', $data);
}
/**
* @name 调试信息
* @desc 获取用户调试信息(开发模式)
* @lastUpdate v9.0.0 新增vip/cloud_space/online字段
*/
public function debug()
{
$user = $this->auth->getUser();
$lastActiveTime = isset($user->last_active_time) ? intval($user->last_active_time) : 0;
$data = [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'mobile' => $user->mobile,
'score' => $user->score,
'status' => $user->status,
'is_test' => $user->is_test ?? 0,
'verification' => $user->verification,
'jointime' => $user->jointime,
'last_signin_date' => $user->last_signin_date,
'signin_days' => $user->signin_days,
'is_online' => (time() - $lastActiveTime < 300) ? 1 : 0,
'last_active_time' => $lastActiveTime,
'vip_start_time' => isset($user->vip_start_time) ? intval($user->vip_start_time) : 0,
'vip_end_time' => isset($user->vip_end_time) ? intval($user->vip_end_time) : 0,
'cloud_space_total' => isset($user->cloud_space_total) ? intval($user->cloud_space_total) : 819200,
'cloud_space_used' => isset($user->cloud_space_used) ? intval($user->cloud_space_used) : 0,
'avatar_url' => isset($user->avatar_url) ? $user->avatar_url : '',
];
$this->success('', $data);
}
/**
* @name 设备管理
* @desc 查看/删除/下线用户设备
* @lastUpdate v9.0.0 新增
*/
public function devices()
{
$this->checkRateLimit('devices');
$userId = $this->auth->id;
$action = trim($this->request->param('action', 'list'));
if ($action === 'list') {
$devices = db('user_device')
->where('user_id', $userId)
->order('last_active_time', 'desc')
->field('id,device_name,device_model,platform,app_name,ip,ip_city,ip_range,last_active_time,is_online,createtime')
->select();
foreach ($devices as &$d) {
$d['last_active_text'] = $d['last_active_time'] ? date('Y-m-d H:i:s', $d['last_active_time']) : '-';
$d['createtime_text'] = $d['createtime'] ? date('Y-m-d H:i:s', $d['createtime']) : '-';
}
unset($d);
$this->success('', ['devices' => $devices]);
} elseif ($action === 'remove') {
$deviceId = $this->request->param('device_id/d', 0);
if (!$deviceId) {
$this->error('device_id必填');
}
$device = db('user_device')->where('id', $deviceId)->where('user_id', $userId)->find();
if (!$device) {
$this->error('设备不存在');
}
db('user_device')->where('id', $deviceId)->delete();
$this->success('设备已移除');
} elseif ($action === 'offline') {
$deviceId = $this->request->param('device_id/d', 0);
if (!$deviceId) {
$this->error('device_id必填');
}
$device = db('user_device')->where('id', $deviceId)->where('user_id', $userId)->find();
if (!$device) {
$this->error('设备不存在');
}
db('user_device')->where('id', $deviceId)->update(['is_online' => 0, 'updatetime' => time()]);
$this->success('设备已下线');
} elseif ($action === 'offline_all') {
db('user_device')->where('user_id', $userId)->update(['is_online' => 0, 'updatetime' => time()]);
$this->success('所有设备已下线');
} elseif ($action === 'rename') {
$deviceId = $this->request->param('device_id/d', 0);
$deviceName = trim($this->request->param('device_name', ''));
if (!$deviceId) {
$this->error('device_id必填');
}
if (!$deviceName) {
$this->error('device_name必填');
}
$this->validateLength($deviceName, '设备名称', 1, 100);
$device = db('user_device')->where('id', $deviceId)->where('user_id', $userId)->find();
if (!$device) {
$this->error('设备不存在');
}
db('user_device')->where('id', $deviceId)->update([
'device_name' => $deviceName,
'updatetime' => time(),
]);
$this->success('设备已重命名');
} else {
$this->error('无效操作,支持: list/remove/offline/offline_all/rename');
}
}
/**
* @name 记录或更新设备信息
* @desc 登录后调用,记录当前设备信息
* @lastUpdate v9.0.0 新增
*/
public function registerDevice()
{
$this->checkRateLimit('devices');
$userId = $this->auth->id;
$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');
$ipCity = $this->request->post('ip_city', '', 'trim');
$ipRange = $this->request->post('ip_range', '', 'trim');
$userAgent = $this->request->header('user-agent', '');
$this->validateLength($deviceName, '设备名称', 0, 100);
$this->validateLength($deviceModel, '设备型号', 0, 100);
$this->validateLength($platform, '登录平台', 0, 30);
$this->validateLength($appName, '应用名称', 0, 50);
$this->validateLength($ipCity, 'IP归属地', 0, 200);
$this->validateLength($ipRange, 'IP段范围', 0, 100);
$allowedPlatforms = ['ios', 'android', 'web', 'windows', 'mac', 'linux', 'other'];
if ($platform && !in_array(strtolower($platform), $allowedPlatforms)) {
$platform = 'other';
}
$ip = $this->request->ip();
$now = time();
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,
'ip_city' => $ipCity ?: ($exists['ip_city'] ?? ''),
'ip_range' => $ipRange ?: ($exists['ip_range'] ?? ''),
'last_active_time' => $now,
'is_online' => 1,
'user_agent' => substr($userAgent, 0, 500),
'updatetime' => $now,
];
db('user_device')->where('id', $exists['id'])->update($updateData);
$this->success('设备已更新', ['device_id' => $exists['id']]);
}
}
$insertId = db('user_device')->insertGetId([
'user_id' => $userId,
'device_name' => $deviceName,
'device_model' => $deviceModel,
'platform' => $platform,
'app_name' => $appName,
'ip' => $ip,
'ip_city' => $ipCity,
'ip_range' => $ipRange,
'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,
]);
db('user')->where('id', $userId)->update([
'last_active_time' => $now,
'is_online' => 1,
]);
$this->success('设备已注册', ['device_id' => $insertId]);
}
/**
* @name 获取同账号在线设备(互传用)
* @desc 返回当前用户所有在线设备,排除当前设备
* @lastUpdate v10.0.0 新增
*/
public function myDevices()
{
$this->checkRateLimit('devices');
$userId = $this->auth->id;
$currentDeviceId = $this->request->param('device_id', '', 'trim');
$devices = db('user_device')
->where('user_id', $userId)
->where('is_online', 1)
->order('last_active_time', 'desc')
->field('id,device_id,device_name,device_model,platform,app_name,ip,ip_city,ip_range,last_active_time,is_online')
->select();
$this->success('ok', [
'currentDeviceId' => $currentDeviceId,
'devices' => $devices,
]);
}
/**
* @name EXP变动记录
* @desc 获取当前用户EXP变动日志分页返回
* @lastUpdate v10.1.0 新增
*/
public function expLog()
{
$page = $this->request->param('page', 1, 'intval');
$pagesize = $this->request->param('pagesize', 20, 'intval');
$pagesize = min($pagesize, 50);
$userId = $this->auth->id;
$list = db('user_exp_log')
->where('user_id', $userId)
->order('createtime', 'desc')
->page($page, $pagesize)
->select();
$total = db('user_exp_log')
->where('user_id', $userId)
->count();
$this->success('', [
'list' => $list,
'total' => $total,
'page' => $page,
'pagesize' => $pagesize,
]);
}
}