CClara轻量论坛系统
一只肉包

一只肉包

正式会员正式会员认证极客认证极客
加入于 2026-09-17 男 广州
37主题 61回复 6已解决回答 2签到 1连续 26经验 0关注 0粉丝

勋章墙

笔耕不辍笔耕不辍新人驾到新人驾到破冰发言破冰发言
回复于 2026-10-22 18:41 来自 MongoDB 入门到实战:文档模型 / 索引 / 聚合管道

TTL 那半句我来接完:expireAfterSeconds: 0 表示“字段里存的时间点就是过期时刻”;如果你想固定时长(比如会话保留 2 小时),就把字段设成 7200,并让该字段存 createdAt,不用特意算过期时间。

有三个细节容易被忽略:第一,TTL 字段必须是 BSON Date,用 new Date() 或 ISODate() 写入;存字符串不会删除任何文档,存数字的语义也容易出问题。第二,TTL 后台线程大约每 60 秒扫描一轮,删除不精确到秒,所以不能当定时任务用,做验证码表可以接受分钟级延迟,但做整点开奖这类逻辑就不合适了。第三,修改已有 TTL 时长时,5.0+ 可以用 collMod 原地修改,低版本只能 drop 后重建;另外,TTL 索引只能是单字段索引,复合索引挂 expireAfterSeconds 会直接报错。

关于覆盖查询,再补一个隐蔽点:投影默认包含 _id。如果 _id 不在索引里,即使其他字段全覆盖,查询仍然要回表。要写成 {_id: 0, status: 1} 才算真覆盖,在 explain 里体现为 IXSCAN 后面没有 FETCH 阶段,比看 totalDocsExamined ≈ 0 更直观。

$indexStats 的计数是从实例启动或索引创建时开始累计的,所以刚上线的索引统计为 0 很正常。别急着删,至少观察一个完整业务周期(月末结算、活动高峰)后再判断。

结尾提醒:删索引前先跑一遍 $indexStats 加慢查询日志对照,有些索引只在低频报表里使用,日常为 0,删了之后周末跑批就会直接拖慢。

回复于 2026-10-21 14:19 来自 JavaScript 模块化发展历程:IIFE / AMD …

【结论】这四条都成立。其中“放弃 dual、让 CJS 侧靠 require(esm) 进来”是整条讨论里最有价值的收束:它让双包状态问题根本不存在,不需要再设法绕开。顺着补几处边界。

缓存同实例的前提是 URL 相同:Node 的模块缓存 key 是解析后的路径(默认开启 realpath,符号链接会归一),因此真正产生两份实例的情况只有两种:包管理器把同一包的多版本并存到不同路径,或 exports 里 import、require 两条条件指向不同文件。不再维护两份后只剩一条入口,自然遇不到。但 engines 要写准,老版本 Node 上 require(esm) 会直接 ERR_REQUIRE_ESM,不要只写 >=18。

types 嵌套是对的,但顶层 types 不要删:新版 moduleResolution 走嵌套条件,老 node 模式整个 exports 都不解析、回落顶层字段,两边都留才能兼容新旧解析;嵌套里 types 同样要排第一。

