[开发]「用户改名」插件 开发规划方案

👑Lv.11 元老 🌞 骨干会员
2026-10-26 00:21:26

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/用户改名插件开发方案_2026-10-04.md。

6. 控制器与路由

6.1 路由表

前台(hook/route_register.php,先 if (!isset($router)) return;):

方法路径处理说明
GET/account/usernameFrontController::page改名页
POST/account/usernameFrontController::submit提交改名
POST/account/username/send-codeFrontController::sendCode发验证码

⚠️ 路径避让(§11.34b):站点根真实目录为 app/ assets/ cli/ data/ docs/ lang/ plugins/ protected/ release/ update/ 审计/ 待办事宜/(本次实测)—— 没有 account/ → /account/... 安全。不要用 /plugins、/data、/assets 等真实目录名开前台路由。

后台(hook/admin_route_register.php):

方法注册路径实际 URL处理
GET/username-change/admin/username-changeAdminController::index(用户搜索 + 列表)
POST/username-change/change/admin/username-change/changeAdminController::change(管理员改名)
GET/username-change/settings/admin/username-change/settingsAdminController::settings(设置 Tab)
POST/username-change/settings/admin/username-change/settingsAdminController::saveSettings

⚠️ 不写 /admin 前缀(§11.22,否则 404)。

⚠️ 后台多页必须页内自建 Tab 导航(项目记忆红线:核心卡片只认单个 admin_url,其余页 UI 上等于不存在)→ 用核心 .admin-tabs / .admin-tab-btn(§11.10,勿自定义 tab 类)。

6.2 前台控制器(FrontController extends app\Core\Controller)

public function page()
{
    $this->requireLogin();
    $this->view('plugins/username_change/front', [
        'mode'         => Plugin::verifyMode(),
        'smtpOk'       => Plugin::smtpConfigured(),
        'settings'     => Plugin::publicSettings(),   // 只暴露规则提示所需的最小集
        'history'      => Plugin::history((int)$_SESSION['user_id'], 5),
        'ok'           => (string)($_GET['ok'] ?? ''),
        'err'          => (string)($_GET['err'] ?? ''),
    ]);
}

public function submit()
{
    $this->requireLogin();                              // ① 先登录(§11.28)
    \app\Helpers\Csrf::verifyOrDie($_POST['csrf'] ?? ''); // ② 后 CSRF

    $max = (int)\app\Helpers\Settings::get('uc_rate_submit_max', '10');
    $win = (int)\app\Helpers\Settings::get('uc_rate_submit_window', '3600');
    if (!\app\Helpers\RateLimiter::check('uc_submit', $max, $win)) {   // ⚠️ true = 允许
        $this->redirect('/account/username?err=rate_limited');
        return;
    }

    $uid     = (int)$_SESSION['user_id'];
    $newName = \trim((string)($_POST['new_username'] ?? ''));
    // ... 校验 → 身份验证 → 写入 → 收尾(§8/§9/§10/§11)
    // 全部走 PRG:失败 redirect(?err=xxx)、成功 redirect(?ok=1)
}

⚠️ RateLimiter::check() 契约 = true 表示「允许」(项目记忆红线,写反会恒 429)。

⚠️ 本插件不用 htmx 局部刷新(改名是整页表单 + PRG),因此不触发 §11.4-35 的 SW 缓存陷阱;若后续改成 htmx,必须按该条加 no-store + HX-Request 排除。

6.3 后台控制器(AdminController extends app\Controllers\Admin\BaseController)

namespace Plugin\username_change;

use app\Controllers\Admin\BaseController;
use app\Helpers\Csrf;

class AdminController extends BaseController
{
    // ⚠️ 不要覆写 __construct() 后忘了 parent::__construct()
    //    父类构造函数 = isAdmin() + 二次验证超时门禁(33 个插件已验证)
    // ⚠️ 若确实需要自定义构造函数,必须显式调用 parent::__construct()
    //    否则后台门禁被整个绕过(越权高危,项目记忆已复发 2 次)

