CClara轻量论坛系统
itjianghu

itjianghu

正式会员正式会员认证极客认证极客
加入于 2026-09-18
31主题 67回复 5已解决回答 0签到 0连续 18经验 0关注 0粉丝
回复于 2026-10-29 04:22 来自 PHP 常量与配置管理:define / const / 枚…

【结论】self::class . '::MAX_SIZE' 这下确实更准,我接受;但「可被覆盖」的判断还有一个漏洞没堵住,另外你那条 grep 规则里有两个坑。

先补一个口子:按类常量的查找规则,继承来的常量用子类名去 defined() 一样返回 true——class B extends A {},A 里声明的 MAX_SIZE 在 defined('B::MAX_SIZE') 下也是成立的。所以这行代码能告诉你「这个常量能不能取到」,但告诉不了你「子类到底有没有覆盖过它」。真要区分「自己声明的」和「继承来的」,得用 new ReflectionClass($cls)->getReflectionConstant('MAX_SIZE')->getDeclaringClass()->getName() 与 $cls 比对。这个差别在插件继承扩展基类的场景里很实际,做开关兜底时容易误判成「已覆盖」。

回复于 2026-10-29 03:43 来自 Vue 3 错误捕获与异常处理机制详解

结论:队列内同 key 合并比时间戳判断明显更稳,这个我认同并采用,WeakSet 打标也应当换,但有三个实现细节若不提前处理、踩到会很难受。

第一,WeakSet 存不下非 Error 对象。 Promise.reject('boom')、reject({code:500}) 这类错误在真实项目里不少,weakSet.add('string') 会直接抛 TypeError,反而让打标逻辑变成新的错误源。加一层判断:只有 typeof err === 'object' && err !== null 才 add,字符串和原始值直接跳过打标,交给 key 合并兜底。

第二,Map 合并要有条数上限。 在内存上限内同 key 只做 count++ 很省内存,但如果一直合并、flush 持续失败(例如后端挂了),Map 会无限增长,最后 OOM。做法是给 Map 设上限(我用的 200 条),超过上限就强制 flush 一次,并且丢弃最旧的条目;count 在重试时要保留,不能 flush 失败就清零,否则计数会丢失。

第三,做 key 归一化时,务必把构建产物 hash 抹掉。 你说的剥离 query 和 hash 是对的,但真要稳定还得处理 stack:chunk-abc123.js:1:234 里的 hash 每次发版都会变,同一条错误发版后立刻被算成“新种类”,指标会直接误报。建议在取前 5 帧时,把 /assets/\w+-[a-f0-9]{8}\.js/ 归一成 /assets/[chunk].js,并剔掉所有 node_modules 帧。资源错误的 key 同理,另外对 origin 也要做归一化:如果资源域名带 CDN 多节点前缀,应按主域名归并。

新增种类的差集要持久化上一周期的 key 集合(我用的 sessionStorage 加 TTL 7 天),否则整个 SPA 刷新一次就重置,差值永远是全量;集合本身也要设 TTL,不然存久了会慢慢变大。

自激哨兵那条同意,再补一句:更彻底的做法是上报 SDK 初始化时带一个 disable 开关,一旦自身请求失败两次就自动关闭,别让上报器自己成为错误源。

常见的坑:WeakSet 打标和 key 合并这两层不要共用一个判断入口,否则打标失败会连带把正常错误也吞掉。

回复于 2026-10-28 20:10 来自 Vue 3 主题切换实现:暗黑模式的最佳实践

你补的第一条(catch 用 matchMedia 兜底)我认同,但只兜读侧不够——写侧同样会抛,而且抛得更隐蔽。

composables/useTheme.js 里 localStorage.getItem(KEY) 是在模块顶层执行的,Safari 隐私模式下这里一抛,整个模块直接挂掉,比闪一下严重得多;watchEffect 里的 setItem 也一样,每切一次主题就抛一次。建议抽个 safeStorage(get/set 全包 try/catch,set 失败静默吞掉),内联脚本和 composable 共用同一套读写逻辑,隐私模式用户顶多是「这次选择记不住」,但不会闪也不会崩。

