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

+102 -26
View File
@@ -15,6 +15,37 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
// 兼容旧键名(历史版本曾写入 'menu')
layui.data(appName, { key: 'menu', remove: true });
}
// 安装/升级失败的可读化:后端 AddonService::validateDependencies() 收集式校验后,
// 会在 data.problems 里带上完整清单(缺哪些依赖、该装还是该升),这里逐条展示并给
// 「去应用市场安装」入口,避免只弹出一长串难以扫读的文案。
function showDependencyProblems(res) {
var data = res && res.data;
var list = (data && data.problems) || [];
if (!list.length) { return false; }
var badgeText = { addon: '插件', extension: 'PHP 扩展', framework: '框架' };
var rows = list.map(function (it) {
var badge = badgeText[it.type] || '依赖';
return '<li style="padding:6px 0;border-bottom:1px dashed #eee;">'
+ '<span class="layui-badge layui-bg-cyan" style="margin-right:6px;">' + badge + '</span>'
+ $('<div>').text(it.text).html()
+ '</li>';
}).join('');
var missing = (data && data.missing_addons) || [];
var extra = missing.length
? '<div style="margin-top:12px;"><a class="layui-btn layui-btn-normal layui-btn-xs" href="market" target="_blank">'
+ '去应用市场安装缺失插件(' + $('<span>').text(missing.join('、')).html() + ')</a></div>'
: '';
layer.open({
type: 1,
title: '依赖检查未通过(' + list.length + ' 项)',
area: ['620px', 'auto'],
content: '<div style="padding:16px 18px;font-size:13px;">'
+ '<ul style="list-style:none;margin:0;padding:0;">' + rows + '</ul>' + extra + '</div>',
btn: ['知道了']
});
return true;
}
// ===== 渲染核心表格 =====
var { access_token, refresh_token } = layui.setter.getTokens();
table.render({
@@ -24,7 +55,7 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
{ field: 'title', title: '插件名称', minWidth: 180 },
{ field: 'name', title: '标识', width: 120 },
{ field: 'author', title: '开发者', width: 120 }
, { field: 'version', title: '版本号', width: 100, align: 'center' }
, { field: 'version', title: '版本号', width: 170, align: 'center', templet: '#versionTpl' }
, { field: 'description', title: '功能描述', minWidth: 250 }
, { field: 'status', title: '运行状态', width: 110, align: 'center', templet: '#switchTpl' } // 绑定开关模板
, { fixed: 'right', title: '操作', toolbar: '#operateTpl', width: 300, align: 'right' } // 绑定操作栏模板
@@ -42,14 +73,8 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
}
, done: function (res, curr, count) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
// 开发模式关闭时隐藏操作列的「设计」按钮
// if (!ADDON_DEV) { $('.dev-design').remove(); }
}
@@ -79,6 +104,10 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; }
location.href = 'setting?addon=' + encodeURIComponent(addonName);
break;
case 'upgrade':
if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; }
active.upgrade(addonName, data.latest_version || '');
break;
case 'uninstall':
if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; }
active.uninstall(addonName);
@@ -146,22 +175,69 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
// 顶部功能按钮 + 行内操作共用
var active = {
// 一键升级到市场最新版(version 为空时由服务端取最新版)
upgrade: function (name, version) {
var loadIdx = layer.load();
// 升级含下载+备份+覆盖,放宽超时到 10 分钟,避免半途被中断
http.post('upgrade', { name: name, version: version }, { timeout: 600000 })
.then(function (res) {
layer.close(loadIdx);
if (res.code === 0) {
layer.msg(res.message || '升级成功', { icon: 1 });
clearMenuCache(); // 插件菜单可能变更
table.reload('dataTable');
} else if (!showDependencyProblems(res)) {
layer.msg(res.message || '升级失败', { icon: 2 });
}
})
.catch(function () {
layer.close(loadIdx);
layer.msg('请求失败', { icon: 2 });
});
},
uninstall: function (name) {
layer.prompt({
formType: 1
, title: '敏感操作,请验证口令'
}, function (value, index) {
layer.close(index);
http.delete('uninstall', { name: name })
.then((res) => {
if (res.code === 0) {
layer.close(index);
clearMenuCache(); // 插件菜单已删除,清掉侧栏菜单缓存
location.reload(); // 重新加载整页,侧栏立即刷新
} else {
layer.msg(res.msg || '操作失败');
}
});
// 卸载选项:是否备份数据 / 是否删除数据表(对应 AddonService::uninstall() 的 options)
layer.open({
type: 1,
title: '卸载插件:' + name,
area: ['460px', 'auto'],
content: '<div style="padding:16px 18px;">'
+ '<label style="display:block;margin-bottom:10px;">'
+ '<input type="checkbox" id="unin_backup" checked> 卸载前备份数据(SQL 落到 runtime/addon/backup/)'
+ '</label>'
+ '<label style="display:block;">'
+ '<input type="checkbox" id="unin_delete" checked> 同时 DROP 插件数据表(不勾选则保留数据)'
+ '</label>'
+ '<div class="layui-word-aux" style="margin-top:10px;">'
+ '提示:删除数据表后数据不可恢复,建议保持「备份数据」勾选。</div>'
+ '</div>',
btn: ['下一步', '取消'],
yes: function (index) {
var opts = {
backup_data: document.getElementById('unin_backup').checked ? 1 : 0,
delete_data: document.getElementById('unin_delete').checked ? 1 : 0
};
layer.close(index);
layer.prompt({
formType: 1
, title: '敏感操作,请验证口令'
}, function (value, index2) {
layer.close(index2);
http.delete('uninstall', {
name: name,
backup_data: opts.backup_data,
delete_data: opts.delete_data
})
.then((res) => {
if (res.code === 0) {
clearMenuCache(); // 插件菜单已删除,清掉侧栏菜单缓存
location.reload(); // 重新加载整页,侧栏立即刷新
} else {
layer.msg(res.msg || '操作失败');
}
});
}
}
});
},
batchDel: function () { // 批量卸载
@@ -206,8 +282,8 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
layer.msg('插件安装成功!', { icon: 1 });
clearMenuCache(); // 插件菜单已变化,清掉侧栏菜单缓存
location.reload(); // 重新加载整页,侧栏立即刷新
} else {
layer.msg('安装失败: ' + res.message, { icon: 2 });
} else if (!showDependencyProblems(res)) {
layer.msg('安装失败: ' + (res.message || '未知错误'), { icon: 2 });
}
},
error: function () {