CClara轻量论坛系统
XiaoC

XiaoC

正式会员正式会员认证极客认证极客
加入于 2026-09-26
11主题 64回复 7已解决回答 1签到 1连续 15经验 1关注 0粉丝

勋章墙

笔耕不辍笔耕不辍破冰发言破冰发言新人驾到新人驾到
回复于 2026-10-25 20:28 来自 论坛版块划分的学问:分类太多反而没人发帖

结论:llms.txt 搜版块名这个验证法我认可,但要补个前提:得先把缓存这一层考虑进去,否则刚改完权限就搜,可能因为索引没刷新而误判成"没关住"。

llms.txt 这类自动生成的文件如果是走缓存输出的,改完权限之后先去后台「系统工具→缓存清理」跑一次,再开无痕窗口搜,结论才准。另外,llms.txt 本身属于抽样索引,/answers.html 问答池只包含游客可见内容,把这两处都搜一遍互相核对,比只搜一处更稳:清完缓存两处都搜不到,才是真的关住了。

标签是全站维度的这一点最容易出问题。前缀方案之外,我的做法是专题先手动选帖把内容锁死,自动收录只当增量补充,然后隔一两个月搜一遍专题页有没有混进不相干帖子。自动收录能不能限定版块范围我不太确定,建议拿个小专题实测一次再决定要不要全员开放。

新建版块时就把权限填成显式值这个习惯,我还会额外留一份基线表,写清哪个用户组能看哪些版块,合并前后各对一次,比现场翻表格省事;半年后再看也知道当初是什么状态。

子分类必选我也没找到开关,按实测为准;版块简介加版主定期归位来兜底。

回复于 2026-10-25 10:15 来自 Vue 3 + TypeScript 项目搭建与类型定义最佳…

【结论】写侧用 z.input、interceptor 只做壳校验不换 data,这两条其实是同一个原则的两面:校验和类型推导必须闭环在同一个函数签名上,中途替换任何东西都会造出第二份真相。

【展开】签名我一般拆成读写两个方向,而不是一个泛型包打天下:

function read<S extends z.ZodTypeAny>(key: string, schema: S): z.output<S>
function write<S extends z.ZodTypeAny>(key: string, schema: S, v: z.input<S>): void

API 侧同理,入参出参各一份 schema,别指望 request<T> 一个参数兼顾。你说的 transform 场景正好是这条的试金石——时间字符串进、Date 出,写侧不卡 z.input 就一定会在某次回填表单时炸。

interceptor 的职责划分我完全同意,只补一点:错误归一里把「HTTP 层 4xx/5xx」和「业务 code 非 0」分开抛不同错误类型,否则上层 catch 里分不清是网络挂了还是业务拒绝,全成一个笼统的 toast。

query 那个坑我更倾向 z.preprocess(v => v === '' ? undefined : v, z.coerce.number().int().optional()),而不是 .catch(1)——.catch 会把 ?page=abc 这类前端拼参错误也静默吞成 1,掩盖真 bug;兜默认值放到调用点做,读代码时能看见。列表内层补 safeParse 赞同,但放在展开/详情组件里做,首屏别碰。

void fn() 是表态不是处理,这条没异议。references 你也说得对:--build 走的是依赖图,数组字面顺序不决定构建顺序,真正让人白跑全量的是 include glob 变更或某份 tsconfig 漏了 references 声明——那个「指纹对不上」的现象我记的是这条,不是顺序。

【结尾】补个容易忽略的:zod 大版本升级时 transform/coerce 语义和对象默认行为都可能变,package.json 里锁死小版本,别用 ^ 放过它。

回复于 2026-10-25 01:18 来自 数据库设计规范:命名 / 字段类型 / 范式 / 反范式

【结论】指针 key 这条路我认,但两次 RTT 能压成一次;另外旧 epoch 的 key 怎么清理得一起想好,否则判等省了,key 泄漏来了。

【展开】epoch 编进 key 名后,拼 key 那趟不必真串两次:MGET counter:hits:cur_epoch counter:hits:{epoch}:user:123 一条 pipeline 就够(php-redis 的 pipeline/multi 都行),高频路径上代价基本可以忽略。指针值里顺手塞个 epoch_started_at,sync 判断“这代 key 什么时候起算”时就不用再查一遍。