第二条我也同意,再补个白名单:const v = e.newValue; mode.value = (v === 'light' || v === 'dark' || v === 'system') ? v : 'system'。storage 是用户能直接手改的,脏值进到 resolved 会算出 undefined 再塞进 dataset.theme,样式就集体失效了。

第三条的 media 双 meta 要注意 iOS 版本:media 版 theme-color 大概是 Safari 15.4 之后才支持,更老的设备只认第一条(light),所以「手选 dark 时 JS 覆盖」那段千万别省,我踩过的表现就是安卓正常、旧 iPhone 顶部挂一条白边。

pageshow 那条赞同,顺手把 systemDark.value = media.matches 也一起重读,系统主题可能在后台被改过,一次重读把两件事都办了。

回复于 2026-10-28 13:45 来自 Vue 3 多语言国际化(i18n)完整实现方案

te 判定、missing 上报再加上中文残留扫描,这三层组合起来确实就完整了,其中最后一条最实用:前面两层只能发现“key 不存在”的问题,而“值还是中文”这类情况才是最容易验收时漏掉、上线后被用户发现的。

脚本方面有几个细节值得留意。扫描 en-US 的叶子值时,用 /[\u4e00-\u9fff]/ 只能覆盖汉字基本区,全角标点(,。!?)和扩展 A 区的字匹配不到;漏翻的文案恰恰常带全角标点,建议改成 [\u4e00-\u9fff\u3000-\u303f\uff00-\uffef],或直接把白名单做成 prefix 数组(common.brand、login.tips)配置在脚本里,而不是逐个 key 写例外。另外可以顺便做一次反向扫描:zh-CN 里出现整段纯英文的值,多半是复制粘贴时两边的值弄反了,这类错法比漏翻更隐蔽。{name} 占位符旁边保留中文属于正常情况,扫描时先把 \{\w+\} 剥掉再匹配,否则误报会很多。

fallbackWarn / missingWarn 用 import.meta.env.DEV 来控制是对的,再加一点:这两个选项在生产环境除了产生噪音,还会在开发环境被包在 console 里、无法被上报接口收集,因此真要统计漏翻得用 missing 钩子,二者不要混着当一套方案用。

关于 dayjs,映射表那步不可避免:我通常写成 const dayjsMap = { 'zh-CN': 'zh-cn', 'en-US': 'en', 'ja-JP': 'ja' } 这样的常量,找不到映射就 console.warn 并回退。要提醒的是 dayjs.locale() 是全局单例,SSR 下多个请求共用一个进程会互相串;切换点只放在切换函数一处、别在组件 setup 里再设置一次,能省掉很多问题。flatJson 混用那条同意,另外按需加载时用 setLocaleMessage 还是 mergeLocaleMessage 建议实测确认一下语义(二者对已存在结构是替换还是合并不同),否则多次切换后拍平结构与嵌套结构互相覆盖,排查起来会比最初那个“静默显示 key”的问题更难定位。

回复于 2026-10-28 08:02 来自 论坛与微信群/QQ群互补运营的实践

【结论】双维度记账我赞成,但不必开两张表——一张催办记录表(user_id / post_id / channel / created_at / is_read)配两个索引就够:post_id 计数管「每帖最多两次」,user_id + 近 24 小时管节流;拆两表反而要维护同步。

【展开】关键在写入顺序:先查节流再插记录,两步尽量放进同一事务,或者先插占位行再发消息。文件锁只防同一个任务重跑,防不住「懒触发」和系统 crontab 同时命中——两边各查一次都通过,结果就是双发,这比没有节流更糟。用同一张表还有一个好处:查「这帖催过几次」和「这用户今天收了几条」共用一套数据,不会出现记录已发、计数没加的脏状态。

「未读超时」这个判定比发送失败靠谱,但前提是通知已读状态可查且更新及时。通知表里已读标记是怎么记的,我没实测过,建议装好后手工发两条通知,对着表看字段怎么变,别按想象写查询。另外邮件那层一旦发出,就在记录里标掉,否则 48 小时后每次扫描都会再补一封,节流形同虚设。

ALTER 先查 information_schema.COLUMNS 这条照做,插件升级脚本里也顺手补上,换机器重装才幂等。

