已修复 后台管理编辑用户组调整审核权限时,取消勾选后点击保存,依然默认勾选 [复制链接]

三级用户组

如题,用AI检测了下系统源码,给出的反馈如下,请站长参考:

# 修复方案:用户组「发帖免审 / 回帖免审 / 资料免审」取消勾选后刷新重新勾选

> 排查日期:2026-08-17 | 关联版本:XIUNOX v1.1.7
> 状态:**仅排查与方案,未改动任何代码**(按需求暂不实施)

---

## 一、问题现象

后台「用户组 → 编辑用户组」页面,取消勾选 **发帖免审 / 回帖免审 / 资料免审** 三个审核权限后,点击确定:

- 页面提示「编辑成功」并刷新;
- 刷新后三个复选框 **又被勾选**;
- 清理应用缓存 / 浏览器缓存均无效。

其它权限项(允许阅读、允许发帖、版主权限等)保存正常,唯独这三个审核权限不生效。

---

## 二、根因(已定位)

**`lib/PermissionService.php` 的 `tableExists()` 方法使用了错误的配置键读取表前缀,导致 `group_permission` 表被判定为"不存在",写入与读取都被静默跳过。**

### 2.1 错误代码

`lib/PermissionService.php:224-233`:

```php
private static function tableExists(): bool {
    static $exists = NULL;
    if($exists === NULL) {
        global $conf;
        $tablepre = $conf['db']['master']['tablepre'] ?? 'bbs_';   // ← 键路径错误(第 228 行)
        $row = db_sql_find_one("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$tablepre}group_permission'");
        $exists = !empty($row);
    }
    return $exists;
}
```

### 2.2 实际配置结构

`conf/conf.php`(本站真实配置)的表前缀位于 **两层驱动嵌套** 之下:

```php
'db' => array (
    'type' => 'pdo_mysql',
    'mysql' => array (
        'master' => array ( 'tablepre' => 'xiunox_', ... ),   // 正确路径 ①
    ),
    'pdo_mysql' => array (
        'master' => array ( 'tablepre' => 'xiunox_', ... ),   // 正确路径 ②
    ),
),
```

- `$conf['db']['master']` **不存在**(`db` 下没有直接名为 `master` 的键),取值为 `NULL`;
- `?? 'bbs_'` 兜底生效,`$tablepre` 恒等于 **`'bbs_'`**;
- 实际表前缀是 **`xiunox_`**,于是查询的是不存在的 `bbs_group_permission`;
- `INFORMATION_SCHEMA` 查不到该表 → `tableExists()` 恒返回 `FALSE`。

> 对比:框架标准助手 `xiunophp/db.func.php` 的 `db_check_table_exists()`(519-526 行)与 `db_check_column_exists()`(510-517 行)都用 `$db->tablepre`,**是正确的**。本方法绕开了标准助手,自己拼了错误的 `$conf` 键,是唯一一处问题点。

---

## 三、连锁影响(为什么"保存成功"却什么都没存)

`tableExists()` 是 `PermissionService` 内部所有 `group_permission` 读写的前置开关,返回 `FALSE` 导致三级连锁:

| 方法 | 行为 | 后果 |
|---|---|---|
| `updatePermissions()`(152-179 行) | `if(!self::tableExists()) return FALSE;` **直接返回,不写库** | 三个免审值(`allow_direct_post/reply/profile`)**从未写入** `xiunox_group_permission` |
| `getPermissions()`(120-144 行) | 跳过 `group_permission` 读取,回退 `$grouplist[$gid]`(`bbs_group` 表旧字段) | 设置页勾选状态取自 `bbs_group.allow_direct_*`(默认值 `1`)→ **刷新后又勾选** |
| `check()` / `getPermissionValue()`(82-113 / 241-248 行) | `getPermissionValue()` 返回 `NULL`,回退 `bbs_group` 旧字段 | **运行时**同样把这三个用户组视为「免审=1」,审核永不生效 |

### 3.1 为什么其它权限正常

`admin/route/group.php` 的 POST 处理分两段保存:

1. **第一段**(132-142 行):`allowread / allowthread / allowpost / allowattach / allowdown / 版主权限` 等写进 **`bbs_group` 表**(`group_update()`)→ 走正常路径,**能持久化**;
2. **第二段**(167-174 行):所有注册权限键写进 **`group_permission` 表**(`updatePermissions()`)→ 被 `tableExists()` 拦截,**静默失效**。

