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

@@ -0,0 +1,144 @@
<div class="layui-card">
<div class="layui-card-header">主框架版本管理(升级服务器)</div>
<div class="layui-card-body">
<blockquote class="layui-elem-quote layui-quote-nm" style="margin-bottom:12px;">
同一版本号可先后上传「整包」「增量补丁」「完整安装包」,三者共存于同一条版本记录;
客户端升级时自动择优(正好停在补丁基础版本走补丁,否则走整包),完整安装包供全新部署下载。重新上传任一包后需再次「发布」。
</blockquote>
<form class="layui-form" id="uploadForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">版本号</label>
<div class="layui-input-inline">
<input type="text" name="version" placeholder="x.y.z" class="layui-input" required>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">标题</label>
<div class="layui-input-inline">
<input type="text" name="title" placeholder="可选,如 安全修复" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">类型</label>
<div class="layui-input-inline">
<select name="type" id="pkgType" class="layui-input">
<option value="0">整包(full)</option>
<option value="1">增量补丁(patch)</option>
<option value="2">完整安装包(install)</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">基础版本</label>
<div class="layui-input-inline">
<input type="text" name="from_version" id="fromVersion" placeholder="仅增量补丁需要,如 1.2.0" class="layui-input">
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn" id="btnUpload">
<i class="layui-icon layui-icon-upload"></i> 上传核心包
</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">更新日志</label>
<div class="layui-input-block">
<textarea name="changelog" class="layui-textarea" placeholder="更新内容..."></textarea>
</div>
</div>
</form>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script>
layui.use(['table', 'upload', 'jquery', 'layer'], function () {
var table = layui.table, upload = layui.upload, $ = layui.jquery, layer = layui.layer;
table.render({
elem: '#dataTable',
url: '{:url("/appmall/backend/framework/index")}',
page: true,
cols: [[
{field: 'id', title: 'ID', width: 70},
{field: 'version', title: '版本号', width: 120},
{title: '包', width: 200, templet: function (d) {
var h = '';
h += d.file_path ? '<span class="layui-badge layui-bg-blue">整包</span> '
: '<span class="layui-badge layui-bg-gray">无整包</span> ';
h += d.patch_path ? '<span class="layui-badge layui-bg-orange">补丁</span> ' : '';
h += d.install_path ? '<span class="layui-badge layui-bg-cyan">安装包</span>' : '';
return h;
}},
{field: 'patch_from', title: '补丁基础版本', width: 110, templet: function (d) {
return d.patch_from || d.from_version || '';
}},
{field: 'title', title: '标题'},
{field: 'changelog', title: '更新日志'},
{field: 'status', title: '状态', width: 90, templet: function (d) {
return d.status == 1
? '<span class="layui-badge layui-bg-green">已发布</span>'
: '<span class="layui-badge">草稿</span>';
}},
{field: 'download_count', title: '下载数', width: 90},
{title: '操作', width: 360, templet: function (d) {
var h = '';
if (d.status != 1) h += '<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="publish">发布</a>';
if (d.file_path) h += '<a class="layui-btn layui-btn-xs" lay-event="download">下载整包</a>';
if (d.patch_path) h += '<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="downloadPatch">下载补丁</a>';
if (d.install_path) h += '<a class="layui-btn layui-btn-xs layui-bg-cyan" lay-event="downloadInstall">下载安装包</a>';
h += '<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>';
return h;
}}
]]
});
upload.render({
elem: '#btnUpload',
url: '{:url("/appmall/backend/framework/upload")}',
accept: 'file',
exts: 'zip',
before: function () {
if (!$('input[name="version"]').val()) {
layer.msg('请先填写版本号'); return false;
}
if ($('#pkgType').val() == '1' && !$('#fromVersion').val()) {
layer.msg('增量补丁必须填写基础版本'); return false;
}
},
data: {
version: function () { return $('input[name="version"]').val(); },
title: function () { return $('input[name="title"]').val(); },
changelog: function () { return $('textarea[name="changelog"]').val(); },
type: function () { return $('#pkgType').val(); },
from_version: function () { return $('#fromVersion').val(); }
},
done: function (res) {
layer.msg(res.message || '完成');
if (res.code === 0) table.reload('dataTable');
},
error: function () { layer.msg('上传失败'); }
});
table.on('tool(dataTable)', function (obj) {
var d = obj.data;
if (obj.event === 'publish') {
$.post('{:url("/appmall/backend/framework/publish")}', {id: d.id}, function (r) {
layer.msg(r.message); if (r.code === 0) table.reload('dataTable');
});
} else if (obj.event === 'download') {
location.href = '{:url("/appmall/backend/framework/download")}?id=' + d.id + '&type=full';
} else if (obj.event === 'downloadPatch') {
location.href = '{:url("/appmall/backend/framework/download")}?id=' + d.id + '&type=patch';
} else if (obj.event === 'downloadInstall') {
location.href = '{:url("/appmall/backend/framework/download")}?id=' + d.id + '&type=install';
} else if (obj.event === 'del') {
layer.confirm('确定删除该版本?', function () {
$.post('{:url("/appmall/backend/framework/delete")}', {id: d.id}, function (r) {
layer.msg(r.message); if (r.code === 0) table.reload('dataTable');
});
});
}
});
});
</script>