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

+217 -217
View File
@@ -1,17 +1,148 @@
<div class="layui-fluid">
<!-- 系统概览:与「系统信息」页同源(app\backend\library\SystemInfo),控制台只做展示 -->
<div class="layui-row layui-col-space15">
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card"><div class="layui-card-body">
<div style="color:#666">管理员</div>
<div style="font-size:24px;font-weight:600">{$sys.stat.admin}</div>
</div></div>
</div>
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card"><div class="layui-card-body">
<div style="color:#666">插件</div>
<div style="font-size:24px;font-weight:600">{$sys.stat.addon}</div>
</div></div>
</div>
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card"><div class="layui-card-body">
<div style="color:#666">附件</div>
<div style="font-size:24px;font-weight:600">{$sys.stat.attachment}</div>
<div style="color:#999;font-size:12px">占用 {$sys.stat.attach_size_text}</div>
</div></div>
</div>
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card"><div class="layui-card-body">
<div style="color:#666">数据库</div>
<div style="font-size:24px;font-weight:600">{$sys.stat.db_size_text}</div>
</div></div>
</div>
</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">运行环境</div>
<div class="layui-card-body">
<table class="layui-table layui-table-sm" lay-skin="nob">
<tbody>
{foreach $sys.env as $row}
<tr>
<td style="width:120px;color:#666">{$row[0]}</td>
<td>{$row[1]}</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">磁盘</div>
<div class="layui-card-body">
<div class="layui-progress layui-progress-big" lay-showPercent="true">
<div class="layui-progress-bar" style="width:{$sys.disk.percent}%"></div>
</div>
<div style="margin-top:10px;color:#666">
已用 {$sys.disk.used_text} / 共 {$sys.disk.total_text},剩余 {$sys.disk.free_text}
</div>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">目录权限</div>
<div class="layui-card-body">
<table class="layui-table layui-table-sm" lay-skin="nob">
<thead><tr><th>目录</th><th>存在</th><th>可写</th></tr></thead>
<tbody>
{foreach $sys.dirs as $d}
<tr>
<td>{$d.name}</td>
<td>{if $d.exists}<span class="layui-badge layui-bg-green">是</span>{else}<span class="layui-badge">否</span>{/if}</td>
<td>{if $d.writable}<span class="layui-badge layui-bg-green">可写</span>{else}<span class="layui-badge layui-bg-red">不可写</span>{/if}</td>
</tr>
{/foreach}
</tbody>
</table>
<div style="color:#999;font-size:12px">不可写的目录会导致缓存无法生成、上传失败、插件无法安装。</div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">维护</div>
<div class="layui-card-body">
<div class="layui-btn-group">
<button class="layui-btn layui-btn-normal" id="sys-clean-cache" data-perm="console:clean">清理数据缓存</button>
<button class="layui-btn" id="sys-clean-temp" data-perm="console:clean">清理模板缓存</button>
<button class="layui-btn layui-btn-warm" id="sys-clean-all" data-perm="console:clean">全量清理</button>
</div>
<div style="margin-top:10px;color:#999;font-size:12px">
只清理 runtime/cache 与 runtime/temp;<strong>不会删除 runtime/log</strong>(操作日志与错误日志保留)。
</div>
<div style="margin-top:12px;color:#666">
近 7 天操作 <strong>{$sys.stat.log_week}</strong> 条,其中登录失败 <strong style="color:#ff5722">{$sys.stat.login_fail}</strong> 次。
</div>
</div>
</div>
<!-- 框架升级:与「系统管理 → 框架升级」同源,共用 /admin/framework/check|upgrade -->
<div class="layui-card">
<div class="layui-card-header">框架升级</div>
<div class="layui-card-body">
<p style="margin-bottom:8px">当前版本:<span class="layui-badge layui-bg-blue" id="fw-current">{$version}</span></p>
<div id="fw-status" class="layui-text" style="margin:10px 0;min-height:24px;"></div>
<div id="fw-pkg-box" style="display:none;margin:10px 0;padding:10px 12px;background:#fafafa;border:1px solid #eee;border-radius:4px;">
<div style="margin-bottom:8px;color:#555;">升级包类型:</div>
<div class="layui-btn-group" id="fw-pkg-group">
<button type="button" class="layui-btn layui-btn-sm layui-btn-normal fw-pkg" data-type="auto">自动(推荐)</button>
<button type="button" class="layui-btn layui-btn-sm layui-btn-primary fw-pkg" data-type="full">完整包</button>
<button type="button" class="layui-btn layui-btn-sm layui-btn-primary fw-pkg" data-type="patch">增量小包</button>
</div>
<div id="fw-pkg-tip" style="margin-top:8px;color:#e67700;font-size:13px;"></div>
</div>
<div class="layui-btn-group">
<button class="layui-btn layui-btn-normal" id="fw-check" data-perm="framework:check">
<i class="layui-icon layui-icon-refresh"></i> 检查更新
</button>
<button class="layui-btn layui-btn-danger" id="fw-upgrade" data-perm="framework:upgrade" style="display:none;">
<i class="layui-icon layui-icon-release"></i> 立即升级
</button>
</div>
<div style="margin-top:10px;color:#999;font-size:12px">
升级前会自动备份当前核心文件;本卡片与「系统管理 → 框架升级」是同一套接口与权限。
</div>
</div>
</div>
</div>
</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">
访问量
<span class="layui-badge layui-bg-blue layuiadmin-badge">周</span>
会员总数
<span class="layui-badge layui-bg-blue layuiadmin-badge">总</span>
</div>
<div class="layui-card-body layuiadmin-card-list">
<p class="layuiadmin-big-font">9,999,888</p>
<p class="layuiadmin-big-font">{$stats.total.member}</p>
<p>
总计访问量
<span class="layuiadmin-span-color">88万 <i class="layui-inline layui-icon layui-icon-flag"></i></span>
近 7 天新增
<span class="layuiadmin-span-color">{$stats.total.member_week} 人 <i class="layui-inline layui-icon layui-icon-user"></i></span>
</p>
</div>
</div>
@@ -19,14 +150,14 @@
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">
下载
<span class="layui-badge layui-bg-cyan layuiadmin-badge">月</span>
今日新增会员
<span class="layui-badge layui-bg-green layuiadmin-badge">日</span>
</div>
<div class="layui-card-body layuiadmin-card-list">
<p class="layuiadmin-big-font">33,555</p>
<p class="layuiadmin-big-font">{$stats.today.member}</p>
<p>
新下载
<span class="layuiadmin-span-color">10% <i class="layui-inline layui-icon layui-icon-face-smile-b"></i></span>
会员表实时统计
<span class="layuiadmin-span-color">今日 <i class="layui-inline layui-icon layui-icon-friends"></i></span>
</p>
</div>
</div>
@@ -34,15 +165,14 @@
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">
点赞
<span class="layui-badge layui-bg-green layuiadmin-badge">年</span>
今日操作
<span class="layui-badge layui-bg-cyan layuiadmin-badge">日</span>
</div>
<div class="layui-card-body layuiadmin-card-list">
<p class="layuiadmin-big-font">999,888</p>
<p class="layuiadmin-big-font">{$stats.today.log}</p>
<p>
总赞
<span class="layuiadmin-span-color">*** <i class="layui-inline layui-icon layui-icon-heart"></i></span>
后台操作记录
<span class="layuiadmin-span-color">累计 {$stats.total.log} <i class="layui-inline layui-icon layui-icon-log"></i></span>
</p>
</div>
</div>
@@ -50,15 +180,14 @@
<div class="layui-col-sm6 layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">
活跃用户
<span class="layui-badge layui-bg-orange layuiadmin-badge">月</span>
今日蜘蛛来访
<span class="layui-badge layui-bg-orange layuiadmin-badge">日</span>
</div>
<div class="layui-card-body layuiadmin-card-list">
<p class="layuiadmin-big-font">66,888</p>
<p class="layuiadmin-big-font">{$stats.today.spider}</p>
<p>
最近一个月
<span class="layuiadmin-span-color">15% <i class="layui-inline layui-icon layui-icon-user"></i></span>
搜索引擎抓取
<span class="layuiadmin-span-color">累计 {$stats.total.spider} <i class="layui-inline layui-icon layui-icon-chart-screen"></i></span>
</p>
</div>
</div>
@@ -66,229 +195,78 @@
<div class="layui-col-sm12">
<div class="layui-card">
<div class="layui-card-header">
访问量
<div class="layui-btn-group layuiadmin-btn-group">
<a href="javascript:;" class="layui-btn layui-btn-primary layui-btn-xs">去年</a>
<a href="javascript:;" class="layui-btn layui-btn-primary layui-btn-xs">今年</a>
</div>
近 7 天趋势
<span class="layui-badge layui-bg-blue layuiadmin-badge">会员注册 / 后台操作</span>
</div>
<div class="layui-card-body">
<div class="layui-row">
<div class="layui-col-sm8">
<div class="layui-carousel layadmin-carousel layadmin-dataview" data-anim="fade" lay-filter="LAY-index-pagetwo">
<div carousel-item id="LAY-index-pagetwo">
<div><i class="layui-icon layui-icon-loading1 layadmin-loading"></i></div>
</div>
</div>
</div>
<div class="layui-col-sm4">
<div class="layuiadmin-card-list">
<p class="layuiadmin-normal-font">月访问数</p>
<span>同上期增长</span>
<div class="layui-progress layui-progress-big" lay-showPercent="yes">
<div class="layui-progress-bar" lay-percent="30%"></div>
</div>
</div>
<div class="layuiadmin-card-list">
<p class="layuiadmin-normal-font">月下载数</p>
<span>同上期增长</span>
<div class="layui-progress layui-progress-big" lay-showPercent="yes">
<div class="layui-progress-bar" lay-percent="20%"></div>
</div>
</div>
<div class="layuiadmin-card-list">
<p class="layuiadmin-normal-font">月点赞数</p>
<span>同上期增长</span>
<div class="layui-progress layui-progress-big" lay-showPercent="yes">
<div class="layui-progress-bar" lay-percent="25%"></div>
</div>
</div>
<div class="layui-carousel layadmin-carousel layadmin-dataview" data-anim="fade" lay-filter="LAY-index-trendview">
<div carousel-item id="LAY-index-trend">
<div><i class="layui-icon layui-icon-loading1 layadmin-loading"></i></div>
</div>
</div>
</div>
</div>
</div>
<div class="layui-col-sm4">
<div class="layui-col-sm6">
<div class="layui-card">
<div class="layui-card-header">用户留言</div>
<div class="layui-card-header">最新会员</div>
<div class="layui-card-body">
<ul class="layuiadmin-card-status layuiadmin-home2-usernote">
<li>
<h3>张三</h3>
<p>哦好吧</p>
<span>5月30日 00:00</span>
<a href="javascript:;" layadmin-event="replyNote" data-id="7" class="layui-btn layui-btn-xs layuiadmin-reply">回复</a>
</li>
<li>
<h3>李白</h3>
<p>应是天仙狂醉,乱把白云揉碎</p>
<span>5月02日 00:00</span>
<a href="javascript:;" layadmin-event="replyNote" data-id="5" class="layui-btn layui-btn-xs layuiadmin-reply">回复</a>
</li>
<li>
<h3>张三</h3>
<p>嗯嗯</p>
<span>5月11日 00:00</span>
<a href="javascript:;" layadmin-event="replyNote" data-id="6" class="layui-btn layui-btn-xs layuiadmin-reply">回复</a>
</li>
<li>
<h3>杜甫</h3>
<p>人才虽高,不务学问,不能致圣。刘向十日画一水,五日画一石。</p>
<span>4月11日 00:00</span>
<a href="javascript:;" layadmin-event="replyNote" data-id="2" class="layui-btn layui-btn-xs layuiadmin-reply">回复</a>
</li>
<li>
<h3>鲁迅</h3>
<p>路本是无所谓有和无的,走的人多了,就没路了。。</p>
<span>4月28日 00:00</span>
<a href="javascript:;" layadmin-event="replyNote" data-id="4" class="layui-btn layui-btn-xs layuiadmin-reply">回复</a>
</li>
<li>
<h3>张爱玲</h3>
<p>于千万人之中遇到你所要遇到的人,于千万年之中,时间的无涯的荒野中,没有早一步,也没有晚一步,刚巧赶上了,那也没有别的话好说,唯有轻轻的问一声:“噢,原来你也在这里?”</p>
<span>4月11日 00:00</span>
<a href="javascript:;" layadmin-event="replyNote" data-id="1" class="layui-btn layui-btn-xs layuiadmin-reply">回复</a>
</li>
</ul>
<table class="layui-table layuiadmin-page-table" lay-skin="line">
<thead>
<tr>
<th>账号</th>
<th>昵称</th>
<th>注册时间</th>
</tr>
</thead>
<tbody>
{empty name="stats.members"}
<tr><td colspan="3" style="text-align:center;color:#999;">暂无数据</td></tr>
{/empty}
{foreach $stats.members as $m}
<tr>
<td>{$m.account}</td>
<td>{$m.nickname}</td>
<td>{$m.create_at|date='m-d H:i'}</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-sm8">
<div class="layui-col-sm6">
<div class="layui-row layui-col-space15">
<div class="layui-col-sm6">
<div class="layui-col-sm12">
<div class="layui-card">
<div class="layui-card-header">本周活跃用户列表</div>
<div class="layui-card-header">最近操作日志</div>
<div class="layui-card-body">
<table class="layui-table layuiadmin-page-table" lay-skin="line">
<thead>
<tr>
<th>用户名</th>
<th>最后登录时间</th>
<th>状态</th>
<th>获得赞</th>
<th>管理员</th>
<th>操作</th>
<th>IP</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{empty name="stats.logs"}
<tr><td colspan="4" style="text-align:center;color:#999;">暂无数据</td></tr>
{/empty}
{foreach $stats.logs as $l}
<tr>
<td><span class="first">张三</span></td>
<td><i class="layui-icon layui-icon-log"> 11:20</i></td>
<td><span>在线</span></td>
<td>22 <i class="layui-icon layui-icon-praise"></i></td>
</tr>
<tr>
<td><span class="second">张三</span></td>
<td><i class="layui-icon layui-icon-log"> 10:40</i></td>
<td><span>在线</span></td>
<td>21 <i class="layui-icon layui-icon-praise"></i></td>
</tr>
<tr>
<td><span class="third">李四</span></td>
<td><i class="layui-icon layui-icon-log"> 01:30</i></td>
<td><i>离线</i></td>
<td>66 <i class="layui-icon layui-icon-praise"></i></td>
</tr>
<tr>
<td>王五</td>
<td><i class="layui-icon layui-icon-log"> 21:18</i></td>
<td><i>离线</i></td>
<td>45 <i class="layui-icon layui-icon-praise"></i></td>
</tr>
<tr>
<td>许上进</td>
<td><i class="layui-icon layui-icon-log"> 09:30</i></td>
<td><span>在线</span></td>
<td>21 <i class="layui-icon layui-icon-praise"></i></td>
</tr>
<tr>
<td>小蚊子</td>
<td><i class="layui-icon layui-icon-log"> 21:18</i></td>
<td><i>在线</i></td>
<td>45 <i class="layui-icon layui-icon-praise"></i></td>
</tr>
<tr>
<td>张三</td>
<td><i class="layui-icon layui-icon-log"> 09:30</i></td>
<td><span>在线</span></td>
<td>21 <i class="layui-icon layui-icon-praise"></i></td>
<td>{$l.admin}</td>
<td>{$l.action}</td>
<td>{$l.ip}</td>
<td>{$l.time|date='m-d H:i'}</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-sm6">
<div class="layui-card">
<div class="layui-card-header">项目进展</div>
<div class="layui-card-body">
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<table id="LAY-index-prograss"></table>
</div>
</div>
</div>
</div>
</div>
<div class="layui-col-sm12">
<div class="layui-card">
<div class="layui-card-header">访客地区分布</div>
<div class="layui-card-body">
<div class="layui-row layui-col-space15">
<div class="layui-col-sm4">
<table class="layui-table layuiadmin-page-table" lay-skin="line">
<thead>
<tr>
<th>排名</th>
<th>地区</th>
<th>人数</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>浙江</td>
<td>62310</td>
</tr>
<tr>
<td>2</td>
<td>上海</td>
<td>59190</td>
</tr>
<tr>
<td>3</td>
<td>广东</td>
<td>55891</td>
</tr>
<tr>
<td>4</td>
<td>北京</td>
<td>51919</td>
</tr>
<tr>
<td>5</td>
<td>山东</td>
<td>39231</td>
</tr>
<tr>
<td>6</td>
<td>湖北</td>
<td>37109</td>
</tr>
</tbody>
</table>
</div>
<div class="layui-col-sm8">
<div class="layui-carousel layadmin-carousel layadmin-dataview" data-anim="fade" lay-filter="LAY-index-pagethree">
<div carousel-item id="LAY-index-pagethree">
<div><i class="layui-icon layui-icon-loading1 layadmin-loading"></i></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -296,6 +274,28 @@
</div>
</div>
<script src="/assets/layui/layui.js?t=1"></script>
<script type="text/javascript">
// 本页会再次加载 layui,覆盖掉 layout 里注入的 layui.app(含当前登录者权限集 powers)。
// 必须在这里补回来:auth 模块在加载时读 layui.app.powers,拿不到就会把
// 所有带 [data-perm] 的按钮当成无权限移除(控制台的清理按钮会消失)。
layui.app = {
root: "{$site.root|default=''}",
assetUrl: "/assets",
module: "{$site.module}",
app: "{$site.app}",
routeBase: "{$route_base}",
controller: "{$site.controller}",
action: "{$site.action}",
powers: {:json_encode($powers ?? [])}
};
</script>
<script type="text/javascript">
// 首页下半区真实统计(Console::collectStats 产出),给 console.js 画趋势图用。
// json_encode 带 HEX_* 选项:模板 default_filter 会把 <>&'" 转义成实体,
// 先转成 \u003C 这类 unicode 才能保证丢到 JS 里仍是合法 JSON。
// 第二个参数 15 = JSON_HEX_TAG|HEX_AMP|HEX_APOS|HEX_QUOT(这里不能写竖线,模板把它当修饰符分隔符)
layui.stats = {:json_encode($stats ?? [], 15)};
</script>
<script src="/assets/ywxapp/ywxapp.js" module="backend"></script>
<script>
layui.use('console');