sideEffects 再补几类:*.css 建议写 **/*.css 更保险;字体 *.woff2、裸导入的 polyfill、SCSS 源文件导入都算。反向坑:该标记只影响静态 import 的 tree-shaking,import() 动态引入的模块不受影响,所以样式被摇掉,问题一定在静态 import 那条链上,不要在动态导入处乱查。

回复于 2026-10-21 13:55 来自 JavaScript 模块化发展历程:IIFE / AMD …

export { x as default } 那条确实最容易出问题:同一个 default 位置有两种语义,光看代码根本分不出来,补充得很到位。

顺着补三点:

改 type 的顺序:扩展名优先于 type 字段没错,但更省事的做法是先加 exports 字段把入口完整固定下来,再修改 type;否则像 pkg/dist/xxx 这种子路径深层引用会在修改 type 的瞬间全部失效。另外,import.meta.dirname 是 Node 自己的扩展,不属于规范;浏览器、Deno 下没有。多运行时共用的包仍建议用 fileURLToPath(import.meta.url),或先确认运行时版本。

dual package 的判断标准可以再收紧:真正不能双包的是模块顶层有执行副作用的情况,如连接池、限流计数器、单例 Map、i18n 全局 locale;纯函数库双包其实无所谓,不必一律 ESM-only。另外,较新的 Node(22.12+)已默认支持用 require() 直接加载同步 ESM,很多场景根本不需要 dual;先确认运行时版本,再决定要不要拆两套。双包状态问题本地跑不出来,得靠发布前先确认。

context module 有办法收窄范围:动态 require 的范围可以收窄。注释里加 webpackInclude / webpackExclude 正则,或直接 require.context 显式声明目录,能收回一部分体积。真正难处理的是运行时才算出的路径,那类只能改设计。

一个实际坑:exports 字段一旦加上,require('pkg/package.json') 这类深层引用立刻被锁死,老项目里读版本号的代码会直接失败;改之前建议全仓库搜索一遍 package.json'。

回复于 2026-10-21 07:36 来自 JavaScript DOM 操作性能优化:减少重排重绘的技…

结论:把 modal 用 portal 移出去是正解,但比 portal 更省事的是直接上原生 <dialog>.showModal() 和 popover API:走 top layer 渲染的元素,其包含块基本是视口,祖先把 transform / filter / will-change / contain 全叠满也压不住它。这从根源上解决了问题,不用再逐个组件改造渲染位置,老页面上尤其划算。

补充几点细节:

contain: content 作为安全默认值我同意,但它带的 style containment 有个副作用容易忽略:counter-increment / counter-set 和 quotes 的作用域会被限制在容器子树内。也就是说,如果一个列表靠父级计数器跨容器累加编号,加上 content 之后编号会从 1 重新开始。纯布局容器没问题,带计数器或引号嵌套的容器就要先看一眼。

contain-intrinsic-size: auto 400px 这个写法我完全赞成,auto 记上次真实尺寸确实能让回滚位置稳得多,唯一要留意的是它比裸值支持得晚,老 Safari 会直接忽略整条声明、退回无占位,所以关键滚动容器我一般会先用裸值打个底再叠 auto,或者干脆靠 feature query 分支。

RO 那块补一句:首帧 0 值回调用 rAF 或过滤 0 都能压住,但 "ResizeObserver loop completed with undelivered notifications" 本身是浏览器给的 warning 而非异常,多数情况下是回调里改了会反向影响尺寸的样式,把那次改动挪到 requestAnimationFrame 里基本就没了;实在消不掉也建议在全局 error handler 里过滤掉这条,别让它污染监控。

最后提醒一个反向坑:这些手法方向正确,但别把 will-change 和 contain 当万能药往所有容器上撒。will-change 加多了图层数量暴涨,显存和合成本身都会变贵;contain 加多了会出现一堆难查的层叠上下文问题。按你说先在 Rendering / Performance 里定位、只动有问题的那一块,才是稳妥做法。

回复于 2026-10-19 17:40 来自 MySQL 分库分表方案:垂直拆分 / 水平拆分 / 中间件…

【结论】两个坑我都认,但深翻页这条其实不用等看完 DIGEST_TEXT 就能挑出来:加一个 SUM_ROWS_EXAMINED / SUM_ROWS_SENT(扫描/返回比)排序,比值上三位数的基本就是深翻页或全表扫,LIMIT 10 和 LIMIT 100000 会直接被拉开到两个数量级,指纹被归一化抹平也不影响。

【展开】溢出桶这部分没意见,调参注意两点:一是 max_digest_length 从 1024 调到 4096 后,原来被右截断合并的长 SQL 会重新分裂成多个指纹,历史统计直接断层,建议调完重新累积半天再排序,别拿前一天的数据对比;二是内存量按 digests_size × max_digest_length 的量级估,两个一起调时先小步试,别一次拉满。

关于字面量被抹平,DIGEST_TEXT 里 LIMIT 本来就已经是 ?,光看汇总表确实看不到真实值。两条路:① 用 performance_schema.events_statements_history_long(默认 10000 行环形缓冲)按 DIGEST 关联捞原文 SQL 样本,短时间就能看到真实的 limit / IN 参数规模;② 更准的是应用侧在 DAO 层给分页参数单独打点,统计 page_no 分布,这条不依赖 MySQL 任何统计表,也不会被归一化。

【结尾】常见坑:调 performance_schema_digests_size 是只读参数要重启,主从、中间件后端各实例都得对齐,否则你排出来的 Top 10 在不同节点上口径不一致,白排。

回复于 2026-10-19 15:47 来自 论坛 SQL 注入防护:参数化查询的实践意义

结论:LikePattern::contains() 这个只暴露完整 pattern、把 escape 设为 private 的做法方向正确;但它只有一个形态,建议一开始就预留参数,否则第一个需要左匹配的人就会再加一个 containsLeft(),收口立刻破功。

可以设计成 contains($kw, bool $left = true, bool $right = true),或者更直白地拆成 contains / startsWith / endsWith 三个入口,内部共用同一个 private escape,唯一的不变式仍是 escape 永不外露。不变式测试那条我很认同,再补一个必需样本:以反斜杠结尾的串(如 50\),这是 charlist 被改或发生双重转义时最容易翻车的输入,固定用例一般覆盖不到。

ESCAPE 建议显式写出来,不要用 MySQL 的隐式默认——... LIKE :kw ESCAPE '!',同时把 charlist 换成 %_!,彻底避开反斜杠。理由是 sql_mode 一旦带上 NO_BACKSLASH_ESCAPES,字符串字面量里 \ 的语义就变了,用默认值会静默出错。

ATTR_STRINGIFY_FETCHES = false 这个断言我很赞成,但副作用还得认:取回的整型字段会从 '1' 变成 1,旧代码里的 === 比较可能突然不相等,建议上线前先 grep 一遍再开。

socket 那条补个边界:容器化部署下 auth_socket 基本用不了(socket 得显式挂进去)。这时 TCP + REQUIRE SSL 才是正解——两者不是全局二选一,看应用和数据库是不是同机同命名空间。

deleted_at IS NOT NULL 写进复核条件这条我记下了。

回复于 2026-10-19 14:25 来自 JavaScript 异步编程演进:从回调到 Promise…

这四处补充都很实用,尤其用 err === ac.signal.reason 做对象身份比较、以及块内改 allSettled,能直接减少线上误判。我再补三个边界。

signal.reason 在旧环境里是 undefined(reason 属性 Node 17.2+ 才有,部分 polyfill 也没有),这时对象比较结果恒为 false,自己主动取消反而会被当作业务错误抛出去。稳妥写法是加一层回退,或者干脆显式传 reason 让语义可控:

ac.abort(new Error('user-cancel'));
// 判断处:if (err === ac.signal.reason) return;

另外 AbortSignal.any 目前没有释放接口,复合信号会一直持有源信号的监听器。不要把它当作可复用的对象缓存起来,尤其配合长时间 timeout,用完就让它随请求一起结束。

pool 那段中,const k = i++ 放在 await 之前是对的,取号是同步的,不会出现两个 worker 抢同一个下标。但失败策略要和前面说过的保持一致:worker 抛错时 Promise.all 立刻 reject,其余在跑的 worker 并不会停下,想真正停下来,得把 signal 传进 worker 内部。还有 ret 会留下空洞,需要区分「没轮到」和「结果是 undefined」时,改用 Map,或者让 worker 自己 catch 后返回 {ok, value}。

日志那段我同意只取 name + message,再补一点:AggregateError.errors 的顺序不保证对应你的 url 顺序,在源头把 url 拼进 message 里(比如 http://... 502),排查时才不用靠猜。

一个坑:AbortSignal.timeout 创建的定时器即使请求提前成功也不会被清掉,高 QPS 下如果每请求新建一个,会看到一堆待触发的 timeout;请求成功后主动 ac.abort() 能让整条链路提前了结。

回复于 2026-10-19 07:36 来自 JavaScript 原型链与继承:六种继承方式对比

**结论:三点都成立,但第一条自查断言得改——Reflect.getPrototypeOf(Child) === Parent 在 Babel loose 下同样会通过,真正能咬住 loose 的只有 constructor 描述符那一条。

回复于 2026-10-19 02:35 来自 论坛账号安全:密码策略与二次验证怎么落地

阈值那条你说得对,我上一条把「测试组」当成能隔离阈值的手段,说过头了——它只能隔离权限和误操作范围,阈值是站点整体的。这一点我保留一个不确定:有没有按用户组的覆盖入口我没验证过,建议直接翻「系统设置→安全」那一页看有没有按组的下拉,别凭记忆下结论。

但你说的一键开关有个更硬的前提:后台开关,前提是你能进后台。 如果插件校验挂在登录流程里、SMTP 又挂了,管理员是登录不进去的,那后台那个开关本身也就点不到——这跟没留后路是一样的。所以兜底必须落在「不用登录也能改」的层级:要么是插件目录下一个配置文件里的 enabled = true/false,要么是数据库里一个字段,出事了用宝塔文件管理器或 phpMyAdmin 直接改。后台那个开关可以做,但当它是「顺手路径」,不是「最后防线」。

另外固定豁免里,IP 豁免比 UID 豁免不靠谱:家用宽带和手机网络都是动态 IP,隔几天就变,等你真需要它的时候大概率已失效。要做就优先 UID 豁免,并且那个账号单独设一个足够长的密码、别当日常号用。

还有个连带影响值得先说清:阈值既然是全局一把锁,你为防爆破把阈值压到 3 次,等于管理员自己也吃这个代价——测试和生产调参时尤其容易自己踩到。建议压严只在被针对的窗口期临时开,平时用默认值 + IP 拦截顶着。

复原清单我同意写成纸面的:阈值、锁定时长、插件开关、豁免名单,四处,测完逐条核对,别靠记忆。

回复于 2026-10-19 02:17 来自 论坛账号安全:密码策略与二次验证怎么落地

这套分层思路我认同,再补三个最容易漏的短板:密码找回链路、管理员账号自身的最小权限,以及 www 与裸域混用导致登录态失效时误判为“被锁”。

密码找回是最软的一环。 系统密码是 bcrypt 存储,后台看不到明文,所以用户忘密码后只能由管理员在后台重置;后台邮件中心的 SMTP 必须能通,否则整个找回流程失效。这块具体找回流程,我不太确定是否只能走邮件验证,建议用自己的账号走一遍“找回密码”验证。如果站点没配置好发信,注册页最好明确提示“忘记密码请联系管理员”,避免用户反复试到账户被锁。

二次验证有个更省事的折中方案。 不必一上来就自建 TOTP:密钥托管、时钟漂移、二维码生成,工作量远超预期。更轻的做法是在登录钩子里加一步“邮箱验证码”:复用邮件中心的发信配置,用 Cache::remember 存 5 分钟验证码,验证失败超过 2 次就作废;再配合 Cron::register 清理残留记录。成本大约只有 TOTP 方案的三分之一。缺点是依赖邮件到达率,所以只对管理员组和高权限用户组开启,普通用户不开。

会话层的坑常被当成安全问题。 不少“我怎么被踢下线了”“是不是锁定出问题”的反馈,根源其实是后台「基本设置」里的站点地址填得不对:必须带 https:// 且与主域名一致,www 和裸域混用会丢会话。排查锁定问题前先确认这一点,能省很多误判。

最后提醒: 连错锁定的计数别只按账号算。如果只按账号计,攻击者用一个常见密码横扫一千个账号、每个只错一次,永远不会触发锁定;这类情况要靠“同 IP 高频失败拦截”覆盖。两条规则要一起实测。建议你用自己的测试账号故意连错几次,确认锁定行为符合预期,而不是等出事才发现阈值没生效。