// +---------------------------------------------------------------------- declare(strict_types=1); namespace addon\haonav\controller; use ywxapp\controller\FrontendBase; use addon\haonav\model\Favorite as FavoriteModel; use addon\haonav\model\FavoriteShare as FavoriteShareModel; use addon\haonav\model\Configure as ConfigureModel; /** * 用户云端收藏(登录后「我的导航」跨端同步) * * 认证:复用框架前台 Auth(Authorization 头或 access_token Cookie)。 * 未登录时接口返回 code=401,前端自动回退 localStorage。 * * @author ywxapp */ class Favorite extends FrontendBase { protected $noNeedLogin = ['*']; protected $noNeedVerify = ['*']; protected function initialize() { } /** * 当前登录用户ID,未登录返回 0 */ protected function uid(): int { if ($this->auth && $this->auth->isLogin) { $info = $this->auth->info; if (is_array($info)) { return (int)($info['id'] ?? 0); } return (int)($info->id ?? 0); } return 0; } /** * 登录态 + 收藏列表 * GET /haonav/favorite/list */ public function list() { $uid = $this->uid(); if (!$uid) { return $this->result->error('未登录', 401); } $list = FavoriteModel::where('user_id', $uid) ->order('sort', 'asc') ->order('id', 'asc') ->select(); return $this->result->success($list); } /** * 新增收藏 * POST /haonav/favorite/add {link_id?,title,url,icon?} */ public function add() { $uid = $this->uid(); if (!$uid) { return $this->result->error('请先登录', 401); } $url = trim((string)$this->request->post('url', '')); $title = trim((string)$this->request->post('title', '')); if ($url === '' || $title === '') { return $this->result->error('参数不完整'); } if (!preg_match('#^https?://#i', $url) && strpos($url, '/') !== 0) { return $this->result->error('网址格式不正确'); } $exists = FavoriteModel::where('user_id', $uid)->where('url', $url)->find(); if ($exists) { return $this->result->success([], '已在我的导航'); } $maxSort = (int)FavoriteModel::where('user_id', $uid)->max('sort'); $model = new FavoriteModel(); $model->user_id = $uid; $model->link_id = (int)$this->request->post('link_id', 0); $model->title = mb_substr($title, 0, 100); $model->url = mb_substr($url, 0, 500); $model->icon = mb_substr((string)$this->request->post('icon', ''), 0, 200); $model->sort = $maxSort + 1; $model->save(); return $this->result->success(['id' => $model->id], '已收藏'); } /** * 移除收藏 * POST /haonav/favorite/remove {id} 或 {url} */ public function remove() { $uid = $this->uid(); if (!$uid) { return $this->result->error('请先登录', 401); } $id = (int)$this->request->post('id', 0); $url = trim((string)$this->request->post('url', '')); $q = FavoriteModel::where('user_id', $uid); if ($id) { $q->where('id', $id); } elseif ($url !== '') { $q->where('url', $url); } else { return $this->result->error('参数不完整'); } $q->delete(); return $this->result->success([], '已移除'); } /** * 保存排序 * POST /haonav/favorite/sort {ids:[3,1,2]} */ public function sort() { $uid = $this->uid(); if (!$uid) { return $this->result->error('请先登录', 401); } $ids = $this->request->post('ids', []); if (is_string($ids)) { $ids = json_decode($ids, true); } if (!is_array($ids) || !$ids) { return $this->result->error('参数不完整'); } $sort = 1; foreach ($ids as $id) { FavoriteModel::where('user_id', $uid)->where('id', (int)$id)->update(['sort' => $sort++]); } return $this->result->success([], '已保存'); } /** * 一次性把本地收藏合并到云端(登录后同步) * POST /haonav/favorite/merge {items:[{title,url,icon},...]} */ public function merge() { $uid = $this->uid(); if (!$uid) { return $this->result->error('请先登录', 401); } $items = $this->request->post('items', []); if (is_string($items)) { $items = json_decode($items, true); } if (is_array($items)) { $maxSort = (int)FavoriteModel::where('user_id', $uid)->max('sort'); foreach ($items as $it) { $url = trim((string)($it['url'] ?? '')); $title = trim((string)($it['title'] ?? '')); if ($url === '' || $title === '') { continue; } $exists = FavoriteModel::where('user_id', $uid)->where('url', $url)->find(); if ($exists) { continue; } $model = new FavoriteModel(); $model->user_id = $uid; $model->link_id = (int)($it['id'] ?? 0); $model->title = mb_substr($title, 0, 100); $model->url = mb_substr($url, 0, 500); $model->icon = mb_substr((string)($it['icon'] ?? ''), 0, 200); $model->sort = ++$maxSort; $model->save(); } } $list = FavoriteModel::where('user_id', $uid)->order('sort', 'asc')->order('id', 'asc')->select(); return $this->result->success($list, '同步完成'); } /** * 获取当前用户的收藏夹分享配置(登录) * GET /haonav/favorite/share */ public function share() { $uid = $this->uid(); if (!$uid) { return $this->result->error('请先登录', 401); } $row = FavoriteShareModel::forUser($uid); return $this->result->success($this->shareData($row)); } /** * 保存分享配置:开关 / 标题 / 重置令牌(登录) * POST /haonav/favorite/sharesave {enabled,title?,reset?} */ public function shareSave() { $uid = $this->uid(); if (!$uid) { return $this->result->error('请先登录', 401); } $row = FavoriteShareModel::forUser($uid); $row->enabled = (int)$this->request->post('enabled', $row->enabled) ? 1 : 0; $title = trim((string)$this->request->post('title', '')); if ($title !== '') { $row->title = mb_substr($title, 0, 100); } if ((int)$this->request->post('reset', 0) === 1) { $row->token = FavoriteShareModel::genToken(); } $row->save(); return $this->result->success($this->shareData($row), '已保存'); } /** * 组装分享配置返回数据(含完整分享链接) */ protected function shareData(FavoriteShareModel $row): array { return [ 'enabled' => (int)$row->enabled, 'token' => (string)$row->token, 'title' => (string)$row->title, 'views' => (int)$row->views, 'url' => $this->request->domain() . '/haonav/favorite/shared/' . $row->token . '.html', ]; } /** * 公开的收藏夹分享页(无需登录,凭 token 访问) * GET /haonav/favorite/shared/:token */ public function shared($token = '') { $token = trim((string)$token); $row = $token !== '' ? FavoriteShareModel::where('token', $token)->find() : null; $valid = $row && (int)$row->enabled === 1; $list = []; if ($valid) { $list = FavoriteModel::where('user_id', $row->user_id) ->order('sort', 'asc') ->order('id', 'asc') ->select(); // 访问计数(不含所有者本人重复刷新的精确去重,简单自增即可) FavoriteShareModel::where('id', $row->id)->inc('views')->update(); } $title = $valid ? ($row->title ?: '收藏夹分享') : '收藏夹不存在或未公开'; $this->view->assign('valid', $valid); $this->view->assign('shareTitle', $title); $this->view->assign('list', $list); $this->view->assign('count', is_countable($list) ? count($list) : 0); $this->view->assign('seo', [ 'title' => $title . ' - 网址导航', 'keywords' => '收藏夹,分享,网址导航', 'description' => (string)ConfigureModel::getVal('seo_description', '简洁实用的网址导航'), ]); $this->view->assign('siteUrl', $this->request->domain()); return $this->view->fetch('favorite/shared'); } }