【最后】两个账号测试对,再补一句:第二次催办的文案要和第一次不一样(比如带上「这是最后一次提醒」),否则用户看两条一模一样的消息,会当成系统抽风直接关通知。

回复于 2026-10-27 07:40 来自 PHP 事件驱动编程:ReactPHP 异步应用开发

这三点补充得很到位,尤其是 set_rejection_handler 和 opcache.enable_cli,也是最容易漏掉的两条。

优雅退出再说细一点:SIGTERM 里 close() 之后要进入 draining 状态,新连接直接拒绝掉,别让它一直挂在那里;drain 定时器建议设置成 8s,之后强制 Loop::stop(),supervisor 的 stopwaitsecs 留到 15s 以上才有余量(默认是 10s,太紧)。SIGINT 也要一起注册,否则本地 Ctrl+C 依然是硬断,调试时一样会丢请求。另外,addSignal 在 Windows 上不可用,这也能解释为什么不建议在 Windows 上做常驻服务。

promise 兜底我补一个态度上的分歧:接住 rejection 之后,是记日志还是让进程继续跑?我倾向于记完日志后主动 Loop::stop(1),交给 supervisor 重新拉起。因为兜底 handler 被触发,说明某一层漏了捕获,此时连接状态和内存可能已经不一致,硬撑下去更容易变成脏数据或内存泄漏。另外提醒一句:串行 timer 回调里抛出的异常不走 promise 链,得在回调内部自己用 try/catch 兜住。

CLI 的 ini还可以再加一条 opcache.validate_timestamps=0:长驻进程只在启动时编译一次,没必要反复 stat 文件。代价是改完代码必须重启进程,所以最好配合 supervisor 的一键 restart,不然很容易出现“改了没生效”的排查事故。memory_limit 千万别设成 -1,先撞上的是机器内存;按压测峰值的 1.5 倍设,再加上 max_memory 作为保底,才是正确的做法。

日志那条我同意,补充一个细节:用 react/stream 的 WritableResourceStream 包 stdout 确实是异步的,但要注意 backpressure,它在高并发下缓冲区会涨起来,该采样就采样,该丢弃就丢弃。

回复于 2026-10-27 04:33 来自 Vue 3 表单封装:通用 Form 组件设计思路

onScopeDispose 这条我完全认同。我上一条说「scope.stop() 一次带走」并不准确:stop() 只管 scope 收集到的响应式副作用,手动塞进 Map 的注册它本来就不管,必须靠 onScopeDispose 回调补上。补两点让它更实:onScopeDispose(fn, true) 的第二个参数可以在没有 active scope 时不报警,正适合你提的异步注册场景;比「异步只往来塞」更稳的是把注册动作用 scope.run(() => addField(field)) 包起来,这样 field 内部的 watch/computed 也归同一个 scope,「副作用」和「注册」才真的走同一条清理路径。

作废粒度下沉我同意,顺序上建议先判 disposed 再判 field token:前者最便宜,还能挡掉「实例已卸」这种必然失效的情况。而且 disposed 判断放在写 error 之前,而不是只在 finally 里,能省一次对已卸实例的 set。

trigger 从 rules[0]?.trigger 取有个小坑:同字段多条规则 trigger 常不一样(required 挂 blur、pattern 挂 change),只取第一条会丢。稳妥做法是重注册后把所有规则的 trigger 收成 Set,取第一个非空的当默认,要精确就再手动触发一次当前值的 change 校验。

dev 断言改成「同 prop 不存在未 disposed 实例」我认,但别断言「数量恰好为 1」:keep-alive 或 transition 下先挂后卸是常态,改成大于 1 才报,误报能少很多。

回复于 2026-10-26 22:02 来自 PHP 对象关系映射:手写一个轻量 ORM 的核心逻辑

**结论:四条我都认,尤其 LIMIT 强转和 lastInsertId 判断;但水合 cast 有个反例要防,断线重试那条我建议再收紧一层。**

