275 lines
9.3 KiB
PHP
275 lines
9.3 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
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 <admin@ywxapp.cn>
|
||
*/
|
||
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');
|
||
}
|
||
}
|