本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/昵称插件开发方案_2026-09-27.md。
昵称插件开发方案(用户自定义显示名)
日期:2026-09-27 | 依据:
docs/插件开发规范.md状态:待确认(本文只出方案,不动手写代码)
建议插件名 / 目录名:
nickname(plugins/nickname空闲 ✓;无下划线 → 不需要class_alias)本文所有结论均来自实测,
文件:行号可逐条复核。
一、结论摘要(先看这段)
一句话:「昵称」只能做成显示名,不能做成「改用户名」(登录只认用户名,见 §2.3);
而「全站把用户名换成昵称」在 FlintHub 现有架构里没有单一渲染入口(71 处 / 24 文件 / 17 种数据形状,见 §2.4),
所以纯插件路线只能覆盖约 85%,且会在插件渲染的片段里出现「一处昵称、一处用户名」的不一致。
三条路线取舍:
| 路线 | 核心改动 | 覆盖率 | 风险 | 建议 |
|---|---|---|---|---|
| A 纯插件(视图数据改写) | 0 | ~85% | 中:语义误伤 + 片段不一致 | 可选(想零核心改动时) |
| B 核心加扩展点 + 插件 | 1 列 + 模型 + ~14 前台视图 | ~95% | 低:语义清晰 | 推荐 |
| C 昵称作为附加显示 | 0 | 仅 3~4 处 | 极低 | 不满足「全站显示昵称」 |
推荐 B 线,两个理由:
- 本项目已有「给核心加扩展点再写插件」的既定实践 ——
thread_show_top(2026-09-25 新增)、
right_sidebar_user_actions_above(2026-09-27 新增)都是这么加的(docs/插件开发规范.md §9.1 F/G 组);
ensureColumnPatches()(app/SplitDB/Schema.php:611)就是为「给存量库补列」设计的,加列成本极低且幂等。
二、现状核查(实测,非推断)
2.1 users 表没有昵称列
实测 25 列:`id, username, password, email, avatar, signature, role, status, group_id, post_count,
points, level, theme, remember_token, last_active_at, last_login, email_verified, email_verify_token,
email_verify_token_sent_at, created_at, hide_right_sidebar, night_theme_auto, list_excerpt,
list_excerpt_len, home_mode —— 无 nickname`。
- 新库建表:
app/SplitDB/Schema.php:270-293 - 存量库补列入口:
app/SplitDB/Schema.php:631-639('users' => [...]补丁块,哈希 marker 门控)
2.2 User 模型没有写用户名的出口
| 项 | 实测 |
|---|---|
$fillable | ['last_login'](app/Models/User.php:18)→ update() 只认 last_login(§11.8 第 4 条) |
| 已有写方法 | updateProfile($id,$email,$signature)(:145)、updateAvatar、updateByAdmin(:518) |
| 缺失 | 无 updateUsername() / updateNickname() |
2.3 登录名 = 用户名 → 「改用户名」这条路是死的
app/Controllers/AuthController.php:89:$user = $userModel->findByUsername($username);
只按用户名登录,无邮箱登录入口。
→ 改 username 等于改登录凭据(老用户直接登不上),与「昵称」语义冲突 → 本方案排除该做法。
2.4 全站 71 处用户名渲染点,没有单一入口
| 项 | 实测值 |
|---|---|
| 渲染点 | 71 处 / 24 个文件(app/Views/) |
| 数据形状 | 17 种:$thread / $post / $item / $user / $currentUser / $blog / $u / $t / $log / $comment / $tei / $b / $r / $lead / $detailItem / $message['sender_username'] / $msg['receiver_username'] |
| 输出方式 | 全部 $this->e(...) → TemplateCompiler::e()(app/Core/TemplateCompiler.php:298)—— 纯转义器,无过滤点 |
| 派生用法 | 头像首字 mb_substr($username, 0, 1)(10 处)、data-quote-user 属性(2 处)、highlightSearchTerms()(1 处) |
典型样例:
app/Views/forum/index.php:47 $username = $this->e($thread['username'] ?? '');
app/Views/thread/show.php:281 <?php echo $this->e($post['username'] ?? ''); ?>
app/Views/admin/logs/audit.php:62 <?php echo $this->e((string)($log['username'] ?? '')) ?>
app/Views/profile/index.php:52 2.5 唯一能改视图数据的钩子 controller_view_before 有 3 个硬约束
触发点 app/Core/Controller.php:49:
try {
\app\Helpers\Plugin::hook('controller_view_before', ['template' => $template, 'data' => &$data]);
} catch (\Throwable $e) { ... }
$data = \array_merge($defaults, $data); // ← :54,钩子跑在它「之前」- 时序:钩子触发在
array_merge($defaults, $data)(:54)之前 → 钩子执行时$data里
还没有 currentUser / currentUsername(它们在 $defaults,:34-35)。
- 范围:只对走
view()的页面生效。 viewRaw()绕过它(:61,直接display(),不触发任何钩子)。实测绕过点:
plugins/floor_reply/FrontController.php:216,249→ 楼中楼片段,视图里就显示作者名(views/list.php:45,49)plugins/daily_checkin/CheckinController.php:203,274,291plugins/mod_system/FrontController.php:105,168
2.6 已有的「昵称」先例都与本需求无关
| 位置 | 性质 |
|---|---|
ai_assistant_members.nickname | AI 人设名(后台展示用) |
third_party_login 存第三方 nick | 第三方账号原始昵称 |
→ 没有可复用的昵称机制;plugins/nickname 目录名空闲。
三、三条路线对比(详细)
A 线 · 纯插件:controller_view_before 深度改写视图数据
做法:钩子里按 $template 白名单,递归遍历 $data,凡是同时含 user_id 与 username 的行,
把 username 换成插件库里的昵称;先把所有 user_id 收集齐,再一次 WHERE user_id IN (...) 查回(1 次查询)。
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 零核心改动,可立即交付 | 盲区 1:viewRaw 片段改不到 → 详情页显示昵称、楼中楼显示用户名(§2.5-3) |
| 能覆盖 forum/category、forum/index、home/、thread/show、blog/、tags/show、message/* | 盲区 2:其它插件渲染的名字(leaderboard / new_members / points_mall…)全部改不到 |
| 不动数据库结构 | 语义误伤:profile/index.php:52、admin/user_edit.php:21、admin/users.php:37 的 disabled 输入框是「登录名」语义,admin/logs/audit.php:62 是审计留痕 → 换成昵称就是假信息(只能靠模板白名单排除,属「打补丁」) |
search/index.php:123 的 highlightSearchTerms($username) 会失效 | |
每页多一次递归遍历($data 可能很大),需深度上限 + 键白名单 |
结论:能跑,但脆。适合「只想尽快有个能用的版本」且接受上述不一致。
B 线 · 核心加扩展点 + 插件(推荐)
做法:
- 核心
users加nickname TEXT DEFAULT NULL(新库进建表语句,老库进ensureColumnPatches); - 核心
User加setNickname(int $id, ?string $nick)/displayName(array $user): string(nickname ?: username)
/ batchGetDisplayNames(array $ids);
- 核心
TemplateCompiler加视图助手name($row)(内部走User::displayName),前台视图分批迁移; - 插件
nickname负责:设置入口、校验、审计、后台管理。
| ✅ 优点 | ❌ 代价 |
|---|---|
语义干净:username 永远是登录名与 @提及 锚点,只改显示 | 要动核心(1 处建表 + 1 处补列 + 模型 + ~14 个前台视图) |
| admin / audit / search 不受影响(后台视图不迁移,继续显示 username) | 插件渲染的名字(floor_reply 等)仍需各自跟进 |
| 与项目既定实践一致(近期两次核心加钩子) |
⚠️ 关键差别:B 线的「未覆盖」是显式、可控的(后台就是不换、插件要自己跟进);
A 线的「未覆盖」是静默、随机的(同一页面上半截昵称下半截用户名)。
C 线 · 昵称作为「附加显示」
只在 nav_user_menu_items(#10,双触发)、right_sidebar_user_actions_above(#42,仅登录态)、
profile_user_card_after(#25)显示昵称(用户下拉 / 右栏用户卡 / 个人主页卡)。
- ✅ 零核心改动、零风险、无「片段不一致」问题(因为不替换)
- ❌ 不满足「全站用昵称代替用户名」—— 列表页、帖子页、回复仍是用户名
四、推荐方案(B 线)技术设计
4.1 数据落点
| 项 | 决策 | 理由 |
|---|---|---|
| 存储位置 | 核心 users.nickname 列(不建独立库) | 昵称与用户 1:1;独立库会让列表页(每页 N 个作者)退化成跨库 JOIN —— users 在 data/meta/business.sqlite(MEMORY §10 红线),跨库代价高 |
| 空值语义 | NULL = 未设置 → 显示回退 username | 零迁移成本,老用户天然「没昵称」 |
| 唯一性 | 待拍板(§五·3) | 允许重名时 @提及 仍走 username,不受影响 |
4.2 核心改动清单(精确落点)
| # | 文件 | 位置 | 改动 |
|---|---|---|---|
| 1 | app/SplitDB/Schema.php | :270-293 建表语句 | 加一行 nickname TEXT DEFAULT NULL, |
| 2 | app/SplitDB/Schema.php | :631-639 'users' 补丁块 | 加 'nickname' => 'TEXT DEFAULT NULL'(清单哈希变 → 老库自动补列) |
| 3 | app/Models/User.php | 新增方法(batchGetNames() :351 之后) | setNickname() / displayName() / batchGetDisplayNames() |
| 4 | app/Core/TemplateCompiler.php | :298 e() 附近 | 新增 name(array $row): string 视图助手 |
| 5 | 前台视图 ~14 文件 | 见 §4.3 | $this->e($x['username']) → $this->name($x) |
⚠️ 第 2 条的机制:ensureColumnPatches() 的 marker 是
md5(json_encode($columnPatches) . json_encode($indexPatches) . $tagsUniqueDdl)(Schema.php:667)
→ 改清单 → 哈希变 → 自动重跑,不需要手删旧 .ok。
(MEMORY §3 那条「改完手删旧 .ok」指的是只改 ALTER 语句却不改清单的情形 —— 哈希不变会静默跳过。见 §七·7)
4.3 视图迁移范围(分批,后台不动)
P1 迁移(前台 14 文件)
forum/index、forum/category、home/index_default、home/index_cms、thread/show、
blog/index、blog/detail、tags/show、search/index、message/inbox、message/sent、
message/view、_components/right_sidebar、profile/index
⚠️
profile/index.php只改:30的显示名;:52那个disabled输入框是「登录名」语义,保持username不动。
P2 不迁移(后台 7 文件)
admin/index、admin/users、admin/user_edit、admin/threads、admin/trashed、
admin/blog_manage、admin/logs/audit —— 后台一律按「登录名」语义保留 username。
4.4 插件职责与文件清单
plugins/nickname/
├── plugin.json # name=nickname, version=1.0.0, admin_url
├── Plugin.php # 生命周期(无下划线 → 不需要 class_alias)
├── FrontController.php # 设置页 GET/POST(PRG 模式)、校验、写审计
├── AdminController.php # 后台:列表 / 搜索 / 强制重置 / 敏感词配置
├── hook/
│ ├── nav_user_menu_items.php # #10 双触发(pc/mobile)→「设置昵称」
│ └── right_sidebar_user_actions_above.php # #42 登录态 →「设置昵称」快捷入口
├── views/
│ ├── setting.php # 前台设置表单(走 view(),含 CSRF)
│ ├── admin_list.php # 后台列表(.admin-table + 双类提权,MEMORY §5)
│ └── _feedback.php # htmx 局部回包(走 viewRaw,⚠️ 不带 /4.5 校验规则(初版,待拍板)
| 项 | 建议值 | 依据 |
|---|---|---|
| 长度 | 2–16 字符(mb_strlen) | 右栏用户卡 ≈ 5 中文字宽(§11.26);列表 .mn-row-author 会截断 |
| 字符集 | 中英文、数字、下划线、连字符;禁空格 | 与 @提及 正则 [\p{L}\p{N}_\-]+ 对齐(plugins/floor_reply/Plugin.php renderMentions) |
| 保留字 | 禁 admin / 管理员 / 站点名 / 已存在的 username | 防冒充 |
| 敏感词 | 复用 content_review 插件词库,或插件自带词表 | 见 §五·4 |
| 修改频率 | 每 N 天一次(插件表记 updated_at) | 防刷;N 见 §五·5 |
| 唯一性 | 待拍板 | 见 §五·3 |
4.6 i18n 键(初版,~12 键 × 3 语)
plugin.nickname.title / .setting / .current / .new / .hint / .save / .saved /
.err_len / .err_charset / .err_reserved / .err_freq / .admin_title
五、待你拍板的 6 个点
- 目标确认:昵称 = 全站显示名,
username仍是登录名 —— 这个理解对吗?
(如果你的意思是「允许用户改用户名」,那要另开方案:登录凭据会变,且模型无出口,见 §2.2/§2.3)
- 路线:A(纯插件 / 85% 覆盖 / 零核心改动)| B(核心扩展点 / 推荐) | C(附加显示 / 不满足需求)
- 唯一性:昵称允许重名吗?(建议:允许。@提及 走 username,两人同名不影响功能)
- 敏感词:复用
content_review插件的词库,还是插件自带一份? - 修改频率:限制「每 N 天一次」还是不限?N = ?
- 入口:用户下拉(#10)+ 右栏(#42)都要,还是只留一个?
(⚠️ 右栏 ≤1024px 会 display:none(§11.26),所以 #10 必须有)
六、实施阶段(每阶段末暂停确认)
| 阶段 | 内容 | 产出 |
|---|---|---|
| P0 | 侦察复核(本方案 §二) | ✅ 已完成 |
| P1 | 核心扩展点:Schema 加列 + User 模型方法 + TemplateCompiler::name() + 只迁移 thread/show 做样板 | 跑通后停 |
| P2 | 其余 13 个前台视图迁移 | 停 |
| P3 | 插件骨架:plugin.json / Plugin.php / 设置页 / 校验 | 停 |
| P4 | 入口与后台:nav_user_menu_items + right_sidebar_user_actions_above + 后台管理页 | 停 |
| P5 | 验证:三语键比对 + plugin_hygiene.py + HTTP 冒烟 + 双主题走查 + 数据洁净度 | 交付 |
七、风险与坑(逐条对应规范)
viewRaw绕过钩子(§2.5-3)→ B 线靠视图迁移,不受影响;A 线是硬伤。- 钩子在 defaults merge 之前(
Controller.php:49vs:54)→ A 线必须自己
Auth::getCurrentUser() 后注入完整 currentUser,否则会清掉头像/积分(array_merge 顶层覆盖)。
- 头像首字
mb_substr($username, 0, 1)(10 处) → 迁移时要一起换成昵称首字,否则头像字与显示名不一致。 data-quote-user(thread/show.php:87,266) → 消费方assets/js/editor.js:2436-2439把它当引用文本塞进编辑器;
而 @提及 是按 username 反查的 → 若换成昵称,引用产生的 @昵称 无法被解析。
建议保留 data-quote-user = username(待拍板 1 的延伸)。
- @提及 与昵称的关系 →
floor_reply的nameMap是「username → uid」;
昵称不参与提及,否则两套名字会打架。
- 插件零内联 + 闭包作用域(§11.24)→ 钩子文件里
$this不可用,
只能 htmlspecialchars($s, ENT_QUOTES, 'UTF-8') + Csrf::token()。
ensureColumnPatches的 marker 是哈希门控(Schema.php:667)→ 加列时必须把列写进$columnPatches清单;
只改 ALTER 语句而清单不变 → 哈希不变 → 静默不执行。
- 缓存:B 线改的是视图文件 → 上线后必须走后台「维护→清理缓存」(
data/runtime/pages/*.html永久缓存)。 users在data/meta/business.sqlite(MEMORY §10)→ 探针连库别连错。- 两套等级体系 / 白名单同步(MEMORY §8)→ 若昵称要显示成徽章,
.user-level-badge那两处白名单要同步。 - 后台表格列序硬编码(MEMORY §5)→ 插件后台列表若含行内表单,须
.pp-table.pp-table td.pp-col-x双类提权。
八、待确认清单
- [ ] §五 的 6 个待拍板点
- [ ] 是否接受「动核心」(1 处建表 + 1 处补列 + 模型 + ~14 个前台视图)
- [ ] 插件名用
nickname还是别的 - [ ] 是否要「改名审计 / 昵称历史」(后台可查谁改过什么)
- [ ] 「昵称唯一」与「敏感词」是否 v1.0.0 就做,还是留 v1.1.0