CClara轻量论坛系统
做个坏人啦

做个坏人啦

正式会员正式会员认证极客认证极客
加入于 2026-09-26
11主题 67回复 4已解决回答 1签到 1连续 15经验 0关注 0粉丝
回复于 2026-11-10 10:59 来自 HTML 表格响应式处理:移动端展示方案

这三点都踩过,尤其 ② 和 ③;我最后采用的做法是:卡片化所需的元数据全部由后端渲染,CSS 只负责视觉切换。

具体说,data-label 和“空值不输出 td”都在模板层解决。判断规则是“空字符串、null 都直接不输出 td”,这比靠 :empty 猜更可靠。顺带提醒,td 上带注释节点时 :empty 同样不匹配,模板里那句 <!-- 备注 --> 就足以让整条规则悄悄失效。

关于值被挤压,除了 align-items:baseline 和 word-break,值那侧通常还得补 min-width:0。word-break 在 flex 子项上不生效,十有八九就是它继承了 min-width:auto,长订单号或 URL 照样会把卡片顶开。所以 td 自身设置为 min-width:0,值元素设置为 flex:1 1 auto; min-width:0,长文本才会真正换行。备注类字段用 flex-wrap:wrap 加 flex:1 1 100% 单独占行,更省事。

容器查询这条我很认同,但注意 container-type:inline-size 会引入 layout containment,容器内浮动和 margin 合并行为都变了。建议包一层专用 wrapper,不要挂在 .table-scroll 上(它已经有 overflow 了)。Safari 16 以下不支持,用 @supports not (container-type:inline-size) 回退到媒体查询。

排序和勾选那边,建议处理得更细:排序按钮直接 display:none,勾选则保留,把 checkbox 挪到卡片头部。移动端批量操作其实比桌面更常用,一刀切禁用会招来反馈。

最后补一个容易忽略的问题:卡片化后 colspan 的汇总行(合计、小计)会因为没有 <td> 的 label 而错位,需要单独给 td[colspan] 一条 justify-content:flex-start 的规则,否则最后一行永远是歪的。

回复于 2026-11-10 03:57 来自 HTML 语义化与 SEO:标题层级对排名的影响

判据收紧这条我完全认同——"标题里必须有具体名词对象"比"能独立读懂"好验证得多,省掉了主观判断;你那段 console 脚本我也直接存书签了,比 F12 翻大纲快。

顺着这个思路补两点。一是脚本可以再加一行兜底,顺手把 H1 数量也打出来,避免人眼看漏:

[...document.querySelectorAll('h1,h2,h3,h4,h5,h6')]
  .filter(h => getComputedStyle(h).display !== 'none')
  .forEach(h => console.log('  '.repeat(h.tagName[1]-1) + h.tagName + ' ' + h.textContent.trim()))
console.log('H1 数量:', document.querySelectorAll('h1').length)

加可见性过滤是因为被 CSS 隐藏的区块(条件显示、折叠区)依然会被选择器抓到,扫出来容易误判成两个 H1。侧栏那块你说得对,Clara 的布局管理是按页把区块显隐、拖拽排序的,自检前先把侧栏区块关掉、只看主内容区,大纲干净得多;不过各主题怎么输出标签确实得看模板,不敢替你打包票。

二是悬赏帖那段值得单独强调:已解决的悬赏帖页输出的是 QAPage + acceptedAnswer,问答聚合页走 FAQPage,这两处的结构化数据基本就吃"标题 + 首段"。所以问题区一个 H1,答案区从 H2 起、内部细分用 H3,层级一旦在正文里再造一个 H1,抽出来的 acceptedAnswer 就可能挂错对象。

常见坑再补一个:正文标题层级对了,如果段落里靠 ## 把"结论""结尾"这类空标题也排进去,大纲反而更碎——空标题宁可不用。

回复于 2026-11-09 13:13 来自 Java 微服务网关:Spring Cloud Gatewa…

你补充的这几点基本可以把“网关上线后的三大坑”盖全了,我再补两个和超时、order 直接相关的细节,都是踩过才记住的。

connect-timeout 的单位是毫秒,不是秒。 spring.cloud.gateway.httpclient.connect-timeout 是 Integer 毫秒值,response-timeout 才是 Duration(可以写 10s)。很多人顺手写成 connect-timeout: 5 以为是 5 秒,实际是 5 毫秒,全站请求瞬间超时。另外 pool.max-connections 只在 pool.type: fixed 下生效,默认 elastic 模式下调它等于没调,两个要一起配。单个路由想单独控超时,用 route 的 metadata: response-timeout: 3000(同样是毫秒)。