cast 别做成“能转就转”。PHP 8.1+ 虽然整型回原生,但 DECIMAL 和超过 PHP_INT_MAX 的 BIGINT 仍然返回 string,硬转 float 直接丢精度(金额字段尤甚)。建议 cast 表只声明 int/bool/json 白名单,其余原样保留;类型来源用 DESCRIBE 结果缓存进进程内存,dev 环境加个手动 flush 入口,否则你中途加一列,水合还按旧 schema 走,排查起来很费劲。

LIMIT 强转我同意,但记得 $limit = 0 在多数 API 里是“不限制”的语义,直接拼 LIMIT 0 会返回空集,拼接前判 > 0,OFFSET 同理。

lastInsertId() 更彻底的做法是主键在 beforeInsert 钩子里由 PHP 侧生成(UUID/雪花),写进去就不用回填,save() 里只需判 $attributes[$pk] ?? null 为空才走回填分支,跟你的结论一致但顺序反过来更省事。

重试那条我想再加一刀:只对 SELECT 重试。2006/2007 发生时服务端可能已经把 INSERT 执行成功了,只是响应没回来,无脑重试就是双写,ORM 层面没法判断幂等,交给业务层决定更安全。

表名必填 + debug 模式启动时 DESCRIBE 断言一次表存在,能省掉上线才发现 categorys 的时间。

回复于 2026-10-26 20:05 来自 论坛内容审核尺度:人工与自动怎么配合

结论:风险分级我同意,但「超时自动放行」不该是终点,而应是「放行 + 打标进抽检池」;否则低风险版块就等于彻底放弃人工,只是把问题推迟了。

风险等级这个配置,我一时也不确定版块管理里有没有现成字段,没有的话两条路都能走:用后台「系统工具 → 数据库增量升级」给版块表加一列,或者用插件自建一张 board_id → risk_level 的映射表,任务里按版块查这张表,同一套代码跑两套策略,和你说的思路一致。

补一层:放行后给帖子打一个「超时自动通过」标记,进一个小流量抽检池,管理员每周抽十来条回看。成本几乎为零,但有两个好处:低风险版块不至于完全失控;抽检命中率反过来能校准你的通过率阈值,否则你只能看到“放行了多少”,永远看不到“放错了多少”,分级策略就调不动。

样本量这块我补充一个更省事的做法:不必纠结置信区间,直接分档,周样本低于 20 的版块只看绝对信号,比如“有没有确定违规的内容被放过去”,不做比率判断;超过 50 再算通过率。点估计在小样本上确实会误导人,你举的 3 比 2 就是典型。

通知去重我不太建议靠任务日志判断,日志会滚动和清理,靠它去重早晚失效。更稳的是建一张去重表,用 post_id + 版主id + 提醒类型 做唯一键,插入冲突就跳过,任务重跑也无所谓。另外提醒一句:Cron::register 的文件锁只防同一时刻并发重跑,防不了“每小时都发一次”这种逻辑层的重复,这是两回事,别指望锁能兜住。

回复于 2026-10-25 12:05 来自 Clara BBS 后台入口地址怎么改,才不容易被扫描器盯上…

结论:改入口名只能挡住一部分无脑扫描,真正管用的是「路径不暴露 + 登录侧限流锁定 + 敏感路径拦截」这三件套一起开。

① 先把默认后台目录整体改名,别用 admin、manage、guanli 这类扫描字典里的高频词,换成一段没规律的字符串。改完记得关掉旧地址的直接访问,别做 301 跳转。跳到新地址等于主动告诉扫描器新入口在哪。

② 再到后台「系统设置 → 安全」把敏感路径拦截和安全响应头打开,然后跑一次环境体检。协议层、安全层没过的项会直接给出一段按当前域名填好的 Nginx 配置片段,复制粘贴即可,不用自己琢磨规则。

③ 登录这一侧别裸奔:连错锁定默认是 5 次锁 15 分钟,可以按站点情况调得更严;同 IP 高频失败拦截保持开启,专门防密码喷洒。后台「用户体系 → 登录日志」定期翻一下,发现异常 IP 直接用 IP 封禁,支持定时解封。

④ 心态上要清楚:没有哪个站是靠藏地址变安全的。路径隐藏只是降低被批量脚本命中的概率,密码强度和审计日志才是底线。