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

+227 -254
View File
@@ -1,10 +1,18 @@
/**
* console - homepage demo
* 控制台首页
*
* 分工:
* - 后端渲染:系统概览 card、维护按钮、框架升级按钮、统计 card、最新会员 / 最近操作日志表格
* (数据来自 Console::collectStats() 与 SystemInfo::collect())
* - 前端只做两件必须异步的事:近 7 天趋势图(ECharts)、两个 flush 类接口的按钮事件
*
* 原 layuiAdmin 模板自带的演示数据已全部清掉:球员数据雷达图、中国地图、
* 假留言列表、假活跃用户、假地区分布、模拟接口的「项目进展」表格——
* 这些户型要么无数据源,要么与真数据权限重复(运行环境表已覆盖)。
*/
layui.define('ywxapp',function (exports) {
layui.define('ywxapp', function (exports) {
var ywxapp = layui.ywxapp;
//区块轮播切换
layui.use(['ywxapp', 'carousel'], function () {
var $ = layui.$
@@ -31,265 +39,230 @@ layui.define('ywxapp',function (exports) {
});
//八卦新闻
// 近 7 天趋势:会员注册 / 后台操作(真实数据)
//
// 数据由后端渲染进 layui.stats(见 console/index.html 的 json_encode),
// 这里直接取,不再额外发请求——首页是每人的第一屏,少一个请求少一处等待。
layui.use(['carousel', 'echarts'], function () {
var $ = layui.$
, carousel = layui.carousel
, echarts = layui.echarts;
var echartsApp = [], options = [
{
title: {
subtext: '完全实况球员数据',
textStyle: {
fontSize: 14
}
},
tooltip: {
trigger: 'axis'
},
legend: {
x: 'left',
data: ['罗纳尔多', '舍普琴科']
},
polar: [
{
indicator: [
{ text: '进攻', max: 100 },
{ text: '防守', max: 100 },
{ text: '体能', max: 100 },
{ text: '速度', max: 100 },
{ text: '力量', max: 100 },
{ text: '技巧', max: 100 }
],
radius: 130
}
],
series: [
{
type: 'radar',
center: ['50%', '50%'],
itemStyle: {
normal: {
areaStyle: {
type: 'default'
}
}
},
data: [
{ value: [97, 42, 88, 94, 90, 86], name: '舍普琴科' },
{ value: [97, 32, 74, 95, 88, 92], name: '罗纳尔多' }
]
}
]
}
]
, elemDataView = $('#LAY-index-pageone').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme);
echartsApp[index].setOption(options[index]);
window.onresize = echartsApp[index].resize;
};
//没找到DOM,终止执行
if (!elemDataView[0]) return;
var stats = layui.stats || {};
var series = stats.series || {};
renderDataView(0);
});
//访问量
layui.use(['carousel', 'echarts'], function () {
var $ = layui.$
, carousel = layui.carousel
, echarts = layui.echarts;
var echartsApp = [], options = [
{
tooltip: {
trigger: 'axis'
},
calculable: true,
legend: {
data: ['访问量', '下载量', '平均访问量']
},
xAxis: [
{
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
}
],
yAxis: [
{
type: 'value',
name: '访问量',
axisLabel: {
formatter: '{value} 万'
}
},
{
type: 'value',
name: '下载量',
axisLabel: {
formatter: '{value} 万'
}
}
],
series: [
{
name: '访问量',
type: 'line',
data: [900, 850, 950, 1000, 1100, 1050, 1000, 1150, 1250, 1370, 1250, 1100]
},
{
name: '下载量',
type: 'line',
yAxisIndex: 1,
data: [850, 850, 800, 950, 1000, 950, 950, 1150, 1100, 1240, 1000, 950]
},
{
name: '平均访问量',
type: 'line',
data: [870, 850, 850, 950, 1050, 1000, 980, 1150, 1000, 1300, 1150, 1000]
}
]
}
]
, elemDataView = $('#LAY-index-pagetwo').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme);
echartsApp[index].setOption(options[index]);
window.onresize = echartsApp[index].resize;
};
//没找到DOM,终止执行
if (!elemDataView[0]) return;
renderDataView(0);
});
//地图
layui.use(['carousel', 'echarts'], function () {
var $ = layui.$
, carousel = layui.carousel
, echarts = layui.echarts;
var echartsApp = [], options = [
{
title: {
text: '访客地区分布',
subtext: '不完全统计'
},
tooltip: {
trigger: 'item'
},
dataRange: {
orient: 'horizontal',
min: 0,
max: 60000,
text: ['高', '低'],
splitNumber: 0
},
series: [
{
name: '访客地区分布',
type: 'map',
mapType: 'china',
selectedMode: 'multiple',
itemStyle: {
normal: { label: { show: true } },
emphasis: { label: { show: true } }
},
data: [
{ name: '西藏', value: 60 },
{ name: '青海', value: 167 },
{ name: '宁夏', value: 210 },
{ name: '海南', value: 252 },
{ name: '甘肃', value: 502 },
{ name: '贵州', value: 570 },
{ name: '新疆', value: 661 },
{ name: '云南', value: 8890 },
{ name: '重庆', value: 10010 },
{ name: '吉林', value: 5056 },
{ name: '山西', value: 2123 },
{ name: '天津', value: 9130 },
{ name: '江西', value: 10170 },
{ name: '广西', value: 6172 },
{ name: '陕西', value: 9251 },
{ name: '黑龙江', value: 5125 },
{ name: '内蒙古', value: 1435 },
{ name: '安徽', value: 9530 },
{ name: '北京', value: 51919 },
{ name: '福建', value: 3756 },
{ name: '上海', value: 59190 },
{ name: '湖北', value: 37109 },
{ name: '湖南', value: 8966 },
{ name: '四川', value: 31020 },
{ name: '辽宁', value: 7222 },
{ name: '河北', value: 3451 },
{ name: '河南', value: 9693 },
{ name: '浙江', value: 62310 },
{ name: '山东', value: 39231 },
{ name: '江苏', value: 35911 },
{ name: '广东', value: 55891 }
]
}
]
}
]
, elemDataView = $('#LAY-index-pagethree').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme);
echartsApp[index].setOption(options[index]);
window.onresize = echartsApp[index].resize;
};
//没找到DOM,终止执行
if (!elemDataView[0]) return;
renderDataView(0);
});
//项目进展
layui.use('table', function () {
var $ = layui.$
, table = layui.table;
table.render({
elem: '#LAY-index-prograss'
, url: layui.setter.paths.base + 'json/console/prograss.js' //模拟接口
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'prograss', title: '任务' }
, { field: 'time', title: '所需时间' }
, {
field: 'complete', title: '完成情况'
, templet: function (d) {
if (d.complete == '已完成') {
return '<del style="color: #16b777;">' + d.complete + '</del>'
} else if (d.complete == '进行中') {
return '<span style="color: #FFB800;">' + d.complete + '</span>'
} else {
return '<span style="color: #FF5722;">' + d.complete + '</span>'
}
}
var option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['会员注册', '后台操作']
},
grid: {
left: '3%'
, right: '4%'
, bottom: '3%'
, containLabel: true
},
xAxis: [
{
type: 'category'
, boundaryGap: false
, data: stats.dates || []
}
]]
, skin: 'line'
],
yAxis: [
{
type: 'value'
// 都是条数,不该出现 0.5 这种刻度
, minInterval: 1
}
],
series: [
{
name: '会员注册'
, type: 'line'
, smooth: true
, areaStyle: {}
, data: series.member || []
},
{
name: '后台操作'
, type: 'line'
, smooth: true
, areaStyle: {}
, data: series.log || []
}
]
};
var elemDataView = $('#LAY-index-trend').children('div');
//没找到DOM,终止执行
if (!elemDataView[0]) return;
var chart = echarts.init(elemDataView[0], layui.echartsTheme);
chart.setOption(option);
window.onresize = chart.resize;
});
// 清理缓存(控制台顶部「维护」卡片)
//
// 接口:Console::clean(POST /admin/console/clean)
// 权限:console:clean,无权限时 auth.filterDom() 直接把按钮摘掉。
// 「系统信息」页与 System 控制器已下线,清理能力收进 Console(本页),
// 按钮 id 保持 sys-* 不变,只换 URL 与权限名。
layui.use(['layer', 'element', 'auth', 'http', 'jquery'], function () {
var $ = layui.$
, layer = layui.layer
, http = layui.http
, auth = layui.auth;
auth.filterDom('.layui-btn[data-perm]');
function clean(type, tip) {
layer.confirm(tip, {title: '提示'}, function (index) {
layer.close(index);
var loading = layer.load(2);
http.post('/admin/console/clean', {type: type}).then(function (res) {
layer.close(loading);
if (res && res.code === 0) {
layer.msg(res.message || '清理完成', {icon: 1});
setTimeout(function () { location.reload(); }, 800);
} else {
layer.msg((res && res.message) || '清理失败', {icon: 2});
}
}).catch(function (err) {
layer.close(loading);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
});
}
$('#sys-clean-cache').on('click', function () {
clean('cache', '确定清理数据缓存(runtime/cache)?');
});
$('#sys-clean-temp').on('click', function () {
clean('temp', '确定清理模板编译缓存(runtime/temp)?');
});
$('#sys-clean-all').on('click', function () {
clean('all', '确定全量清理(数据缓存 + 模板缓存 + 路由缓存)?');
});
});
console.log('进入控制台',layui.ywxapp);
//回复留言
ywxapp.events.replyNote = function (othis) {
var nid = othis.data('id');
layer.prompt({
title: '回复留言 ID:' + nid
, formType: 2
}, function (value, index) {
//这里可以请求 Ajax
//…
layer.msg('得到:' + value);
layer.close(index);
// 框架升级(与「系统管理 → 框架升级」同源)
//
// 接口:/admin/framework/check(GET 检测)、/admin/framework/upgrade(POST 执行)
// 权限:framework:check / framework:upgrade,无权限时 auth.filterDom() 移除按钮。
// 本页目录是 /admin/console/,所以 URL 必须写绝对路径。
layui.use(['layer', 'element', 'auth', 'http', 'jquery'], function () {
var $ = layui.$
, layer = layui.layer
, http = layui.http
, auth = layui.auth;
auth.filterDom('.layui-btn[data-perm]');
var pkgType = 'auto';
function setDisabled($el, dis) {
if (dis) {
$el.addClass('layui-btn-disabled').prop('disabled', true);
} else {
$el.removeClass('layui-btn-disabled').prop('disabled', false);
}
}
function status(html) {
$('#fw-status').html(html);
}
function loading(text) {
status('<i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate"></i> ' + text);
}
$('#fw-pkg-group').on('click', '.fw-pkg', function () {
if ($(this).prop('disabled')) return;
pkgType = $(this).data('type');
$('#fw-pkg-group .fw-pkg').removeClass('layui-btn-normal').addClass('layui-btn-primary');
$(this).removeClass('layui-btn-primary').addClass('layui-btn-normal');
});
};
function check() {
loading('正在检查...');
// 控制台是首屏:检测限 8 秒,中心站不可达时不拖住整页
http.get('/admin/framework/check', {}, {timeout: 8000}).then(function (res) {
if (!res || res.code !== 0) {
status('<span style="color:#dc2626;">' + ((res && res.message) || '检查失败') + '</span>');
return;
}
var d = res.data;
if (d.current) {
$('#fw-current').text(d.current);
}
if (!d.has_update) {
status('<span style="color:#16a34a;">✓ 已是最新版本(' + d.current + ')</span>');
$('#fw-upgrade').hide();
$('#fw-pkg-box').hide();
return;
}
var html = '发现新版本:<b>' + d.latest + '</b>'
+ ' <span class="layui-badge layui-bg-orange">自动模式将使用'
+ (d.use_patch ? '增量补丁' : '整包') + '</span>';
if (d.use_patch && d.patch_from) html += '(基于 ' + d.patch_from + ')';
if (d.title) html += '(' + d.title + ')';
if (d.error) html += '<br><span style="color:#dc2626;">' + d.error + '</span>';
html += '<br>更新内容:<pre style="margin-top:6px;">' + (d.changelog || '') + '</pre>';
status(html);
if (d.error) {
$('#fw-upgrade').hide();
$('#fw-pkg-box').hide();
return;
}
$('#fw-upgrade').show();
$('#fw-pkg-box').show();
pkgType = 'auto';
$('#fw-pkg-group .fw-pkg').removeClass('layui-btn-normal').addClass('layui-btn-primary');
$('#fw-pkg-group .fw-pkg[data-type=auto]').removeClass('layui-btn-primary').addClass('layui-btn-normal');
setDisabled($('#fw-pkg-group .fw-pkg[data-type=full]'), !d.has_full);
setDisabled($('#fw-pkg-group .fw-pkg[data-type=patch]'), !d.use_patch);
var tips = [];
if (!d.has_full) tips.push('当前无完整包可用');
if (!d.use_patch) tips.push('当前版本(' + (d.current || '') + ')不支持增量小包,需基于 ' + (d.patch_from || '?') + ')');
$('#fw-pkg-tip').text(tips.length ? '提示:' + tips.join(';') + ',建议选「自动」' : '可手动选择完整包或增量小包');
}).catch(function (err) {
status('<span style="color:#dc2626;">请求失败:' + (err && err.message ? err.message : '网络异常') + '</span>');
});
}
$('#fw-check').on('click', check);
$('#fw-upgrade').on('click', function () {
var label = {auto: '自动模式', full: '完整包', patch: '增量小包'}[pkgType] || pkgType;
layer.confirm('将以【' + label + '】升级,将备份当前核心并覆盖,确定继续?', {title: '提示'}, function (index) {
layer.close(index);
loading('正在下载并升级...');
// 下载 + 备份 + 覆盖可能很久,放宽到 10 分钟,别在半途被超时打断
http.post('/admin/framework/upgrade', {type: pkgType}, {timeout: 600000}).then(function (res) {
if (res && res.code === 0) {
status('<span style="color:#16a34a;">✓ ' + res.message + '</span>');
$('#fw-upgrade').hide();
$('#fw-pkg-box').hide();
if (res.data && res.data.version) {
$('#fw-current').text(res.data.version);
}
} else {
status('<span style="color:#dc2626;">' + ((res && res.message) || '升级失败') + '</span>');
}
}).catch(function (err) {
status('<span style="color:#dc2626;">请求失败:' + (err && err.message ? err.message : '网络异常') + '</span>');
});
});
});
check();
});
exports('console', {})
});
});