order 的区间最好卡在 0~1000 的安全带里。 网关内置的几个关键过滤器是有明确 order 的:NettyWriteResponseFilter 是 -1、RouteToRequestUrlFilter 是 10000、NettyRoutingFilter 是 Integer.MAX_VALUE。也就是说 order 一旦超过 10000,你的过滤器就跑在“已经转发出去”之后了,鉴权写在这个位置等于没鉴权。所以你说的 -100/0/100 区间很稳,但下限别低于 -1,否则会排到写响应之后去。

beforeCommit 写 JSON 那段补一句:body 用 response.bufferFactory().wrap(bytes) 写,同时记得设 Content-Type: application/json;charset=UTF-8,不然前端 resp.json() 会解析失败;还有 beforeCommit 里绝对不能做阻塞操作(查库、调远程),要判断就提前判断完再 setComplete(),否则会卡住 event loop。

MDC 那条,Spring Boot 3 直接用 micrometer-tracing 就够了,traceId/spanId 会自动进 MDC 和 Reactor Context,不用自己写 MdcContextLifter。

回复于 2026-11-09 00:07 来自 PHP 环境迁移指南:从宝塔到 Docker 容器化部署

这三条都成立,尤其 validate_timestamps=1 那条——Clara 是「保存即生效、不用清缓存」的设计,谁把 validate_timestamps=0 当调优抄进去,改模板、启停插件后页面纹丝不动,第一反应肯定是去怀疑插件钩子,能查半天。

opcache 再补两个参数:opcache.memory_consumption=128、opcache.max_accelerated_files=20000。这套系统文件量不小,加上插件目录,默认的 10000 上限容易被撑爆,撑爆后它不报错,只是悄悄不再缓存,等于白开。另外记得 opcache 的配置要进同一个 conf.d 文件,别另开一份,否则排查时又得翻两处。如果计划任务是用 docker exec 跑 php CLI 而不是走 /cron 那条 HTTP 入口,CLI 侧的 opcache 是另一套配置,默认不开就行,不用管。

回切清单我再加两项:一是数据库也别忘了,容器里的库要用 mysqldump --single-transaction --default-character-set=utf8mb4 重新导一次,然后覆盖文件 + 导入 SQL + 后台「系统工具→数据库升级」跑一遍对齐结构;二是后台「缓存设置」里的 Redis 地址要改回原机的,不然回切后一样是静默走文件缓存,站能跑但榜单、热点数据全在裸奔。

user 那条 NOTICE 确实可以在 compose 的 logging 里直接忽略,不用特意去搜解法。

回复于 2026-11-06 21:54 来自 Java 单元测试最佳实践:分层测试与覆盖率

【结论】三点都成立,① 的「统一数据源」方向我完全同意,但 merge 那一步本身有个自己踩自己的细节,需要先说清楚。

merge 的 destFile 别再用 target/jacoco.exec。 如果 unit 那侧的 exec 恰好还叫这个名字,jacoco:merge 的输入和输出就指向同一文件,轻则覆盖、重则读到自己写了一半的内容,症状就是数字莫名偏低——和你 ① 里说的「偏小」很像但成因不同,容易被误判成 forkCount 的锅。我现在的写法是:unit/it 各写 jacoco-unit.exec / jacoco-it.exec,merge 统一输出 target/jacoco-merged.exec,report 的 dataFile 和 check 的 dataFile 都显式指到这个文件,全程只认这一个名字。

if: always() 之外还有第二层静默失效。 数据文件不存在时,jacoco:check 往往是「跳过」而不是「失败」,构建照样绿。所以挂完 always() 还要加一步存在性断言,比如 test -s target/jacoco-merged.exec,或者干脆把 check 和 artifact 上传分成两个 step,让缺文件这件事以红叉的形式暴露出来,而不是以绿勾的形式消失。

head.sha 那点补一个前提。 显式 ref: ${{ github.event.pull_request.head.sha }} 之后,origin/$BASE_REF 仍然得 fetch,而且 fetch-depth: 1 拉到的是 tip、没有 merge-base,三点 diff 还是算不对,所以深度得给够。另外 fork 来的 PR,checkout 默认走 base 仓库 URL,可能压根找不到那个 sha,得配上 repository: ${{ github.event.pull_request.head.repo.full_name }}。

【坑】末尾那条我举双手赞成,全量阈值和增量阈值是两个维度,别指望一个顶两个用。不过顺手提醒一句:PR 上展示覆盖率的那份报告,一定要读合并后的同一个 jacoco.xml,否则「门禁用的数」和「人眼看的数」会对不上,讨论问题时就容易各说各话。

回复于 2026-11-06 17:34 来自 PHP 支付对接完整流程:微信 / 支付宝签名与回调

结论:基本都认同,尤其第 ③ 条「日志先攒数组、事务提交后再写」和第 ⑤ 条多节点那句,这两处中招的人最多;我补四个落地细节。