三个免审字段**只在**第二段存在,没有 `bbs_group` 表字段的写入路径(`bbs_group` 里的 `allow_direct_*` 列只用于回退读取),所以唯独它们不生效。

### 3.2 运行时同样受影响(不只是显示问题)

- 发帖免审:`lib/security/AuditService.php:46` → `PermissionService::check('allow_direct_post')`
- 回帖免审:`lib/security/AuditService.php:77` → `PermissionService::check('allow_direct_reply')`
- 资料免审:`api/v1/user.php:447` → `PermissionService::check('allow_direct_profile')`

三条运行时判断全部被 `tableExists()` 卡住并回退到 `bbs_group` 旧字段(=1),**即使本次设置页问题修好之前后台已经把某组改成需审核,运行时也仍然放行免审**。

---

## 四、修复方案

> ⚠️ 本方案仅描述改动点,**尚未实施**。按铁律,改动仅限 `lib/PermissionService.php`,不涉及系统源码其它文件。

### 4.1 推荐方案(最小改动,复用框架正确助手)

将 `tableExists()` 改为直接委托给框架标准助手 `db_check_table_exists()`(它已用 `$db->tablepre`,与 `updatePermissions()` 第 158-159 行现有写法一致):

```php
private static function tableExists(): bool {
    static $exists = NULL;
    if($exists === NULL) {
        $exists = db_check_table_exists('group_permission');
    }
    return $exists;
}
```

- 改动量:`lib/PermissionService.php` 单个方法,约 6 行。
- 安全性:`db_check_table_exists()` 在 `xiunophp/db.func.php:519-526`,参数化绑定 `$db->tablepre.$table`,前缀永远取运行时真实值(`xiunox_`)。
- 一致性:`lib/LoginSecurityService.php:33/84/124`、`lib/security/AuditService.php:1402` 均已用该助手,风格统一。

### 4.2 备选方案(不依赖助手,语义等价)

```php
private static function tableExists(): bool {
    static $exists = NULL;
    if($exists === NULL) {
        global $db;
        $tablepre = (is_object($db) && !empty($db->tablepre)) ? $db->tablepre : 'bbs_';
        $row = db_sql_find_one("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$tablepre}group_permission'");
        $exists = !empty($row);
    }
    return $exists;
}
```

> 与 `HealthCheckService::checkDatabase()`(`lib/HealthCheckService.php:278-288`)同思路:优先 `$db->tablepre`。若担心个别环境 `$db` 未初始化,可再补 `pdo_mysql` / `mysql` 两层 config 回退,但正常请求上下文 `$db` 必然存在,简单写法即可。

### 4.3 不建议的改动(明确排除)

- **不要**在 `admin/route/group.php` 里额外把 `allow_direct_*` 也写进 `bbs_group` 表字段。
  - 现状设计是「`group_permission` 表优先、`bbs_group` 旧字段回退」的双源读取(`getPermissions()` 注释明确说明),把免审改成写 `bbs_group` 会造成双源不一致、升级/迁移复杂化;
  - 修好 `tableExists()` 后,免审字段走 `group_permission` 单源即可,与其它权限一致。
- **不要**全局删除 `bbs_group` 的 `allow_direct_*` 列:升级脚本(`lib/UpgradeService.php:1270-1271/1400`)仍依赖它们作回退与迁移基础,删除属破坏性变更,超出本次修复范围。

---

## 五、验证步骤(修复后执行)

1. **前置确认**(修复前):SQL 确认当前 `group_permission` 无免审记录——
   ```sql
   SELECT gid, permission_key, value FROM xiunox_group_permission WHERE permission_key LIKE 'allow_direct%';
   -- 预期:0 行(此前从未写入成功)
   ```
2. **单元级**:临时在页面 POST 后检查该表——
   ```sql
   SELECT gid, permission_key, value FROM xiunox_group_permission WHERE gid = 102 ORDER BY permission_key;
   -- 修复后预期:17 个注册键全量出现,免审键为本次勾选值(取消勾选 → 0)
   ```
3. **UI 级**:编辑用户组,取消勾选三个免审 → 保存 → 刷新,确认三个复选框保持未勾选;再勾选保存 → 保持勾选。
4. **运行时级**:将该组免审设为 `0` 后,用该组账号发帖 → 帖子进入「待审核」;设为 `1` → 直接发布。回帖、资料修改(昵称/签名/头像)同理。

---

## 六、风险与注意事项

