Files
YwxAppThink/public/static/backend/modules/admin.js
T
yangwx 18101cf11a 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(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

387 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
var $ = layui.$
, table = layui.table
, form = layui.form
, layer = layui.layer
, laydate = layui.laydate
, util = layui.util
, http = layui.http
, laytpl = layui.laytpl; var auth = layui.auth;
// 使用登录时后端返回的真实权限(已存入 layui.data),并以当前控制器解析裸动作 token
var _savedPerms = (layui.data('backend').permission) || [];
auth.init(_savedPerms);
auth.setController('admin');
var dataTable = table.render({
elem: '#dataTable'
, url: "index" // 后端数据接口
, page: true
, limit: 15
, toolbar: '#tableBar'
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', title: 'ID', width: 72, align: 'center', sort: true, fixed: 'left' }
, { field: 'account', title: '账户', width: 120, align: 'center', fixed: 'left' }
, { field: 'nickname', title: '昵称', width: 100, align: 'center' }
// , { field: 'role_id', title: '角色', width: 100, align: 'center', templet: '#roleTpl' }
, { field: 'email', title: '邮箱', minWidth: 180, align: 'center' }
, { field: 'mobile', title: '手机', width: 120, align: 'center' }
, { field: 'login_info', title: '登录信息', width: 160, align: 'center', templet: '#loginInfoTpl' }
, { field: 'status', title: '状态', width: 90, align: 'center', templet: '#statusTpl', unresize: true }
, { field: 'create_at', title: '创建时间', width: 160, align: 'center', sort: true }
, { title: '操作', width: 120, align: 'left', toolbar: '#dataBar', fixed: 'right', unresize: true }
]],
done: function (res, curr, count) {
// 行内按钮按真实权限显隐(data-perm 为完整权限 key,如 admin:edit)
auth.filterDom($('#dataTable').next('.layui-table-view'));
}
});
// 搜索
form.on('submit(searchBtn)', function (data) {
adminTable.reload({
where: data.field,
page: { curr: 1 }
});
return false;
});
// 状态开关
form.on('switch(statusSwitch)', function (obj) {
var id = this.value;
var status = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (status ? '启用' : '禁用') + '该管理员账号吗?', function (index) {
http.post('update', { id: id, status: status }
).then(function (res) {
if (res && res.code === 0) {
layer.msg(res.msg, { icon: 1 });
table.reload('dataTable', {}, true);
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
}).catch(function (err) {
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
});
layer.close(index);
}, function () {
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
});
});
table.on('toolbar(dataTable)', function (elem) {
switch (elem.event) {
case 'dataCreate':
active.dataCreate({ pid: 0, type: 1 });
break;
case 'dataDelete':
var checkStatus = table.checkStatus('dataTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
active.dataDelete(ids);
break;
case 'dataRecybin':
active.dataRecybin();
break;
};
});
table.on('tool(dataTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'update':
active.dataEdit(data);
break;
case 'delete':
active.dataDelete([data.id]);
break;
}
});
var dataFromFun = function (data, callback, done) {
var title = data ? '编辑数据[' + data.account + ']' : '添加数据';
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
layer.open({
title: data.id ? '编辑菜单' : (data.pid ? '添加子菜单' : '添加根菜单'),
content: formHtml,
anim: "slideLeft",
offset: "r",
btnAlign: "l",
area: ['50% ', '99%'],
shade: 0.1,
shadeClose: true,
btn: ['确定', '取消'],
success: function (layero, index) {
callback(layero, index);
form.render();
},
yes: function (index, layero) {
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
done(layero, index, elem);
layui.off('submit(wxapp-form)', 'from');
return false;
});
layero.contents().find("#wxapp-form-submit").trigger('click');
}
});
};
//事件
var active = {
dataDelete: function (ids, force = 0) {
if (ids.length === 0) {
return layer.msg('请选择数据');
}
layer.prompt({
formType: 1
, title: '敏感操作,请验证口令'
}, function (value, index) {
layer.close(index);
layer.confirm('确定删除吗?', function (index) {
http.delete('delete', { ids: ids.join(','), force: force })
.then((res) => {
if (res.code === 0) {
layer.close(index);
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
});
});
},
dataCreate: function (data = {}) {
dataFromFun(data,
function (layero, index) { },
function (layero, index, elem) {
var field = elem.field;
field.icon = 'layui-icon ' + field.icon;
field.status = field.status ? 1 : 0;
http.post('create', field)
.then((res) => {
if (res.code === 0) {
layer.close(index);
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
}
);
},
dataEdit: function (data = {}) {
dataFromFun(data,
function (layero, index) { },
function (layero, index, elem) {
var field = elem.field;
field.icon = 'layui-icon ' + field.icon;
field.status = field.status ? 1 : 0;
http.put('update', field)
.then((res) => {
if (res.code === 0) {
layer.close(index);
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '操作失败');
}
});
}
);
},
dataRecybin: function (data = {}) {
var formHtml = laytpl($('#dataRecybinTpl').html()).render(data || {});
layer.open({
title: '资源回收站',
content: formHtml,
anim: "slideLeft",
offset: "r",
area: ['50%', '99%'],
shade: 0.1,
shadeClose: true,
success: function (layero, index) {
layui.table.render({
elem: '#dataRecybinTable',
url: "recyclebin",
height: 'full-100',
defaultToolbar: [{
title: '批量删除数据',
layEvent: 'dataDelete',
icon: 'layui-icon-delete',
onClick: function (obj) {
var checkStatus = table.checkStatus('dataRecybinTable'), checkData = checkStatus.data;
let ids = checkData.map((item, index, array) => { return item.id; });
console.log(ids);
}
}, 'filter', 'exports', 'print'],
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'title', title: '名称', width: 180, fixed: 'left' },
{ field: 'code', title: '代码', width: 180, fixed: 'left' },
{ field: 'description', title: '描述', minWidth: 200, sort: true },
{ fixed: "right", title: "操作", width: 181, align: "center", toolbar: "#dataRecybinBarTpl" }
]],
page: true,
done: function (res, curr, count, origin) {
console.log('表格渲染完成');
}
});
}
});
},
dataRestore: function (data = {}) {
http.put("restore", { id: data.id })
.then((res) => {
if (res.code === 0) {
layer.msg('恢复成功');
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg || '恢复失败');
}
});
}
};
// 打开管理员表单
function opendataForm(data) {
var title = data ? '编辑管理员 [' + data.account + ']' : '添加管理员';
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
layer.open({
type: 1,
title: title,
area: ['500px', '520px'],
shadeClose: true,
content: formHtml,
success: function (layero, index) {
// 显示/隐藏密码字段
if (data) {
$('#passwordGroup').hide();
} else {
$('#passwordGroup').show();
}
// 表单验证规则
layui.form.verify({
username: function (value) {
if (!/^[\w\u4e00-\u9fa5]{2,20}$/.test(value)) {
return '用户名必须2-20位字符,支持中文、字母、数字和下划线';
}
},
pass: [
/^[\S]{6,20}$/
, '密码必须6到20位,且不能出现空格'
]
});
// 提交表单
form.on('submit(adminFormSubmit)', function (formData) {
var url = data ? "update" : "create";
http.post(url, formData.field).then(function (res) {
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
layer.close(index);
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
return false;
});
form.render();
},
btn: ['保存', '取消'],
yes: function (index, layero) {
// 触发表单提交
layero.find('form').find('button[lay-filter=adminFormSubmit]').click();
}
});
}
// 重置密码
function resetPassword(data) {
layer.confirm('确定要重置管理员 [' + data.username + '] 的密码吗?重置后密码将变为123456', {
btn: ['确定', '取消'],
title: '重置密码'
}, function (index) {
http.post("resetPassword", { id: data.id }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1 });
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
// 删除管理员
function deleteAdmin(data) {
layer.confirm('确定要删除管理员 [' + data.username + '] 吗?此操作不可恢复!', {
btn: ['确定', '取消'],
title: '删除管理员',
icon: 3
}, function (index) {
http.post("delete", { id: data.id }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
// 批量删除
function batchDelete(data) {
if (data.length === 0) {
layer.msg('请先选择要删除的管理员', { icon: 3 });
return;
}
var superAdmins = data.filter(function (item) {
return item.is_super == 1;
});
if (superAdmins.length > 0) {
layer.msg('不能删除超级管理员账号', { icon: 2 });
return;
}
var ids = data.map(function (item) {
return item.id;
});
layer.confirm('确定要删除选中的 ' + data.length + ' 个管理员吗?此操作不可恢复!', {
btn: ['确定', '取消'],
title: '批量删除',
icon: 3
}, function (index) {
http.post("batchDelete", { ids: ids }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
exports('admin', {});
});