fix(commission): 修复分销商身份等级访问异常并健全部署缓存权限保障

这个提交包含在:
wikiboss
2026-09-17 16:37:28 +08:00
父节点 a94b05178e
当前提交 f5b804fb57
共有 4 个文件被更改,包括 143 次插入 和 70 次删除

查看文件

@@ -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);
}