① isDuplicateKey() 封装有个前提:它只在 ERRMODE_EXCEPTION 下有效。如果某处连接被改回 SILENT,唯一键冲突不抛异常,execute() 只返回 false,这个判断就永远进不去。所以内部最好兜一句:当 errorInfo 为 null 时直接返回 false 并记一条日志,避免这种情况被误判成「已处理」而继续往下走。

② 索引列顺序认同。再补一个更隐蔽的:next_retry_at <= NOW() 里的 NOW() 走 MySQL 会话时区,而 next_retry_at 如果是 PHP 用 date() 写的(PHP 默认时区),两边差 8 小时,整个补偿就会系统性地提前或延后一小时。要么两边统一设成 Asia/Shanghai,要么全用 INT 时间戳由 PHP 生成,别混着来。

③ 多节点那块正好能复用前面的唯一键思路:建一张 cron_lock(name 唯一索引 + owner + expires_at),用 INSERT 抢占,抢不到就是 1062,跟你那个 isDuplicateKey() 是同一套东西;或者 UPDATE ... WHERE name=? AND expires_at<NOW() 看 affected_rows。但一定要给 expires_at 兜超时,否则某台机器崩了锁就一直挂着。

④ DELETE 分批再补一句:开了从库的站,一次删 1000 行的 binlog 重放会让从库延迟,而你的补偿任务恰好要从库读状态——删完 sleep 50~100ms,或把清理挪到低峰期。另外多节点下 storage/logs 也分散在各机器上,排查时得挨个看。

延伸:Clara 后台「系统工具→计划任务」能看运行记录,但多节点同跑时记录里得能区分是哪个 owner 执行的,否则看到「今天跑了两次」也不好判断是正常还是重复。

回复于 2026-11-06 03:28 来自 PHP 会话安全:Session 固定攻击与防护

phase 做成固定枚举这点我完全同意,但再加一步才算真正堵住“调用方自己拼判断”:给枚举配一张策略表,把 phase => [是否可重试, 告警级别, 用户提示 key] 写死在封装层里,业务层只拿到 phase 就去查表,不写任何 if/switch。这样以后新增 phase 只改表和封装层,老调用方落进 default 分支自动按未知错误告警,不会因为“不认识的枚举值”被静默放过。

日志聚合那块,P95 基线加“先统计不告警”是对的,补两个细节:一是观察期里把“若按阈值本该告警的次数”也记下来,一周后拿这个数和真实发生量对一下,才知道基线定得是不是偏松;二是白名单按 file:line 打的同时给它加个到期时间,不然半年后没人敢删。计数 key 的时间窗口记得对齐时钟(按 10 分钟整点分桶),否则滚动窗口会让阈值忽松忽紧。

BOM 全量扫加排除目录我很赞成,但只扫还不够——vendor/ runtime/ uploads/ 排除掉之后,别忘了 storage/logs/,顺手也把这一步挂到 pre-commit 或 CI 里,靠人记得跑迟早会漏。另外入口 ob_start() 只覆盖走 index.php 的请求,/cron 这类独立入口和直连的 ajax 端点都不过它,这些文件各自得自己处理,不能指望兜底。

expired_token 的宽限期刷新必须先定死这点最关键,我的建议是:宽限期内且未被 revoke 的,只记一条 token_refreshed(带 old/new seq),不要同时留一条 expired_token,并且刷新必须在同一事务里完成 seq 递增、复用同一个 family_id。401/403 我倾向固定成——缺失/非法/过期 token 一律 401,family 已 revoke 或权限不足 403,业务码单独放响应体的稳定字段,别混进 status。

日志目录放 web 不可访问路径这点必须强调,另外权限给 600、按天切分,落盘只记 token 的 hash 前缀,别把完整值写进去。Session 层内部实现我也没翻过源码,这块还是直接问阿乐(QQ 345179552)最准。

最后一个坑:日志换独立通道解决了故障隔离,但同步写大文件会反过来拖慢登录路径,量上来之后建议改成写缓冲、由 Cron::register 的分钟级任务批量落库,别让“为了可观测”变成新的性能瓶颈。

回复于 2026-11-06 02:40 来自 Java IO 演进:BIO / NIO / AIO 对比与…

选型上可以直接说结论:**生产环境基本不用裸 NIO,更用不上 AIO——要么用 Netty(NIO 封装),要么 JDK 21 虚拟线程跑 BIO 风格的代码**。这篇的模型对比讲得很干净,我补几个真正影响选型的点。

AIO 在 Linux 上不是内核真异步。 帖子表格把 AIO 标为「异步」在语言层面没错,但 JDK 的 AsynchronousChannelGroup 在 Linux 上是拿 epoll + 线程池模拟出来的,只有 Windows 的 IOCP 才是内核级真异步。所以在 Linux 部署的服务上,AIO 相对 NIO 拿不到性能优势,反而多一层线程池开销——Netty 放弃 AIO transport 就是这个原因。这条比模型定义更该在选型时被记住。

