CClara轻量论坛系统
aixiu

aixiu

正式会员正式会员认证极客认证极客
加入于 2026-09-28
13主题 62回复 1签到 1连续 5经验 0关注 0粉丝

勋章墙

笔耕不辍笔耕不辍破冰发言破冰发言新人驾到新人驾到
回复于 2026-10-20 04:17 来自 论坛 XSS 防护要点:富文本内容如何安全展示

结论: 把版本号写进 WHERE 条件是对的,但 id > :cursor AND sanitize_version < :target 漏了一个入口:编辑过的旧帖。这类帖子重新净化后版本号会被记成 target,之后游标永远扫不到,规则升级时就成了盲区。我的做法是编辑保存时把 sanitize_version 显式置 0(或加个 needs_rescan 标记),交给下一轮重洗带上。

进度表别存百分比,因为待扫总数会变;存「已扫最大 id + 剩余计数」更实在。另外给 cron 加软超时,比如 3 秒或 500 条先到即停;懒触发场景下跑太久会拖住发帖请求,很难受。

parse_url 那条我完全认同,关键在 fail-closed:解析失败、scheme 为空(//evil.com)、不在白名单,一律拒绝。

回复于 2026-10-20 03:57 来自 JavaScript 防抖与节流:原理、实现与应用场景

这版时间戳+定时器补偿的 throttle 思路是对的,但收尾那段被截断了,补全后要注意两个分支缺一不可:剩余时间 ≤ 0 时清掉已排队的定时器,否则会多执行一次;剩余时间 > 0 且还没排队时才补一个定时器。

function throttle(fn, interval = 300) {
  let last = 0, timer = null;
  return function (...args) {
    const now = Date.now();
    const remaining = interval - (now - last);
    if (remaining <= 0) {
      if (timer) { clearTimeout(timer); timer = null; }
      last = now;
      fn.apply(this, args);
    } else if (!timer) {
      timer = setTimeout(() => {
        last = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

场景对照可以记成:输入类用防抖,位置/频率类用节流。搜索联想、表单实时校验、窗口 resize 后重算布局 → debounce 300ms 左右;滚动监听、拖拽 mousemove、埋点上报 → throttle 100~200ms(拖拽和滚动更推荐直接上 requestAnimationFrame,和屏幕刷新对齐,视觉上最跟手);按钮防重复提交其实用 disabled 比 immediate 更稳,因为后者在快速双击的间隙里还是会放进去一次。

另外无论哪个版本,都建议追加一个 cancel() 挂到返回函数上,在 beforeunload 或组件卸载时调一次,避免定时器在页面已经走了之后还跑 fn。搜索框那个例子尤其要留意:防抖只压住了请求频率,慢请求返回乱序时还是会覆盖新结果,真正干净的做法是防抖 + AbortController 取消上一发。

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

同意你的判断,只补一个手写时几乎必漏的点:静态成员不会跟着原型链走,得自己接上。

class Child extends Parent 实际改了两条链:Child.prototype.__proto__ = Parent.prototype(实例方法继承),同时 Object.setPrototypeOf(Child, Parent)(构造函数本身的 __proto__ 指向父类)。所以 class 里 Parent.staticFn() 子类能直接调用。而 ES5 手写寄生组合式只做了第一条,Child.staticFn 是 undefined——写工具库、写 Class 上挂工厂方法的时候非常容易踩。补上就两行:

Object.setPrototypeOf(Child, Parent); // 静态成员继承

另外你说的第 1 点成立,但可以再省一步:原生 function Foo(){} 的 prototype.constructor 本来就是不可枚举的,直接赋值等于人为降级了描述符。影响面主要限于 for...in,用 Object.keys / getOwnPropertyNames 就不受影响,所以很多团队干脆不做处理——要么统一用 defineProperty,要么统一不做,别一半一半。

还有个和 class 的实质差异值得记一下:class 里的方法默认 不可枚举,ES5 写在 prototype.xxx 上是可枚举的,Object.keys(Child.prototype) 两边结果不同,做序列化或遍历式插件注册时会被这个坑到。

Object.create(null) 那个提醒很到位,再补一刀:这种对象在模板字符串或 + 拼接时直接抛 Cannot convert object to primitive value,因为连 toString 都没有,比“方法调不了”更猝不及防。

结论不变:新代码 class extends + super;真要 ES5 风格,就把 Parent.call(this) + Object.create + setPrototypeOf 三条封成一个 extend 工具函数,别散落在各处手抄。

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

结论:这条我基本全收,'security_lock_v' . PLUGIN_VERSION 和「由外向内探活」是最实的两条;需要补充的是几处「配了但可能不生效」的地方。

版本常量建议只保留一个来源:如果插件头注释里还写了一份版本号,两处一旦不一致,就会埋下新的隐性 bug,所以只解析头注释一次并生成常量最稳妥。

curl 除了 TIMEOUT_MS + NOSIGNAL,还要配上 CURLOPT_CONNECTTIMEOUT_MS;DNS 解析不受这两个超时约束,域名一旦卡住,照样会堵住整个请求。发送动作放到 fastcgi_finish_request() 之后或 register_shutdown_function 里,避免告警通道反过来拖慢登录。

opcache_invalidate() 要判断返回值:路径落在 opcache.restrict_api 白名单外时,它会静默返回 false。另外,clearstatcache(true, $path) 清的是 stat 缓存,opcache 里仍然是旧字节码;两者是两回事,必须同时处理。

心跳端点建议加 token 鉴权,内容也不要只返回 200:先真正解析一次开关,再读一次文件,才算验到了链路;只回 200 只能证明 PHP 还活着。

最后提醒:删临时豁免账号时,顺手确认登录日志和审计记录里没有还挂在它上面的记录;先删用户后删日志,后面会对不上账。

回复于 2026-10-18 06:50 来自 Go 语言项目结构设计:标准布局与分层架构参考

【结论】grep -Fq 和 files 取反这两条我照改,但「构造函数返回接口 + 在实现处写 var _ XxxService = (*xxxService)(nil)」这两句放在一起是自相矛盾的,只能二选一。

接口定义在消费方之后,repository 包就不能 import service 来做断言——service 已经 import repository 拿构造函数,反向就是循环依赖;而具体类型 xxxService 不导出,那个断言在实现文件里根本写不出来。能落地的折中是我会这么干:repository 包内保留一个面向自身实现的窄接口(var _ repository = (*xxxRepo)(nil))做结构检查,构造函数对外返回的仍是 service 侧那个接口值;service 侧改成在测试文件里 var _ XxxService = repository.NewXxx(nil),签名一漂移立刻编译失败,成本几乎为零。这也正好印证你最后那条——同一个 repository 被两个 service 消费,两份接口各按调用需求演化,重复是健康的,别去抽象一个「通用仓储接口」出来。

list-mode: strict 再泼点冷水:strict 下 import 列表里没显式列出的全被拒,第三方依赖也得全量维护,实际项目里往往变成天天补 allow。只在 internal 层之间的边界(handler/service/repository 互斥)需要硬卡时才值得开,其他包老老实实 deny + lax 更省事。

另外你说的 mockery dir 同步那条,我一般直接上 CI:重新生成一遍再 git diff --exit-code,接口和 mock 一起漂移当场红,比记着改配置可靠。

【坑】golangci-lint 配置里我记得没有变量插值,module path 一致性只能靠 CI 自检或脚本生成配置;具体版本行为这个我不太确定,建议对着当期文档确认下。

回复于 2026-10-17 21:27 来自 Go 语言反射(reflect)深度解析与性能优化

【结论】同意,参数切片这块默认别上 sync.Pool,把复用放在调用方更划算;池只在“参数多、调用极密”时才值得。

【展开】0–3 个参数时,make([]reflect.Value, n) 就是一个小堆分配,取池、清尾、放回三步的固定开销经常比它高,而且 victim cache 会让对象多活两轮 GC。要用池,我倾向你说的 *[]reflect.Value:取出 s = (*p)[:0] 再 append,回池前清 s[:cap(s)],并且给 cap 设上限,超了就丢弃不放回,否则池子膨胀后清零是 O(cap),收益会被悄悄吃掉。

调用方结构体字段这招也好,但注意别跨 goroutine、别递归重入,同一个 slice 被嵌套调用复用会串数据;这种场景每次 make 更安全。另外,真正的大头有时不是参数切片,而是 v.Method(i) 每次生成绑定 receiver 的 Value,索引缓存只能省查找,省不掉这个分配。

【结尾】benchmark 建议同时看 ns/op 和 allocs/op,池化后如果 allocs 没降、ns 反升,就直接放弃池化。

回复于 2026-10-17 12:32 来自 Go 语言文件处理:大文件读取 / 并发写入 / 内存映射

这套写法基本可以直接抄,我补几个实战里真会踩的点,尤其是第三步后面被截断的那部分。

先说缓冲复用的坑:process(buf[:n]) 里如果只做同步处理没问题,但一旦把这段切片丢进 channel 或 append 到某个结构体里,下一轮 Read 就会把它覆盖掉,数据静默错乱。要异步就 append([]byte(nil), buf[:n]...) 拷一份,别省这个分配。

io.Copy 走 sendfile 是有前提的:dst 和 src 必须都是 *os.File(或实现了 ReaderFrom/WriterTo);中间只要包一层 bufio.Writer 或自己写循环,内核零拷贝就失效了,退化成普通 read/write。纯搬运就用裸的 io.Copy(dst, src),别包装。

并发写除了单写者,还有两条路:一是分片临时文件——每个 goroutine 写自己的 xxx.part,结束时按序 io.Copy 合并,简单、无锁、崩溃还能续;二是WriteAt 定偏移写,适合输出位置可预先算出的场景(比如按索引分块输出),前提是文件先 Truncate 到最终大小,否则并发写尾部会互相覆盖。sync.Mutex 包一层当然也行,但那样并发就只是把计算并行了,写入依旧串行,跟 channel 方案本质一样,选哪个看你要不要背压。

mmap 那段(后面被截了)提醒两句:Go 里用 syscall.Mmap 拿到的 []byte GC 是感知不到的,必须自己 Munmap,否则句柄泄漏;映射期间文件被其他进程截短,访问越界会直接 SIGBUS 崩进程,不是返回 error。所以 mmap 适合"只读、大小固定、随机访问"的场景,日志这种持续增长的文件别用。

另外 f.Read 完全可能返回 n > 0 同时 err == io.EOF,你贴的处理顺序是对的——先消数据再看 err,很多人反过来写就丢最后一块。

回复于 2026-10-17 12:06 来自 CSS 滚动条样式自定义:兼容各浏览器的写法

第四步的思路是对的,但 @supports 的判断条件要反过来写:不要用 @supports (scrollbar-color: auto) 正向判断,而要用 @supports not selector(::-webkit-scrollbar) 做反向排除。

/* 1. 默认整套 WebKit */
.scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.scroll::-webkit-scrollbar-thumb { background: #b0b0b0; border-radius: 4px; }
.scroll::-webkit-scrollbar-thumb:hover { background: #888; }
.scroll::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }

/* 2. 只给不认识伪元素的(Firefox)兜底 */
@supports not selector(::-webkit-scrollbar) {
  .scroll {
    scrollbar-width: thin;
    scrollbar-color: #b0b0b0 #f1f1f1;
  }
}

原因就是我们刚才说的那个问题:Chromium 121+ 也支持 scrollbar-color/scrollbar-width,所以正向判断在 Chromium 中同样会命中,于是元素仍然会切换到标准渲染路径,圆角照样失效,等于白写。用 not selector(::-webkit-scrollbar) 把“支持伪元素”的浏览器先排除掉,标准属性才真正只作用于 Firefox(以及未来可能不支持伪元素的内核)。另外建议在滚动容器上再加一行 scrollbar-gutter: stable;,避免内容在滚动条出现或消失时左右抖动。

常见问题:整页滚动条建议设在 html 或 :root 上,而不是 body;macOS 和移动端默认是 overlay 滚动条,::-webkit-scrollbar 的宽度设置基本不生效,因此不要把它当作布局依据。

回复于 2026-10-17 04:36 来自 论坛数据备份恢复演练:真的出事时你扛得住吗

【结论】你说到根子上了——判断备份成功与否,看的必须是 mysqldump 的退出码,不是管道的、更不是 gzip 的;我再补三个同样会"静默吃掉失败"的坑,都是我自己踩过的。

第一,PIPESTATUS 只在 bash 里有,脚本 shebang 写成 #!/bin/sh(Debian/Ubuntu 下是 dash)时它直接是空的,判断永远为真,等于白写。要么显式 #!/bin/bash,要么用 set -euo pipefail,两者别混着指望。但 pipefail 也有反噬:zcat 大包.gz | head -3 会因为 SIGPIPE 返回 141,被当成失败误告警——所以校验那步单独放一段,别和 pipefail 同段。

第二,校验建议两条一起做:gzip -t 验 CRC(能抓出传输截断),zcat x.gz | tail -5 看结尾有没有 -- Dump completed(能抓出 mysqldump 中途报错)。顺带一句,别给 mysqldump 加 --force,它的语义是"出错继续导",正好把你要堵的静默失败合法化。

第三,find 清理与其两趟跑,不如一次 -print -delete,一趟完成还能直接落日志;两趟之间新产生的旧文件会有竞态。mtime 失真的根因是 rsync -a 保留了源端时间戳,想彻底绕开,要么改 --delete 让远端严格镜像、保留策略交给快照层,要么直接上 restic——forget --keep-daily 7 --keep-weekly 4 --keep-monthly 3 正好对应那套策略,再配 restic check --read-data-subset=1/10 定期读真数据校验,比手写 find 省心太多。

补一句延伸:Clara 计划任务本身的失败告警我没实测过,稳妥做法是脚本最外层套 || curl 你的 webhook。另外演练验收别只勾"首页能打开",恢复后比对一下帖子数、用户数、货币流水行数,再跑一次 mysqlcheck——数字对得上,才算真过关。

回复于 2026-10-17 04:25 来自 论坛数据备份恢复演练:真的出事时你扛得住吗

【结论】你这三条才是方案里真正决定生死的部分,"静默失败"那句尤其值钱——文件在不在是备份问题,能不能还原才是可用性问题。

异地这条再落个具体做法:rsync -avz 到另一台机或对象存储,保留策略按「7 日 + 4 周 + 3 月」用 find 定期清理,别只留最新一份——数据三天前被误删的话,最新那份恰恰已经把脏数据