    public function index()  { /* 用户搜索 + 列表(只读,走 User::adminSearch) */ }
    public function change() { /* 管理员改名 */ }
    public function settings() { /* 设置 Tab */ }
    public function saveSettings() { /* 保存设置(⚠️ §11.32 字段缺失静默写最小值) */ }
}

后台设置保存的红线(§11.32 / 项目记忆):

// ❌ 错误:字段缺失会被静默写成 0 / 最小值,302 成功但 DB 与缓存都不变、无日志
$value = (int)($_POST[$key] ?? 0);

// ✅ 正确:缺失即跳过
foreach (Plugin::defaultSettings() as $key => $def) {
    if (!\array_key_exists($key, $_POST)) { continue; }
    \app\Helpers\Settings::update($key, (string)$_POST[$key], 'username_change');
}
\app\Helpers\Settings::clearCache();   // ⚠️ settings_cache.php 不自动补新键

⚠️ 验证设置保存必须同时回读 DB + protected/settings_cache.php(项目记忆:只回读一处会漏掉缓存未清的情况)。

6.4 写操作安全顺序(硬性,§11.28)

$this->requireLogin();                       // ① 先登录
Csrf::verifyOrDie($_POST['csrf'] ?? '');     // ② 后 CSRF
// ③ 再限频 → ④ 校验 → ⑤ 身份验证 → ⑥ 写入

顺序颠倒的后果:会话过期时 CSRF 先失败 → 403,且来源页丢失。


7. 视图与前端

7.1 前台改名页(views/front.php)

  • 容器用核心 .mn-section(§6.4);
  • 样式走 section('css'),JS 走 section('js')(若布局支持)或 assets/script.js;
  • 表单:<form method="post" action="…/account/username"> + <input type="hidden" name="csrf" value="<?php echo $this->e(\app\Helpers\Csrf::token()); ?>">;
  • 按 mode 条件渲染:
  • password / both → 当前密码框;
  • email / both → 「发送验证码」按钮(type="button",JS 走 assets/script.js 的 addEventListener,禁内联 onclick)+ 验证码输入框 + 倒计时;
  • 顶部提示区:成功 / 各类错误 / SMTP 降级提示(smtpOk === false 且 mode 含 email 时);
  • 底部:最近 5 条改名历史(history);
  • 双主题:只用 --mn- 变量,禁硬编码色(§11.7);⚠️ --mn-danger / --mn-spectrum- / --mn-radius-* 不存在(项目记忆红线)。

7.2 用户菜单入口(hook/nav_user_menu_items.php)

⚠️ 三个陷阱同时存在:

  1. 双触发(pc + mobile,main.php:274 / :388)→ 必须按 $params['placement'] 分支渲染,且钩子文件顶部禁声明函数/类(include_once 降级会静默少输出);
  2. $this 不可用(闭包作用域,§11.24)→ 用 htmlspecialchars($s, ENT_QUOTES, 'UTF-8') 与 \app\Helpers\Csrf::token();
  3. 图标 14px(nav_user_menu_items 专属,§6.4)→ 用核心字号类 mn-fs-14,禁内联 style=。
<?php
/** @file plugins/username_change/hook/nav_user_menu_items.php */
if (!\app\Helpers\Plugin::isActivated('username_change')) { return; }
if (\app\Helpers\Settings::get('uc_enabled', '1') !== '1') { return; }
if (!\app\Helpers\Auth::isLoggedIn()) { return; }

$placement = (string)($params['placement'] ?? 'pc');
$url  = (\defined('BASE_PATH') ? BASE_PATH : '') . '/account/username';
$text = \app\Helpers\I18n::get('plugin.username_change.menu_entry');
?>

    <?php echo htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); ?>

⚠️ 上面 &#xf4ff; 是占位:实现时必须从核心图标表取 user-pen 的真实码点(grep -A1 "'user-pen' =>" app/Core/TemplateCompiler.php),不要照抄本示例的十六进制。