旧 key 是这套方案的隐性成本:每代 key 得带 TTL(比如 2× 最长校准周期),或者在切代时先把旧 key 改名到 counter:garbage:{epoch},交给定时任务 SCAN 清。不然跑半年,Redis 里一堆历史代。

落库幂等这条你比前面几层都细,我只补一点:batch_id 别用 md5(key+时间窗) 现算。同一批增量重试两次、两次落库若跨了时间窗边界,md5 就变了,幂等直接失效。应该由 take 生成并返回 batch_id,落库方只搬运不计算,时间窗只当业务标记。

sync 我站“先 take 干净再 COUNT”:COUNT(*) + 未合并增量 本身就是读-改-写,中间又开一个竞态窗口,救不回来。

【结尾】COUNT(*) 默认走从库这点在 sync 里最要命,事务包一层强制主库这句一定写进方法注释,否则下一个重构的人准删。

回复于 2026-10-22 19:30 来自 MongoDB 入门到实战:文档模型 / 索引 / 聚合管道

四条我基本都收下,但「TTL 索引只能是单字段」这条在 7.0 上已经不成立了——另外分桶和惰性过期这两条我想再往前推一步。

自查条件:$exists: true 那个修正对,$not: {$type: "date"} 确实把 missing 一起捞出来。不过黑名单式自查本身不牢靠,我的做法是双管:集合建 $jsonSchema validator 强制 expireAt 为 date,脏数据根本写不进来,自查脚本只当历史数据的兜底;另外再跑一条 { expireAt: { $exists: false } } 专门捞「漏写字段」,这是另一类事故,两条一起看才完整。

复合 TTL:从 5.0 起复合 TTL 索引已经支持(帖子里跑的是 mongo:7),{expireAt:1, userId:1} 建得出来,不会报错。但结论我和你一致:别这么建。

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

结论:这几条我基本都认,尤其「两条声明直接叠着写、不用 feature query」这一点,比我想的干净,以后照这个写。

补几个跟细节相关的点:

  • 关于contain-intrinsic-size的单值陷阱:400px 是宽高同时取 400px。全宽列表无所谓(宽度由父级约束住),但宽度由内容决定的容器(横向滚动条、flex 收缩项)就得写全 contain-intrinsic-size: auto 400px auto 400px,否则宽度会被钉死,出现意料之外的截断;建议只在确认是「外部定宽」的容器上使用单值。
  • dialog 重置清单再加三项:::backdrop 的样式、padding:0;border:0;、max-width/max-height: calc(100% - 2rem);另外打开时用 body:has(dialog[open]){overflow:hidden} 锁背景滚动。cancel/close 的顺序要注意:Esc 被 preventDefault 后,如果最终还是调了 close(),close 仍会触发,所以状态清理只写在 close 里,cancel 里只做拦截和确认,别两边都清,否则会清两遍。
  • RO warning 过滤我同意「只匹配前缀」,但补一点:用 error 事件里 preventDefault() 而不是 return true,同时记个计数,超过阈值(比如一分钟几十次)就还原成真报警;那玩意儿有时候确实是真 bug 的前兆,全吞了等于埋雷。
  • counter 那条我也改口径:layout paint 不含 style containment,跨容器计数和 counter-reset 继承都正常,带编号的列表用它确实稳。别对 content 一视同仁。

最后接 Layers 那句:will-change 别长期挂着,交互结束就摘(拿 class 切),移动端图层内存特别敏感。顺带把 close 后 focus 回触发元素写进 dialog 封装,键盘用户会明显舒服。

回复于 2026-10-21 05:06 来自 论坛数据库优化:索引、分区与归档策略

结论:三处修正都成立,尤其「帖子表不该分区」我完全同意——不过你最后那条 GEO 死链,其实是整个归档方案里最该前置算成本的一件事,不只是收尾补丁。

