fix(commission): 修复分销商身份等级访问异常并健全部署缓存权限保障
这个提交包含在:
@@ -9,35 +9,14 @@ use think\Db;
|
|||||||
class Level extends Common
|
class Level extends Common
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $noNeedLogin = ['index', 'select'];
|
|
||||||
protected $noNeedRight = ['index', 'select'];
|
protected $noNeedRight = ['index', 'select'];
|
||||||
|
|
||||||
protected $model = null;
|
protected $model = null;
|
||||||
|
|
||||||
public function _initialize()
|
public function _initialize()
|
||||||
{
|
{
|
||||||
try {
|
parent::_initialize();
|
||||||
if (input('test_admin')) {
|
$this->model = new LevelModel();
|
||||||
$admin = \app\admin\model\Admin::get(1);
|
|
||||||
if ($admin) {
|
|
||||||
\think\Session::set('admin', $admin->toArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent::_initialize();
|
|
||||||
$this->model = new LevelModel();
|
|
||||||
} catch (\think\exception\HttpResponseException $e) {
|
|
||||||
throw $e;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
echo json_encode([
|
|
||||||
'code' => 500,
|
|
||||||
'msg' => 'INIT_ERROR: ' . $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'trace' => explode("\n", $e->getTraceAsString())
|
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,49 +24,25 @@ class Level extends Common
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if (input('read_err_log')) {
|
if (!$this->request->isAjax()) {
|
||||||
$rules = Db::name('auth_rule')->where('name', 'like', '%commission/level%')->select();
|
return $this->view->fetch();
|
||||||
$admins = Db::name('admin')->field('id,username,nickname,status')->select();
|
|
||||||
echo json_encode([
|
|
||||||
'rules' => $rules,
|
|
||||||
'admins' => $admins,
|
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$defaultLevel = $this->model->find(1);
|
||||||
if (!$this->request->isAjax()) {
|
if (!$defaultLevel) {
|
||||||
return $this->view->fetch();
|
$this->model->save([
|
||||||
}
|
'name' => '默认等级',
|
||||||
|
'level' => 1,
|
||||||
$defaultLevel = $this->model->find(1);
|
'commission_rules' => [
|
||||||
if (!$defaultLevel) {
|
'commission_1' => '0.00',
|
||||||
$this->model->save([
|
'commission_2' => '0.00',
|
||||||
'name' => '默认等级',
|
'commission_3' => '0.00'
|
||||||
'level' => 1,
|
]
|
||||||
'commission_rules' => [
|
]);
|
||||||
'commission_1' => '0.00',
|
|
||||||
'commission_2' => '0.00',
|
|
||||||
'commission_3' => '0.00'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$list = $this->model->sheepFilter()->select();
|
|
||||||
|
|
||||||
$this->success('全部等级', null, $list);
|
|
||||||
} catch (\think\exception\HttpResponseException $e) {
|
|
||||||
throw $e;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
echo json_encode([
|
|
||||||
'code' => 500,
|
|
||||||
'msg' => 'INDEX_ERROR: ' . $e->getMessage(),
|
|
||||||
'file' => $e->getFile(),
|
|
||||||
'line' => $e->getLine(),
|
|
||||||
'trace' => explode("\n", $e->getTraceAsString())
|
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
$list = $this->model->sheepFilter()->select();
|
||||||
|
|
||||||
|
$this->success('全部等级', null, $list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -83,13 +83,6 @@ $langSet == 'en' && $lang = array_combine(array_keys($lang), array_keys($lang));
|
|||||||
</div>
|
</div>
|
||||||
<div class="subheader">
|
<div class="subheader">
|
||||||
<?=$debug?$message:$lang['The page you are looking for is temporarily unavailable']?>
|
<?=$debug?$message:$lang['The page you are looking for is temporarily unavailable']?>
|
||||||
<?php if (isset($exception) && $exception instanceof \Throwable): ?>
|
|
||||||
<div style="font-size:13px;color:#c0392b;text-align:left;margin-top:15px;background:#f9f9f9;padding:10px;border-radius:4px;word-break:break-all;">
|
|
||||||
<strong>Exception:</strong> <?=htmlspecialchars($exception->getMessage())?><br>
|
|
||||||
<strong>File:</strong> <?=htmlspecialchars($exception->getFile())?>:<?=$exception->getLine()?><br>
|
|
||||||
<pre style="font-size:11px;max-height:150px;overflow:auto;margin-top:5px;"><?=htmlspecialchars($exception->getTraceAsString())?></pre>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<div class="context">
|
<div class="context">
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ fi
|
|||||||
log "清理运行时缓存..."
|
log "清理运行时缓存..."
|
||||||
rm -rf "${PROJECT_DIR}/runtime/cache/"* 2>/dev/null || true
|
rm -rf "${PROJECT_DIR}/runtime/cache/"* 2>/dev/null || true
|
||||||
rm -rf "${PROJECT_DIR}/runtime/temp/"* 2>/dev/null || true
|
rm -rf "${PROJECT_DIR}/runtime/temp/"* 2>/dev/null || true
|
||||||
|
mkdir -p "${PROJECT_DIR}/runtime/cache" "${PROJECT_DIR}/runtime/temp" "${PROJECT_DIR}/runtime/log"
|
||||||
chown -R ${WEB_USER}:${WEB_USER} "${PROJECT_DIR}/runtime" 2>/dev/null || true
|
chown -R ${WEB_USER}:${WEB_USER} "${PROJECT_DIR}/runtime" 2>/dev/null || true
|
||||||
chmod -R 777 "${PROJECT_DIR}/runtime" 2>/dev/null || true
|
chmod -R 777 "${PROJECT_DIR}/runtime" 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const vm = require('vm');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function assert(condition, testName, detail = '') {
|
||||||
|
if (condition) {
|
||||||
|
console.log(` [PASS] ${testName}`);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
console.error(` [FAIL] ${testName} - ${detail}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("========================================================");
|
||||||
|
console.log(" 身份等级 (shopro/commission/level) 页面与权限 TDD 测试");
|
||||||
|
console.log("========================================================\n");
|
||||||
|
|
||||||
|
// 1. 验证 Level.php 控制器存在与安全性
|
||||||
|
const levelPhpPath = path.resolve(__dirname, '../application/admin/controller/shopro/commission/Level.php');
|
||||||
|
assert(fs.existsSync(levelPhpPath), 'Level.php 控制器文件存在');
|
||||||
|
|
||||||
|
const levelPhpContent = fs.readFileSync(levelPhpPath, 'utf-8');
|
||||||
|
|
||||||
|
// 1.1 验证无临时 $noNeedLogin 遗留(后台安全保障)
|
||||||
|
assert(!levelPhpContent.includes('protected $noNeedLogin'), '无临时 $noNeedLogin 遗留(必须验证登录态)');
|
||||||
|
|
||||||
|
// 1.2 验证 $noNeedRight 包含 index 和 select(允许登录管理员正常进入列表)
|
||||||
|
const hasNoNeedRightIndex = /protected\s+\$noNeedRight\s*=\s*\[.*'index'.*\];/s.test(levelPhpContent);
|
||||||
|
const hasNoNeedRightSelect = /protected\s+\$noNeedRight\s*=\s*\[.*'select'.*\];/s.test(levelPhpContent);
|
||||||
|
assert(hasNoNeedRightIndex && hasNoNeedRightSelect, '$noNeedRight 必须包含 index 和 select', '缺少 index 或 select');
|
||||||
|
|
||||||
|
// 1.3 验证移除所有临时调试探针代码
|
||||||
|
assert(!levelPhpContent.includes('test_admin'), '已彻底清除 test_admin 模拟登录调试代码');
|
||||||
|
assert(!levelPhpContent.includes('read_err_log'), '已彻底清除 read_err_log 日志探测代码');
|
||||||
|
assert(!levelPhpContent.includes('INIT_ERROR'), '已彻底清除临时 INIT_ERROR 诊断代码');
|
||||||
|
assert(!levelPhpContent.includes('INDEX_ERROR'), '已彻底清除临时 INDEX_ERROR 诊断代码');
|
||||||
|
|
||||||
|
// 1.4 验证核心方法完整性
|
||||||
|
const requiredMethods = ['_initialize', 'index', 'add', 'edit', 'detail', 'delete', 'select'];
|
||||||
|
requiredMethods.forEach(method => {
|
||||||
|
assert(levelPhpContent.includes(`public function ${method}`), `包含动作方法: ${method}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 验证前端 JS (level.js) 语法与逻辑完整性
|
||||||
|
const levelJsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/commission/level.js');
|
||||||
|
assert(fs.existsSync(levelJsPath), 'level.js 前端脚本存在');
|
||||||
|
|
||||||
|
const levelJsContent = fs.readFileSync(levelJsPath, 'utf-8');
|
||||||
|
|
||||||
|
let jsSyntaxOk = false;
|
||||||
|
let jsSyntaxError = '';
|
||||||
|
try {
|
||||||
|
new vm.Script(levelJsContent, { filename: 'level.js' });
|
||||||
|
jsSyntaxOk = true;
|
||||||
|
} catch (e) {
|
||||||
|
jsSyntaxError = e.message;
|
||||||
|
}
|
||||||
|
assert(jsSyntaxOk, 'level.js 语法无 SyntaxError', jsSyntaxError);
|
||||||
|
|
||||||
|
// 2.1 模拟执行并检查 Controller 结构
|
||||||
|
const dummyDefine = function (deps, factory) {
|
||||||
|
const $ = () => ({});
|
||||||
|
const Backend = { api: {} };
|
||||||
|
const Table = {};
|
||||||
|
const Form = {};
|
||||||
|
return factory($, undefined, Backend, Table, Form);
|
||||||
|
};
|
||||||
|
|
||||||
|
let moduleLoaded = false;
|
||||||
|
let controllerObj = null;
|
||||||
|
try {
|
||||||
|
const sandbox = {
|
||||||
|
define: dummyDefine,
|
||||||
|
console: console,
|
||||||
|
location: { search: '?ref=addtabs' },
|
||||||
|
window: {},
|
||||||
|
Fast: { api: { ajax: () => {}, open: () => {}, fixurl: (u) => u } },
|
||||||
|
Vue: {
|
||||||
|
ref: (v) => ({ value: v }),
|
||||||
|
reactive: (o) => o,
|
||||||
|
onMounted: (cb) => cb(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vm.createContext(sandbox);
|
||||||
|
controllerObj = vm.runInContext(levelJsContent, sandbox);
|
||||||
|
moduleLoaded = !!(controllerObj && controllerObj.index);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("level.js 模拟加载异常:", e.message);
|
||||||
|
}
|
||||||
|
assert(moduleLoaded, 'level.js 包含 Controller.index 动作');
|
||||||
|
|
||||||
|
// 3. 验证视图模板存在且无语法残缺
|
||||||
|
const levelHtmlPath = path.resolve(__dirname, '../application/admin/view/shopro/commission/level/index.html');
|
||||||
|
assert(fs.existsSync(levelHtmlPath), 'index.html 视图模板文件存在');
|
||||||
|
const levelHtmlContent = fs.readFileSync(levelHtmlPath, 'utf-8');
|
||||||
|
assert(levelHtmlContent.includes('身份等级'), '模板包含身份等级标题');
|
||||||
|
assert(levelHtmlContent.includes('commission_rules?.commission_1'), '模板具备佣金比例安全可选链展示');
|
||||||
|
assert(levelHtmlContent.includes('state.makerBonus'), '模板具备创客进阶基数展示');
|
||||||
|
|
||||||
|
// 4. 验证 deploy.sh 对 runtime 目录的权限保障
|
||||||
|
const deployShPath = path.resolve(__dirname, '../scripts/deploy.sh');
|
||||||
|
assert(fs.existsSync(deployShPath), 'deploy.sh 部署脚本存在');
|
||||||
|
const deployShContent = fs.readFileSync(deployShPath, 'utf-8');
|
||||||
|
assert(deployShContent.includes('chown -R ${WEB_USER}:${WEB_USER} "${PROJECT_DIR}/runtime"'), '部署脚本包含 runtime 目录所有权赋予 www 用户');
|
||||||
|
assert(deployShContent.includes('chmod -R 777 "${PROJECT_DIR}/runtime"') || deployShContent.includes('chmod -R 775 "${PROJECT_DIR}/runtime"'), '部署脚本包含 runtime 目录写权限');
|
||||||
|
|
||||||
|
// 5. 验证生产异常模板 think_exception.tpl 干净无调试代码泄露
|
||||||
|
const exceptionTplPath = path.resolve(__dirname, '../application/common/view/tpl/think_exception.tpl');
|
||||||
|
assert(fs.existsSync(exceptionTplPath), 'think_exception.tpl 存在');
|
||||||
|
const exceptionTplContent = fs.readFileSync(exceptionTplPath, 'utf-8');
|
||||||
|
assert(!exceptionTplContent.includes('$exception instanceof \\Throwable'), 'think_exception.tpl 已恢复干净,无临时注入调试代码');
|
||||||
|
|
||||||
|
console.log(`\n测试统计: 通过: ${passed}, 失败: ${failed}`);
|
||||||
|
if (failed > 0) {
|
||||||
|
console.error(" 自动化测试未全部通过,需要继续修复!");
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log(" 全部自动化测试通过!");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
在新工单中引用
屏蔽一个用户