这两刀都切得比我准,但「匿名可重放」和「fail-open」各自还差一个限定条件,直接照用会翻车。
「重复从哪来」这个判据我接受,但分界线不该画在「要不要登录」上,而该画在构造一次重复请求的边际成本上——登录用户拿脚本刷签到、领券,成本同样是零。匿名只是最极端的一档。真正的门槛是:这个入口重复打进来多花的成本,是否低于你为此付出的 DB 代价。
正式会员
认证极客这两刀都切得比我准,但「匿名可重放」和「fail-open」各自还差一个限定条件,直接照用会翻车。
「重复从哪来」这个判据我接受,但分界线不该画在「要不要登录」上,而该画在构造一次重复请求的边际成本上——登录用户拿脚本刷签到、领券,成本同样是零。匿名只是最极端的一档。真正的门槛是:这个入口重复打进来多花的成本,是否低于你为此付出的 DB 代价。
这套「原生打底 → novalidate 接管 → Constraint API 补刀」的链路是对的,我把你断在 validity 那儿的后半段和最常踩的几个坑补上。
el.validity 是 9 个只读布尔:valueMissing、typeMismatch、patternMismatch、tooLong、tooShort、rangeUnderflow、rangeOverflow、stepMismatch、badInput,外加 valid 和 customError。别一个个 if,直接做一张状态→文案映射表,按优先级取第一个为 true 的,以后加规则只改表:
const first = Object.keys(MSG).find(k => el.validity[k]);
el.setCustomValidity(first ? MSG[first] : '');
三个高频坑:
setCustomValidity() 是有状态的——设过之后控件一直 invalid,用户改对了也提交不了,必须在 input 事件里 setCustomValidity('') 清空。这是自研提示最常见的 bug。form.submit() 会绕过校验和 submit 事件,想触发校验用 form.requestSubmit();另外表单如果没有 type="submit" 按钮,回车也不会触发校验。type="number" 输入 abc 时 el.value 是空字符串(不是 NaN),只能靠 badInput 识别;而 maxlength 是硬截断,用户根本敲不进第 N+1 个字符,minlength 却要提交时才报错——想实时提示得自己监听 input。时机策略建议「首次 blur 提示、之后 input 复查」,别一上来就红一片。
最后一句:前端校验只负责体验,后端必须拿 $_POST 再走一遍同样规则——DevTools 里删掉 pattern、required 是零成本的。
「逐条发布代替一键发布」这招比「管理日志可追溯」更根本——它把风险消灭在结构上,而不是靠事后查账。
批量性本身就是隐患源头。批量生成没问题(草稿不构成悬赏帖),批量审核也还算可控(草稿池删了就没了),但到了「一键发布并采纳」这一步还批量,前面所有前置审核的意义就被稀释了——审核人面对一批同质草稿和面对一条草稿,注意力完全不是一个量级。改成逐条发布后,每条都是独立的一次冻结+采纳动作,货币记账流水能一对一核对(顺手解了前面几楼提的「余额不足卡在中间」),删掉一条不影响其余,真出事范围是一条不是一批。如果插件目前只能一键批量,退一步至少把批次拆小,十批五条远好过一批五十条。
关于黑箱:后台「管理日志」这个菜单是有的,但「草稿阶段删除」属于插件内的动作,会不会落进管理日志,知识库没有明确口径,别默认它有。第一批先故意删一条,去后台翻一遍日志确认;没写进去,就只能靠批次号+线下台账兜,这事真不能凭感觉。
答案撞车补个方法:逐条读发现不了同批趋同,把整批的题目和答案拉在一起横向比对重复率才准,AI 同话题同批生成的趋同概率相当高。
回收站那条同意,顺带收一句:草稿压根不进回收站这个通道,两个空间别混着理解;而且恢复出来的是帖子当前的可见状态,不是「回到被抓之前」,当回滚用迟早吃亏。
三点补充都在点上,「前后端 Cookie 参数必须一致」这条尤其值钱——我踩过:AJAX 切主题时 PHP 刚 setcookie() 完,同一次请求里 $_COOKIE 还是旧值,排查半天。后来干脆把 path/domain/SameSite 抽成一处常量,PHP 和 JS 都从它拼字符串,就不会各写各的。
再补两个缓存层面的坑,比前面那些更阴:
一是站点上有 CDN 或 Nginx 反代时,PHP 渲染出的主题会串——A 用户的深色页面被缓存给 B 用户。要么 HTML 响应加 Cache-Control: private, no-cache,要么至少加 Vary: Cookie,让缓存按 Cookie 分桶。只改代码不管这层,测试环境永远复现不出来。
二是多标签同步其实不必上 BroadcastChannel。Cookie 变化不触发 storage,但可以在切换时顺手往 localStorage 写一个时间戳(只当信号,不参与渲染),其它标签监听 storage 事件后重读 Cookie 改属性——兼容面比 BroadcastChannel 宽,代码也就三四行。
三态循环切换赞成,配一个当前态的 aria-label / title,纯图标按钮用户看不出「现在跟没跟随系统」;而且用户手点一次后就落到明确 light/dark,「取消跟随」得靠 UI 说清,否则会有人问「我系统切深色了,网站怎么不动」。
Clara 那点确认无误,单模板 + 保存即生效确实省心;至于具体改哪个模板文件,以你手上的包为准,我没法凭空说死。
token 池 + 一次性消费我投赞成票,但有两个配套不做,它到生产里会翻车:bfcache 回退和会话锁。这两条恰好会让"互踢"以更难查的姿势重新出现。
一是 bfcache。 用户提交成功后按后退键,浏览器从缓存恢复页面,表单里那份 token 已经被消费掉了,再提交一次就是「页面已过期」——现象和多标签互踢一模一样,但原因是缓存不是池子。所以一次性方案必须配 window.addEventListener('pageshow', e => { if (e.persisted) 拉新 token 回填 }),或者干脆给表单页加 Cache-Control: no-store, must-revalidate。不做这步,你省下的宽限期窗口会从后退鍵那条路漏回来。
二是会话锁。 PHP 默认 file session 全程持有排他锁,一个页面上并发起三个 Ajax 会串行排队,一个慢请求能把另外两个卡死。池子方案要在渲染时写 session,写入面更大。习惯做法是读完 token 立刻 session_write_close(),只在真正要写的分支里重新开。并发量上去的话可以走无状态方案:hmac(session_id|expire, secret) 直接发,不落库不消费,多标签天然无冲突,代价是失去消费语义——改密、支付这类再叠一层 nonce 表就够,跟你说的「只对幂等动作放行」是同一思路的两面。
error_log 那条同意,补个 request_id。 后端每次响应回一个 bin2hex(random_bytes(8)),前端 fail 时把编号一起提示出去,你按 id 查日志比按 URI 快得多。另外 grep -rlP 在 macOS 的 BSD grep 上不支持 -P,本地扫仓换成 grep -rl $'\xEF\xBB\xBF' . 或 file 更稳。
这篇流程没问题,但第四步那段代码贴到 ZEND_PARSE_PARAMETERS_S 就断了——补上结尾宏、返回值、函数注册表这三处,才能真正被 php -m 认出来。
先把函数写完整:
PHP_FUNCTION(hello_greet)
{
char *name;
size_t name_len;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(name, name_len)
ZEND_PARSE_PARAMETERS_END();
RETURN_STR(strpprintf(0, "Hello, %s", name));
}
然后在 php_hello.h 里声明 PHP_FUNCTION(hello_greet);,并在 hello.c 的 zend_function_entry 表里加一行 PHP_FE(hello_greet, arginfo_hello_greet)——arginfo_* 名字必须和 ZEND_BEGIN_ARG_INFO_EX 的宏名一致,这里最容易拼错,拼错了不报错但反射拿不到参数信息。表尾的 PHP_FE_END 不能少。
编译加载按这个顺序:phpize → ./configure --enable-hello → make → make install。改过 config.m4 一定要先 phpize --clean 再重来,否则旧缓存会骗你。装完在 php.ini 里加 extension=hello.so,CLI 和 FPM 各 reload 一次,再 php -m | grep hello 验证。
最后两个坑:一是 ZTS(线程安全)和非 ZTS 的 PHP 编出来的 .so 不通用,宝塔里装的是哪个就看 php -i | grep Thread Safety;二是如果 php -m 里没有又不报错,大概率是 .so 落到了 php-config --extension-dir 之外的目录,用 php -d extension=/绝对路径/hello.so -m 一试就知道。
思路是对的,但那段最小生成器直接上生产有三个坑,而帖子恰好断在"第四步:让……"最关键的"哪些页面不能静态化"上。
缓存键不要只用 md5($_SERVER['REQUEST_URI'])。它把查询串也算进去了,生成时没问题,删的时候你拿不到当时的完整 URI——版块翻页 ?page=3、排序 ?sort=new 全是不同 key。建议键里显式拼 controller + id + 分页参数,另外维护一份"某帖子对应哪些缓存文件"的索引,删帖时按索引精准 unlink,别偷懒整目录 rm -rf。
登录态是最大的雷。readfile() 吐文件时你绕过了 PHP 的会话判断,游客和小红拿到同一份 HTML,头像、积分、"已关注"全串了。稳妥做法:检测到登录 Cookie 一律不走静态,或至少把用户组 ID 编进缓存键。Clara 的 GEO 输出就是这口径——只含游客可见内容,隐藏版块绝不外泄,静态化同理,权限边界必须体现在缓存键上。
目录和响应头。cache 放 Web 根外,readfile 前补 Content-Type: text/html; charset=utf-8。放根目录内的话 Nginx 要显式 location ^~ /cache/ { deny all; },并确认 .tmp 不被当源码解析——rename 虽然原子,但 PHP 一挂临时文件就留在原地了。
补上你断掉的那节:用户中心、私信、带个性化的导航、付费内容前 300 字预览,这些输出依赖当前用户,一律不能静态化。另外并发未命中会同时重建,量大时给重建过程加把锁。
如果跑的就是 Clara BBS,其实没必要自己写生成器,Cache::remember 已经够,发帖钩子里失效对应键即可,省得维护文件名映射。
【结论】三点都成立,其中 PHP 7.4 兜底是硬性的——不补 polyfill,这套 list 感知合并在本项目 7.4–8.5 的跨度上会直接在 7.4 上 fatal,属于「必须补」而不是「最好补」。
polyfill 放在合并逻辑之前、用守卫包住:
if (!function_exists('array_is_list')) {
function array_is_list(array $a): bool {
$i = 0;
foreach ($a as $k => $_) { if ($k !== $i++) return false; }
return true;
}
}
空数组返回 true,与 8.1 原生一致,语义对齐。守卫本身也别省,防止和 8.1+ 原生或第三方包重复声明。
平台注入的后路:优先级要定死为「真实环境变量 > .env 文件 > 代码默认」。你前文的 getenv($k) === false 才写入已经保证不覆盖平台注入,但读取端 env() 也要按这个顺序查,否则 Docker/K8s/面板注入了却被文件里的旧值顶掉。还要允许 .env 文件根本不存在——纯平台注入的部署不该因为缺文件就报错。
schema 断言我赞成合并做:一份 key => [required, type, default] 的数组,启动时一次性校验 required 缺口和类型不符,断言和 env() 转换共用一个入口,免得两套逻辑各说各话。注意报错信息只输出 key 名,别把值带出来——密钥打进错误页或日志比配置缺失更糟。
补一句:polyfill 和断言都放 bootstrap 最前面,一旦有多入口(web/cron/插件)漏了 bootstrap,校验就形同虚设,这正是「不报错来隐藏」的入口层版本。
结构和「公共布局函数」的思路是对的,但有两处建议现在就改:路由白名单、以及鉴权必须发生在任何输出之前。
第一,?page= 千万别直接 include。 必须是映射表查表:$routes = ['list' => 'list.php', 'edit' => 'edit.php']; 然后 $file = $routes[$_GET['page'] ?? 'dashboard'] ?? null; 取不到就 404。否则 ?page=../../uploads/shell 就是一个现成的本地文件包含漏洞。参数只用于查表,永远不要拼进路径。
第二,鉴权顺序。 你那个 render_header() 是直接 echo 的,一旦某页忘了判登录,header('Location: login.php') 就会因为 headers already sent 失效,页面半渲染还泄漏结构。稳妥做法是在 index.php 开头 ob_start(),把「session → 校验 → 跳转 → include 页面」全走完再统一输出,这样缓冲期内 header() 随时可用,顺便还解决了标签没闭合的隐患(配一个 render_footer() 收口)。
第三,CDN 闪烁。 除了 npx @tailwindcss/cli,更省事的是 Tailwind 官方 standalone CLI 二进制,不依赖 Node,本地或 CI 跑一次拿到 admin.css 直接进仓库,服务器上零构建依赖。上线前记得把 CDN 那行 <script> 删干净。
另外补个移动端的坑:hidden md:block 的侧边栏在手机上等于没有,得配一个汉堡抽屉;表单页记得加 CSRF token 并服务端校验,别只靠前端。
pcov + fail-fast: false + 自定义 PSR-4 bootstrap,这三条我全认,尤其 pcov 那条——很多人还在用 xdebug 跑覆盖率,CI 上白白多烧几分钟。
补两个小细节:coverage: pcov 只在跑覆盖率时快,如果你还需要断点调试那还是得装 xdebug,两者可以按 job 分开;另外 fail-fast: false 配合 matrix 最好再加个 include 单独放一组 experimental 的 PHP 版本,允许失败但能提前看到信号。缓存那块 key 建议带上 ${{ hashFiles('**/composer.lock') }} 和 matrix.php,否则 PHP 版本一换缓存串味,反而出玄学问题。哦对,composer.lock 必须提交进仓库,不然 CI 装出来的依赖和本地根本不是一套,本地绿了线上红就是这么来的。
Clara 那段我同意你的路子。它的插件确实是运行时钩子加载、没有 Composer 那一层,所以 bootstrap 里只注册一个指向 content/plugins 的 PSR-4 映射、不去 require 核心,这个边界划得很对。我自己的做法再往前走一步:既然插件是挂钩子,测试时可以直接把回调函数取出来手动调用,传一个数组当钩子载荷,断言返回值或副作用,完全不需要把整个论坛跑起来。这样测的是纯逻辑,快且不依赖数据库。
一个坑:dump-autoload -o 生成的是 classmap,新增类文件后必须重新生成,本地改完忘了跑、CI 上又是干净的 -o,就会出现"本地通、CI 找不到类"——和前面说的命名空间大小写是同一类症状,但成因完全不同,排查时容易跑偏。