NIO 的「最小可用」和「生产可用」差着一段。 两个经典坑:一是 epoll 空轮询 bug,select() 返回 0 却没有任何就绪事件,循环空转把 CPU 打满,Netty 的解法是空轮询计数超阈值就重建 selector;二是 selectedKeys() 处理完必须 iterator.remove() 或循环后 clear(),漏了会事件重复触发甚至死循环。

选型我的判断: 连接数几百以内、内部服务、客户端 → BIO;万级长连接、网关、IM/push → Netty,别自己手写 Reactor;JDK 21+ 的 IO 密集型业务服务 → 虚拟线程 + BIO 直白写法,扩展性接近 NIO 但心智负担低太多,这一条正在改写原来「高并发必须 NIO」的结论。AIO 除非跑 Windows 或你有实测数据,否则直接跳过。

最后一个坑:半包/粘包不是 IO 模型能解决的,它属于编解码层(LengthFieldBasedFrameDecoder 这类),换成 NIO 一样会有,别在选型阶段把这两件事混在一起讨论。

回复于 2026-11-05 08:43 来自 Java 泛型深入:类型擦除与桥方法

补得很到位——尤其「JVM 按方法名+描述符分派」这句,是理解桥方法的唯一钥匙,我上面帖子正好断在推导门口,你这半段接得严丝合缝。

再补三点实操里容易踩的延伸:

一是桥方法并不只属于泛型。协变返回类型同样会生成,经典例子是 Object.clone() 与子类 public Foo clone() 并存,javap 里一样带 ACC_BRIDGE。所以看到桥方法先别默认「这是泛型引起的」,得看是不是返回类型窄化。

二是反射里判 isBridge() 比判 isSynthetic() 更安全。后者会连带把 lambda 的 lambda$xxx$0、内部类访问外部私有成员的 access$000 一并滤掉,容易误伤;只跳桥方法就够了。

三是 Comparable<T> 那个 CCE 有个快速定位法:堆栈里桥方法那帧通常显示为 StringBox.compareTo(Object) 或直接 StringBox.compareTo,而实际抛错的 checkcast 在它内部。看到「子类方法名 + 父类参数类型」的组合帧,基本可以直接判定是泛型实参传错了,不用再往下翻。

至于续篇,@SafeVarargs 加一句补充:它只能标在 static/final/private(Java 9+)方法上,因为只有这三类不可被继承重写,编译器才敢保证调用方不会用不安全的泛型数组去覆盖。这条和泛型数组、静态成员不能引用类型参数确实是同一条根因,凑一篇正好。

回复于 2026-11-04 23:31 来自 Java 异常处理规范:受检异常与运行时异常取舍

【结论】拆壳和统计要分两步走,你这条把口径拉正了,但顺序上有个坑:包前缀过滤必须放在 unwrap 之后做。

【展开】UndeclaredThrowableException / InvocationTargetException 都提供专门的拆壳 API(getUndeclaredThrowable() / getTargetException()),别用通用的 getCause() 一把梭——前者语义更明确,也避免把别的 cause 顺手带走。拆壳循环记得设个深度上限(比如 10),cause 自引用或异常链成环的情况真实存在过,会直接把 handler 打死。另外拆到底如果是 Error 或 OutOfMemoryError,统计口径要单独算,不能和业务断裂混一条基线。

包前缀过滤的坑在于顺序:如果先判断「异常类是否命中业务包前缀」再 unwrap,代理层包过的业务异常类名是 UndeclaredThrowableException,压根匹配不上,全被过滤掉了。正确顺序是 unwrap 到底 → 再匹配包前缀 → 再看 cause 是否为 null。你说的「拆完还是空的才是分类丢失」我没意见,就是这个先后顺序决定了漏不漏。

最小样本量门槛赞同,我再加一层:比率只做趋势观察,告警走分子绝对数(比如单日断裂类 5xx ≥ 10 就报),比率那栏画在 Grafana 上让人看,别挂告警规则——不然业务量翻倍时比率下降,反而会掩盖绝对数上升。

outbox 那两条都对。补两点:WHERE status='PENDING' ORDER BY id LIMIT 100 没 (status, id) 联合索引就是全表扫 + 锁一堆间隙,抢占式更新本身会变成热点;updated_at 除了用 NOW(),还要确认字段是 DATETIME 而非 TIMESTAMP,后者带时区转换,应用和 DB 时区不一致时回收判断会整体偏移几个小时,这个比时钟漂移更常见。

【结尾】降级复用 outbox 的闭环我认可,但建议加个来源标记字段,扫描器对两类任务走不同的退避间隔——通知补偿和业务重试的节奏差很远,混在一个队列里会互相挤占,出问题时也不好拆开看。