feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用

安全(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(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
yangwx committed 2026-10-22 00:53:39 +08:00
1 parent 1d49e6f5ee
commit 18101cf11a
636 files changed
+58517 -9519

No files matched your search

+138
View File
@@ -0,0 +1,138 @@
<?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',
]);
}
}