安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
138 lines
4.8 KiB
PHP
138 lines
4.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace addon\haonav\controller\trait;
|
|
|
|
use think\facade\Cache;
|
|
use think\facade\Db;
|
|
use think\Response;
|
|
use addon\haonav\model\Configure as ConfigureModel;
|
|
use addon\haonav\model\Ad as AdModel;
|
|
|
|
/**
|
|
* Index 控制器共享 helper
|
|
* - 配置 / SEO / 用户 / 广告位 helper 被多数 trait 调用,集中放此处
|
|
* - 静态资源输出(Favicon / Snapshot)也共用,统一缓存头
|
|
*/
|
|
trait BaseTrait
|
|
{
|
|
/* ===================== 配置 / SEO ===================== */
|
|
|
|
protected function siteConfig(): array
|
|
{
|
|
return [
|
|
'default_engine' => ConfigureModel::getVal('default_engine', 'baidu'),
|
|
'enable_submit' => (int)ConfigureModel::getVal('enable_submit', '1'),
|
|
'enable_hot_api' => (int)ConfigureModel::getVal('enable_hot_api', '0'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 外观背景配置注入(背景图 / 遮罩 / 随机变换色),
|
|
* 供前端 nav-theme.js 读取:window.NavAppearance。
|
|
*/
|
|
protected function assignAppearance(): void
|
|
{
|
|
$this->view->assign('appearanceJson', json_encode([
|
|
'image' => (string)ConfigureModel::getVal('bg_image', ''),
|
|
'mask' => (int)ConfigureModel::getVal('bg_mask', 35),
|
|
'random' => (int)ConfigureModel::getVal('bg_random', 0),
|
|
'festival' => (int)ConfigureModel::getVal('festival_theme', 1),
|
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
protected function seoConfig(): array
|
|
{
|
|
return [
|
|
'title' => (string)ConfigureModel::getVal('seo_title', '网址导航 - 精选实用网站大全'),
|
|
'keywords' => (string)ConfigureModel::getVal('seo_keywords', '网址导航,常用网址,网站大全,上网导航'),
|
|
'description' => (string)ConfigureModel::getVal('seo_description', '简洁实用的网址导航,收录精选优质网站,支持分类浏览、站内搜索、热门排行与网址投稿。'),
|
|
];
|
|
}
|
|
|
|
protected function assignSeo(): array
|
|
{
|
|
$seo = $this->seoConfig();
|
|
$this->view->assign('seo', $seo);
|
|
$this->view->assign('siteUrl', $this->request->domain());
|
|
return $seo;
|
|
}
|
|
|
|
/* ===================== 用户 / 权限 ===================== */
|
|
|
|
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;
|
|
}
|
|
|
|
/* ===================== 广告位 / 点击统计 ===================== */
|
|
|
|
protected function assignAds()
|
|
{
|
|
$this->view->assign('ads', AdModel::allActiveBySlot());
|
|
}
|
|
|
|
/**
|
|
* 点击时段聚合(供后台「点击热力图」)。表结构首次访问自愈,
|
|
* 写入失败静默不影响跳转。仅按 (link_id, date, hour) 聚合,
|
|
* 全站每天最多 N链接×24 行,规模可控。
|
|
*/
|
|
protected function recordClickStat(int $linkId, int $cid): void
|
|
{
|
|
$table = 'wxapp_haonav_click_stats';
|
|
try {
|
|
$ymd = date('Y-m-d');
|
|
$hr = (int)date('G');
|
|
Db::execute(
|
|
"INSERT INTO `$table` (`link_id`,`cid`,`date`,`hour`,`clicks`) VALUES (?,?,?,?,1) " .
|
|
"ON DUPLICATE KEY UPDATE `clicks` = `clicks` + 1",
|
|
[$linkId, $cid, $ymd, $hr]
|
|
);
|
|
} catch (\Throwable $e) {
|
|
// 统计写入失败不影响跳转
|
|
}
|
|
}
|
|
|
|
/* ===================== 静态资源输出(Favicon / Snapshot 共享) ===================== */
|
|
|
|
protected function serveFile(string $path)
|
|
{
|
|
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
|
$map = [
|
|
'png' => 'image/png',
|
|
'jpg' => 'image/jpeg',
|
|
'jpeg' => 'image/jpeg',
|
|
'gif' => 'image/gif',
|
|
'ico' => 'image/x-icon',
|
|
'svg' => 'image/svg+xml',
|
|
'webp' => 'image/webp',
|
|
];
|
|
$ct = $map[$ext] ?? 'image/png';
|
|
$data = file_get_contents($path);
|
|
return Response::create($data)->header([
|
|
'Content-Type' => $ct,
|
|
'Cache-Control' => 'public, max-age=86400',
|
|
'Expires' => gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT',
|
|
]);
|
|
}
|
|
|
|
protected function serveDefault()
|
|
{
|
|
$path = app()->getRootPath() . 'public/static/haonav/img/default.png';
|
|
if (is_file($path)) {
|
|
return $this->serveFile($path);
|
|
}
|
|
$gif = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
|
|
return Response::create($gif)->header([
|
|
'Content-Type' => 'image/gif',
|
|
'Cache-Control' => 'public, max-age=3600',
|
|
]);
|
|
}
|
|
} |