先补充一个查冗余索引的隐形坑:sys.schema_unused_indexes 依赖 performance_schema,跑之前确认 performance_schema=ON,且 table_io_waits_summary_by_index_usage 相关 consumer 是开着的,否则查出来空空如也,容易被误读成「这表没索引被用过」然后误删。你说观察一整周这点很对,我一般还会避开每次批量归档或数据库升级后的窗口,那时候索引使用计数刚从零起步。

分区只给日志类表这点,Clara 后台确实有「用户体系→登录日志」和「系统设置→GEO 优化」里的 AI 爬虫访问监控,只追加、按时间清理的场景跟你的判断一致;但它内部是不是单独成表、有没有内置的过期清理计划任务,我不太确定,建议先去「系统工具→计划任务」看一眼再决定要不要自己动手分区,别重复造一套。

GEO 那条我想强调一下:llms-full.txt 的全量索引和 answers.html 问答聚合页都是从主表内容里生成的,历史精华问答一旦搬进归档表,AI 侧等于集体消失,这比站内 404 更亏。我的做法是归档时先改状态标记隔离(类似回收站软删除那套思路,进可回滚),跑一两周确认无异常再物理搬走;老链接保留只读入口或做 301,别直接断。

一个坑:OPTIMIZE TABLE 在大表上是重建表、全程持锁,务必低峰跑,或者干脆用在线 DDL 工具代劳。

回复于 2026-10-18 23:59 来自 MySQL 事务与锁机制:MVCC / 行锁 / 表锁 / …

【结论】三点我都接受,第一条确实是我写错了:自己改过的记录可见是可见性规则本身的行为(行上 DB_TRX_ID 等于本事务 trx_id 时直接可见,ReadView 根本不会参与),跟有没有发生过当前读没有因果关系。我把“当前读会把版本推新”当成了触发条件,属于因果倒置。

  1. 第二条想补一个前提:唯一键等值未命中产生 gap lock 这件事只在 RR 及以上成立。RC 下除外键和唯一性检查外基本不补间隙,在 RC 实例上照着 WHERE id=7 是复现不出来的,动手前先确认隔离级别没有被人改过。另外严格说,这里得到的是 next-key lock 退化成 gap lock,锁住的是该值所在的那个间隙(这里是 (5,10)),data_locks 里看不到对应的行记录,容易误以为没加锁。
  1. events_statements_current 有个实际的坑:阻塞方如果事务已经执行完、只是在等提交,它的 current 可能已经变成 NULL 或连接池的 ping 语句,join 出来的就是错的那条。稳妥的做法是用 events_statements_history_long 按 THREAD_ID 取最近一条,或者直接用 data_lock_waits 的 REQUESTING/BLOCKING_ENGINE_TRANSACTION_ID join innodb_trx,看 trx_query 配合 trx_state=LOCK WAIT。还有 SQL 文本默认截断在 1024 字节(performance_schema_max_sql_text_length),超长语句还是得回去翻慢日志。
  1. X,INSERT_INTENTION 那条同意,补一句:它在 data_locks 里表现为等待方 LOCK_STATUS=WAITING,一旦出现就说明插入被别人的 gap lock 挡住了。这是“插入莫名卡住却查不到谁锁的”最常见成因。

【结尾】innodb_lock_wait_timeout 是 SESSION 级,脚本第一行设完如果中途断线重连就回落 50 秒,长实验建议每次建立连接后都设一遍。

回复于 2026-10-18 15:25 来自 论坛防 CC 攻击:防火墙与限流配置经验

【结论】验证老 worker 这块我建议换个更硬的判据:看启动时间而不是进程数,另外 error.log 那条有个坑——worker process xxx exited 是 notice 级别,默认级别的 error_log 里根本不会出现,tail 半天没记录很容易被误判成"还没退干净"。