7.3 后台页(views/admin.php / views/admin_settings.php)

  • ⚠️⚠️ 后台弹层/表单 markup 必须留在 <main class="admin-content"> 内(核心表单/按钮样式全在 .admin-content 作用域,放 </main> 之后 = 「弹出页没有 UI」且零日志);
  • Tab 用核心 .admin-tabs / .admin-tab-btn;
  • 插件后台 CSS 走 section('css'),并注意插件 CSS 注入早于核心 editor.css → 同权重核心后胜(§4/项目记忆)→ 需要提权时用双类(如 .uc-table.uc-table td);
  • 用户列表只读展示 + 单个「改名」按钮(打开表单);改名前显示当前名与新名对照,二次确认。

7.4 零内联 / 双主题 / 图标(验收)

  • 零内联:grep -nE '<style|<script>|on(click|change|submit|load)=' plugins/username_change/views plugins/username_change/hook → 应零命中(<script src> 引用除外);
  • 双主题:默认浅色 + 暗夜星辰各走查一次(§11.7);
  • 图标:$this->icon('名称') 的名称必须在核心 $map 内,否则回退问号 &#xf128;。

8. 用户名校验规则(自定义,因核心无规则)

8.1 规则表

规则默认取值依据 / 说明
先 trim()—与核心注册一致(AuthController:215)
最小长度3 字符(可配 2~64)核心只截断到 ≤64,无下限 → 插件补下限
最大长度20 字符(可配 3~64)与核心的 64 硬上限兼容;默认收得更紧
字符集^[\p{L}\p{N}_\-\x{4e00}-\x{9fa5}]+$(字母/数字/下划线/连字符/中文)⚠️ 需站长确认是否允许中文名、空格、点号
首字符不得为 _ / - / 数字(可配)防 _admin 类伪装
保留字黑名单admin, administrator, root, system, moderator, 管理员, 版主, 站长(可配)大小写不敏感比对
不得等于自己的当前名—无变化直接拒(省一次写入 + 一条无意义审计)
不得等于任何用户的邮箱—防「用户名看起来像邮箱」的钓鱼观感(⚠️ 需站长确认是否要这条)
控制字符 / 换行一律拒防注入与排版破坏
前后空格trim 后与原值不等 → 静默按 trim 后处理与核心一致

⚠️ 这套规则只作用于改名时的新名,不回溯存量用户(核心注册可以造出不合规的名字,插件无权改)。

8.2 唯一性(预检 + 兜底,双保险)

// 预检:走核心 Model,不直查 users 表(§10.1)
$taken = (new \app\Models\User())->findByUsername($newName);   // User.php:27
if ($taken && (int)$taken['id'] !== $uid) { return [false, 'taken']; }

// 兜底:并发下仍可能被抢先 → applyUsernameChange() 捕获 UNIQUE 违约(§4.5)

⚠️ 必须做预检,理由与核心改邮箱一致(ProfileController.php:40-44 注释):users.username 是 UNIQUE,直接 UPDATE 撞名会抛 PDOException → 通用 500,且「成功 / 500」的差异构成用户名占用枚举预言机。

8.3 错误码(→ i18n 键)

错误码语言键文案要点
emptyplugin.username_change.err_empty请输入新用户名
too_short…err_too_short不少于 N 个字符
too_long…err_too_long不超过 N 个字符
bad_chars…err_bad_chars含不允许的字符
reserved…err_reserved该名称已被系统保留
taken…err_taken该用户名已被占用
same…err_same与当前用户名相同
pwd_wrong…err_pwd_wrong密码不正确
code_invalid…err_code_invalid验证码不正确或已过期
code_attempts…err_code_attempts验证码尝试次数过多,请重新获取
rate_limited…err_rate_limited操作过于频繁,请稍后再试
smtp_down…err_smtp_down邮件服务未配置,已改用密码验证
write_failed…err_write_failed保存失败,请稍后重试
not_admin…err_not_admin无权限
cooldown…err_cooldown距上次改名不足 N 天(扩展 1)
points_low…err_points_low积分不足(扩展 2)

⚠️ 语言键必须三语齐全(lang/zh.php / en.php / zh_tw.php),前缀 plugin.username_change.*(§8)。I18n::get() 缺键返回键名原文、永不白屏 → 缺键只能靠全量比对发现,不能靠肉眼。


9. 安全设计