1. **行为变化提示**:当前运行时由于 bug,所有用户组实际都是「免审=1」。修复后,`PermissionService::check()` 将真正读取 `group_permission` 表值——**若某组此前曾被设置为需审核但从未生效,修复后再次保存才会真正生效**。部署后建议逐一核对关键用户组的免审设置。
2. **数据迁移**:修复前 `group_permission` 仅有 `install.sql` 种子数据(gid 0/1/2/4 的少数键,不含免审键)。修复后首次进入设置页时,免审项仍会显示为「勾选」(来自 `bbs_group` 回退值 1),属预期;**保存一次**后即写入 `group_permission` 并与显示一致。如需一次性同步,可在 `lib/UpgradeService.php` 增加一个幂等迁移:把 `bbs_group.allow_direct_*` 逐组 upsert 进 `group_permission`(可选,非必须)。
3. **清缓存**:`PermissionService` 无持久缓存(`tableExists()` 的 `static $exists` 仅请求内生效),本次问题与缓存无关,无需清缓存;改完代码需清理 `tmp/` 编译产物(`_include()` 对 `lib/` 类文件编译缓存)。

---

## 附:关键文件定位

| 文件 | 位置 | 角色 |
|---|---|---|
| `lib/PermissionService.php` | 224-233 行(**228 行**) | 根因:`tableExists()` 错误配置键 |
| `lib/PermissionService.php` | 152-179 / 120-144 / 241-248 行 | `updatePermissions()` / `getPermissions()` / `getPermissionValue()` 受影响链路 |
| `admin/route/group.php` | 132-142 / 167-174 行 | 第一段写 `bbs_group`(正常)/第二段写 `group_permission`(失效) |
| `admin/view/htm/group_update.htm` | 64-70 行 | 免审复选框渲染(数据源 `$saved_perms` = `getPermissions()`) |
| `lib/security/AuditService.php` | 46 / 77 行 | 发帖 / 回帖运行时审核判断 |
| `api/v1/user.php` | 447 行 | 资料(昵称)免审判断 |
| `xiunophp/db.func.php` | 510-526 行 | 正确的 `db_check_table_exists()` / `db_check_column_exists()`(参照实现) |
| `conf/conf.php` | db.mysql.master.tablepre / db.pdo_mysql.master.tablepre | 真实前缀 `xiunox_` |
Bug反馈
程序版本:
1.1.7
PHP版本:
php8.4
MySql:
8.0
复现步骤:
后台管理编辑用户组调整审核权限时,取消勾选后点击保存,页面自动刷新,依然默认勾选
预期结果:
管理员标记为「已修复」,奖励 +5 金币
最新回复
  • 晨曦笔记 一级用户组
    感谢楼主这么详细的排查,这种后台权限勾选保存后又自己勾回去的问题确实烦人,清理缓存也没用的话基本可以确定是源码逻辑的问题了。我这边也是 1.1.7,平时没怎么动过用户组的审核权限,回头我按你说的步骤试一下能不能复现,能复现的话也来反馈一份,方便站长定位。
    26天前
  • xiunox 一级用户组

    这个排查很详细,感谢 @姚先森 的定位分析。tableExists() 读错 $conf 键路径导致 group_permission 被静默跳过,链路完整、证据充分——尤其是「第一段写 bbs_group 正常、第二段写 group_permission 失败」恰好解释了为何只有三个免审字段不生效,逻辑上是站得住脚的。

    修复方案我倾向 4.1 的 db_check_table_exists() 委托写法,与 LoginSecurityServiceAuditService 现有调用一致,最小且安全。建议实际修的时候确认一下:你的 conf/conf.phpdb 节点下是否确实没有直接的 master 键?另外修复后务必跑一遍第五节验证步骤,特别是运行时验证(发帖进审核/直接发布),能确认修复前后的行为差异。

    如果站长采纳修复,建议顺带核对一下已有用户组的免审回退值,避免修复后出现权限收紧的意外。 @晨曦笔记 也可以按楼主的复现步骤验证一下,多一份反馈更稳。【 AI 自动回复,仅供参考】

    26天前
  • 贰先生 社区元老金冠神 管理员组
    好的,我看下
    26天前
  • 贰先生 社区元老金冠神 管理员组

    已修复,更新到最新版本即可

    https://xiunox.org/thread-313.html

    26天前
    姚先森 三级用户组 作者 回复@贰先生
    好的,谢谢
    26天前

请先登录后再回复 登录