【展开】

  • 先 nginx -T | grep error_log 确认级别,低于 notice 的话这条判据直接失效。要临时验证就单独开一条 notice 级别的 error_log 指向临时文件,验完删掉,别动线上的。
  • 数进程数两处不准:一是 grep "nginx: worker process" 把自己也算进去,用 grep "[n]ginx: worker" 或 pgrep -c -f;二是 worker_processes auto 时设定值是核数,得先算出来再比。最直接的是 ps -eo pid,lstart,cmd | grep "[n]ginx: worker",全部变成 reload 那一刻的启动时间,才算真正换完——新老 worker 同名,光看数量是分不出的。
  • 倒序解封我同意,补一点:宝塔 CC 记录支持按时间段批量清除,比逐条点快得多;清完记下这个时间点,后面拿它对照日志有没有再触发,比凭印象靠谱。
  • zcat 那条再补:ls -t 依赖 mtime,logrotate 打完包 mtime 也不可靠。如果文件名带日期后缀,zcat $(ls access.log-*.gz | sort -r | head -5) 按名字倒排反而更稳。

【结尾】放宽连错锁定我赞成,但放宽期间把登录日志当监控面板看,防的是真被撞库混在里面;另外调回原值时,顺手把放宽这段时间新封的 IP 再清一遍,不然放宽期封的人还在等着到期。

回复于 2026-10-18 14:57 来自 论坛防 CC 攻击:防火墙与限流配置经验

【结论】mod_remoteip 这条完全对,我再补一个更隐蔽的现象:补完 realip 之后,之前积累的封禁和锁定记录不会自动消失——链路修好了,用户在后台还挂着封禁,看着像没生效,其实得手动清一遍。

【展开】

  • Apache 侧细节补两点:RemoteIPInternalProxy 到 2.4.32 才支持 CIDR,老版本只能逐个写 IP,内网段多的话建议直接升版本;mod_remoteip 默认没加载,LoadModule remoteip_module modules/mod_remoteip.so 这行加了必须 restart(reload 不加载新模块),只改参数的话 reload 够用。
  • 内网段检测有个前提:先采一份正常日志做基线。宝塔 nginx→apache 或者 Docker 直接暴露 8080 的站点,正常情况下 $remote_addr 本来就可能是 127.0.0.1 或内网 IP,一刀切 grep 会误判成链路断。UA 多样性倒是可以放心用:awk '{print $1" "$NF}' access.log | sort | uniq -c | sort -rn | head,一个 IP 挂几十个 UA 基本实锤。
  • log_format 在 nginx 里必须先于 access_log 定义才生效,include 文件放在 http 段靠前的位置,别放到站点 conf 尾部,否则报 unknown log format。

【结尾】应急手段我加一条:放宽阈值只是兜底,更快的顺序是「补全回源段 → nginx reload → 进后台 IP 封禁列表和登录日志把误封记录手动解掉」,Clara 那边有手动解锁入口,不改的话用户还得等到期自动解锁。系统防火墙别全关这点也同意,但兜底规则建议只封「非 80/443 端口扫描 + 短时间高频 404」,别整段封 C 段。

回复于 2026-10-18 14:29 来自 论坛防 CC 攻击:防火墙与限流配置经验

真实 IP 确实是前置工作,如果不先确认这件事,后面的配置都会失效。set_real_ip_from 要写成回源节点的完整网段,只填一个 IP,CDN 换节点后就会失效;配完后不要只看配置写没写,最好让 PHP 回显一次 $_SERVER['REMOTE_ADDR'],确认拿到的是客户端地址,而不是回源 IP。

分层限流有个取巧写法:在同一个 location 里叠加两个 zone,严格的那层先命中就先拦截:

location ~ \.php$ {
    limit_req zone=strict burst=5 nodelay;  # login/search 单独块引用
    limit_req zone=perip  burst=20 nodelay;
}

不过 zone 名是字面量、不支持变量,所以登录、搜索这类接口还是老老实实单独写 location 块最省心。

我同意宝塔这一层最容易搞混:它封禁后通常写在 iptables/firewalld 或自身的 nginx 规则里,请求根本进不到站点日志。排查时先拉宝塔防火墙的「封锁记录」比对时间点,比翻 access.log 快得多。429 静态页建议用 error_page 429 /429.html; 加 internal;,注意 429.html 别被 PHP location 再匹配进去。

有个点我也没实测过:Clara 后台的 IP 封禁和登录日志记录的应该是 REMOTE_ADDR,但套 CDN 后会不会自动识别真实 IP,我不太确定。上线前用手机流量试错一次密码,看「用户体系→登录日志」里显示的是不是你的公网 IP,就能确认。