9.1 CSRF(需求 4)

  • 所有写操作(提交改名 / 发码 / 后台改名 / 保存设置)必带 csrf 字段,服务端 Csrf::verifyOrDie($_POST['csrf'] ?? '');
  • 若用 AJAX 发码:FormData 带 csrf 或 X-CSRF-Token 头(红线 8);
  • 改名成功后可选 Csrf::consume()(对照核心改密码的做法 ProfileController.php:80,防 Token 重放)。

9.2 限频(需求 4)

动作action 名默认阈值保护对象
提交改名uc_submit10 次 / 3600s防刷
密码校验uc_pwd5 次 / 900s防暴力猜密码(需求点名)
发验证码uc_code3 次 / 3600s防刷邮件(需求点名)
后台改名uc_admin30 次 / 600s防误操作/脚本

  • ⚠️ check() 返回 true = 允许;
  • ⚠️ action 名不得复用核心已有名(login / resend_verify / forgot_password…)—— 复用会共享同一个计数器,互相挤占额度;
  • 需要向用户报剩余等待时间时用 RateLimiter::retryAfter($action, $max, $win)(不要写死整窗口);
  • 若走 hit()/hitConfig()(直接输出 429),注意它会自己再判一次。

9.3 HTTPS(需求 4)

  • 插件级开关 uc_force_https(默认 '1');
  • 判定(兼容反代):$_SERVER['HTTPS'] === 'on' 或 (int)$_SERVER['SERVER_PORT'] === 443 或 ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
  • 未命中时:
  • 若 SITE_URL(或 uc_https_host)是 https://… → 302 跳到 https 同路径;
  • 否则(站点压根没 HTTPS)→ 不跳(避免跳死循环),改为页面顶部警示 + 提交时若 uc_force_https=1 则拒绝并提示站长;
  • ⚠️ 核心无统一 isHttps() 助手(各文件各自判 $_SERVER['HTTPS']),插件自建一个小函数即可,不要改核心。

9.4 越权 / IDOR

  • 前台改名只能改自己:$uid 一律取 $_SESSION['user_id'],绝不从 $_POST['user_id'] 取;
  • 后台改名:目标 id 一律 (int) 强转 + 校验用户存在;
  • 所有 SQL 走 prepare() + 绑定参数(红线 2);
  • 后台控制器继承 Admin\BaseController(门禁在构造函数里,继承 app\Core\Controller 不触发)。

9.5 管理员判定唯一口径

if (!\app\Helpers\Auth::isAdmin()) { /* 拒绝 */ }   // role==='admin' && group_id===4(AND)

禁自造(项目记忆:插件自造管理员判定 = 越权高危,已复发 2 次)。


10. 缓存一致性(需求 5)

改名是「当前请求还是旧名 / 游客看到旧名」的典型场景,必须三处都处理:

#位置调用说明
1Auth 静态缓存\app\Helpers\Auth::clearUserCache();(Auth.php:107)否则本请求后续(含视图渲染)读到的仍是旧名
2审计 session 名\app\Helpers\AuditLog::setSessionUsername($newName);(:218)否则后续审计日志仍写旧名($_SESSION['audit_username'] 是登录时写入的)
3前台页面缓存\app\Helpers\PageCache::invalidate();(:151)⚠️ 仅列表页且仅游客;游客在列表页看到的是「按 user_id 现取」的名字,理论上无需清,但按规范 §11.5-31 一律调用更稳妥
4设置缓存(改设置时)\app\Helpers\Settings::clearCache();(:242)⚠️ settings_cache.php 不自动补新键 → 上线新设置项后必须清一次
5插件设置缓存本插件无设置走核心 settings_cache.php,无需自建

⚠️ 改的是自己时才需要同步 #1 / #2(管理员改别人 → 那人的 session 在别的进程,下次请求自然重取;无需也不能动)。

10.4 头像首字母(可选)

User::defaultAvatarPath() 生成的 SVG 首字母取自 username,但落盘路径是 seed_avatars/{月}/{id}.svg(不含用户名)→ 改名不破坏头像文件,只是 SVG 里的首字母仍是旧名。

选项行为风险
保持旧字母(默认推荐)什么都不做视觉上「名字变了但头像字母没变」,可接受
重生成调 User::defaultAvatarPath($uid, $newName, $createdAt) 覆写同名文件需确认该方法只对默认头像生效(用户上传过头像时绝不能覆写,否则把用户头像冲掉)

⚠️ 若选「重生成」,必须先判断当前 users.avatar 是否为空/是否指向 seed_avatars/,否则会覆盖用户自传头像。建议本期不做,留作后续可选。


11. 审计(需求 6)

统一走核心 AuditLog::log(string $action, string $target_type = '', int $target_id = 0, string $detail = '')(AuditLog.php:110):

\app\Helpers\AuditLog::log(
    'username_change',                                  // action
    'user',                                             // target_type
    $uid,                                               // target_id
    '用户名变更: ' . $oldName . ' -> ' . $newName       // detail(旧名 → 新名)
);

场景actiondetail
本人改名username_change用户名变更: 旧名 -> 新名
管理员改名username_change管理员改名: 旧名 -> 新名(+ 管理员 id 由 AuditLog 自带的 admin_id 字段记录)
发验证码(可选)username_change_code发送改名验证码(⚠️ 量大,默认不记,避免审计表膨胀)

⚠️ audit_logs.username 是历史快照列(Schema.php:615,TEXT DEFAULT '')→ 日志里的「谁」是当时的名字,不会被后续改名改写(这正是审计要的效果)。

⚠️ 改名后必须同步 $_SESSION['audit_username'](§10 #2),否则本条日志之后的日志仍写旧名。


12. 可选扩展(先留字段,本期不启用)

扩展预留启用方式依赖
1 改名频率限制(30 天一次)uc_state.last_changed_at + uc_cooldown_days(默认 0 = 不限)提交前查 uc_state:time() - last_changed_at < days*86400 → 拒 cooldown⚠️ 冷却检查用 $diff >= 0 保护未来/异常时间戳
2 改名扣积分uc_points_cost(默认 0 = 免费)写名前调 \app\Helpers\Points::deduct($uid, $cost, 'username_change', 0, 'username_change')⚠️ 必须用 Points::deduct()(条件原子扣减,§11.8 红线),禁 UPDATE users SET points=points-…;⚠️ 扣积分要在写名之前,且失败要能回滚(同一事务或先扣后退)
3 改名历史时间线uc_history 表本期即建即写(成本极低)前台改名页底部已展示最近 5 条;后台可查全量无

⚠️ 扩展 2 与「唯一写入点」的事务边界要一起设计:积分扣减 + 用户名写入若跨库(points 在核心库、uc_* 在插件库),无法用一个事务包住 → 建议顺序为「先扣积分(核心库,自带事务)→ 再写名(核心库)→ 最后写插件库历史」,任一步失败则补偿(积分 Points::award() 退回)。


13. 红线与自检清单

13.1 最高级红线(8 条,一票否决)逐条对照

#红线本插件如何满足
1禁 unserialize/eval/assert/preg_replace('/e')全程 json_decode;无动态执行
2禁滥用绝对路径全部基于 __DIR__ / BASE_PATH;不用 $_SERVER['DOCUMENT_ROOT']
3渲染钩子禁 die()/exit()/header()nav_user_menu_items / init_after 一律 return;
4上传白名单 + 强随机名本插件无上传功能(不适用)
5数量原子扣减扩展 2 走 Points::deduct()(条件原子);验证码 attempts 用 UPDATE … SET attempts = attempts + 1
6IN (...) 强转 + 占位符用户列表/历史查询若有 IN,array_map('intval') + 占位符
7PDO 不覆盖核心属性Plugin::db() 原样用,不设任何 setAttribute
8AJAX 必带 CSRF发码接口必带 csrf 字段/头

13.2 规范 §11 相关条目对照(本插件涉及项)

条目落点
§11.4-24 每请求 hook 查询 ≤ 5§4.4:本插件合计 0 次
§11.5-30 改 hooks 后更新 plugins_cache.json交付后由系统重建;改 hooks 清单时必须刷新
§11.5-31 改布局钩子后清页面缓存调 PageCache::invalidate()
§11.5-34 卸载残留自检§4.3 uninstall() + 交付前自查
§11.8 核心库访问边界§4.5:无专属方法的 username 经 Model execute();扩展 2 走 Points::deduct()
§11.15 建表兜底两条路init_after(marker 短路)+ db() 自愈(可选)
§11.22 admin_route_register 不写 /admin§6.1
§11.24 渲染钩子 $this 不可用§7.2
§11.25 核心无 Plugin::budget()§4.4 自实现
§11.28 Auth::loginUrl() + 先登录后 CSRF§6.4;视图里用 $this->loginUrl(),钩子里用 Auth::loginUrl()
§11.32 后台设置字段缺失静默写最小值§6.3 array_key_exists
§11.34 命名空间逐字节 = 目录名§4.1 + 自检
§11.34b 前台路由避让真实目录§6.1(/account/... 已核实安全)
§11.35 投稿包结构(若走前台投稿)§13.4

13.3 发布前必跑的自检(4 个)

# ① 命名空间大小写体检(Linux 部署必做,本机 Windows 全绿不代表 Linux 能跑)
python .workbuddy-ai/tools/plugin_case_lint.py plugins/username_change
#     → 期望:「全部命名空间在 Linux 精确匹配下均可被自动加载」

# ② i18n 三语键全量比对(应零缺失)
php .workbuddy-ai/tools/fh_i18n_check.php plugins/username_change

# ③ 零内联(应零命中,

附加自检:

# 语法
for f in $(find plugins/username_change -name '*.php'); do php -l "$f"; done

# 登录链接不得裸写 /login(§11.28)
grep -rnE 'href="[^"]*/login' plugins/username_change/views plugins/username_change/hook   # 应零命中

# 图标名必须在核心表内
grep -cE "^ *'user-pen' *=>" app/Core/TemplateCompiler.php   # 应为 1

13.4 发布路径(两条,二选一)

路径命令 / 入口包结构
后台发布(推荐,直接上架)php cli/release_build.php --type=plugin --name=username_change --version=1.0.0 --sign --key-id=flinthub-2026 --notes=<说明文件>,先 --dry-run 过 9 项门禁允许根级 改动说明.md
前台投稿/plughub/submit(build_submit_zip.py + verify_submit_zip.php)禁止任何根级文件,必须 plugins/username_change/… 单根

⚠️ 后台发布出完包后必须再跑 --type=index 重建 release/plugins/index.json,否则前台橱窗看不到新版本。

⚠️ ~~交付 3 个文件:release/plugins/index.json + data/market/packages/.zip + data/market/manifests/.json(不传 data/market/mirrors/)。~~ 🔴 【已作废 · 见下方更正】

⚠️ plugin.json 的 version 必须与包名一致(否则商店永远显示「可升级」)。

🔴 【2026-10-09 更正 · 以上第 2 条「交付 3 个文件」已作废】

正确口径:只交付 2 个文件 —— data/market/packages/.zip + data/market/manifests/.json;

release/plugins/index.json 禁止上传。

理由:它是 data/market/manifests/ 的确定性函数(HubCatalog::rebuildIndex() 只

glob('manifests/manifest-*.json') 再写 Plugin::indexFile())。上传本地那份会

① 抹掉只在 Hub 上的社区投稿;② 复活已下架插件(Hub 把它们移进了 manifests-off/)。

正确做法 = 增量摆上 2 个文件后,到 Hub 后台 市场 Hub → 设置 → 「重建清单」。

⚠️ --type=index 仍要跑 —— 但那是给本机(发布端 = Hub 同机) 重建本地橱窗用的,不是「上传」。

权威依据:待办事宜/FlintHub_发布流程手册.md §12.5 + 红线表 #8;

明细见 .workbuddy-ai/memory/REF_发布流程.md §A.1.8 ⑤ / §A.1.9 ③。


14. 建议开发阶段划分

阶段内容产出验证方式
P1 骨架目录 + plugin.json + Plugin.php(3 表 DDL + 生命周期 + class_alias + budget)+ 4 个 hook 骨架 + 三语空包可安装/可激活/可卸载激活后建表成功;plugin_case_lint.py 通过;卸载后无孤儿表
P2 前台改名(password 模式)FrontController + front.php + 校验/唯一性/写入/缓存/审计密码模式端到端可用探针跑:正常改名、重名、非法名、错密码、限频、并发抢先
P3 三选一验证 + 邮件uc_requests 发码/验码 + send-code 路由 + 邮件模板 + SMTP 降级三种模式可用探针跑:发码限频、码过期、超尝试次数、SMTP 未配降级
P4 后台管理AdminController(搜索/改名/设置)+ admin.php/admin_settings.php + Tab管理员改名 + 设置保存HTTP 冒烟(flinthub-admin-http-smoke):路由 200/302、CSRF 负例、设置回读 DB + 缓存
P5 收尾与发布说明.md + 清理测试数据 + 4 个自检 + 出包可分发包--dry-run 门禁全绿 + 出包后 --type=index

每个阶段结束都跑一次「业务逻辑探针」(CLI 或 HTTP),界面观感由站长自测(分工约定)。


15. 风险与待站长确认项

15.1 必须站长拍板(3 项)

#问题选项本方案默认
1users.username 写入路径A 经 Model execute()(推荐)/ B 核心新增 updateUsername() / C 裸 PDOA(含 method_exists 前瞻)
2SMTP 未配置时的行为① 自动降级仅密码 + 双端警示(推荐)/ ② 硬阻断 / ③ 仅提示①
3改名后头像首字母保持旧字母(推荐)/ 重生成 SVG保持

15.2 建议站长确认(4 项)

#问题本方案默认
4新名字符集是否允许中文?是否允许空格/点号?允许中文;不允许空格/点号
5是否禁止「用户名 = 别人邮箱」?禁止
6后台管理员改名是否也受冷却/扣积分约束?不受(管理员豁免)
7扩展 1/2(冷却、扣积分)本期是否要启用?本期只留字段,不启用

15.3 已知风险

风险影响缓解
核心注册规则宽松 → 存量用户名可能不合插件规则只影响新名,不回溯规则只作用于改名;文档写明
Model::execute() 返回值语义需核实写入成功判定可能误判§4.5 已改为「回读校验」而非依赖返回值
核心 settings 无删除接口(需核实)卸载可能留空串 key实现时 grep 确认;必要时按既有插件做法
并发改名同一用户名唯一性预检通过但写入撞车applyUsernameChange() 捕获 UNIQUE 违约 → 返回 taken
跨库事务(扩展 2 积分)无法原子补偿式:先扣后写,失败退回
前台路由 /account/... 与将来核心新增同名路由冲突路由注册顺序 + 文档记录;已避让现有真实目录


附:本方案已完成的事实核查清单(可复现)

# 需求①校准:mention_user_id 全仓不存在
grep -rn "mention_user_id" app/ plugins/ cli/          # → 零命中

# 需求②校准:核心注册无用户名字符规则
sed -n '215,222p' app/Controllers/AuthController.php  # → 仅 trim + ≤64 截断
sed -n '489,500p' app/Models/User.php                  # → create() 只 password_hash

# 改名能力:users.username 只有两处写
grep -rn "username" app/Models/User.php | grep -iE "insert|update|set"   # → create / updateByAdmin(不含 username)

# 核心自己怎么写 users 表(路径 A 的先例)
sed -n '40,53p' app/Controllers/ProfileController.php

# Model 白名单
sed -n '18p' app/Models/User.php                        # → $fillable = ['last_login']

# 唯一约束
sed -n '300,328p' app/SplitDB/Schema.php                # → username TEXT UNIQUE NOT NULL

# 分类白名单
sed -n '22,23p' app/Controllers/Admin/PluginController.php

# 图标名
grep -cE "^ *'user-pen' *=>" app/Core/TemplateCompiler.php   # → 1

# 限频阈值硬编码清单(插件加不进去 → 用自己的键)
sed -n '151,163p' app/Controllers/Admin/SettingsController.php

# 站点根真实目录(路由避让)
ls -d */

# 钩子触发点(行号会漂移,引用前复核)
grep -rn "nav_user_menu_items" app/Views/layouts/main.php


本方案为规划文档,未改动任何代码。评审通过后按 §14 分阶段落地。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 2 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录