玄墨染

玄墨染

正式会员正式会员认证极客认证极客
加入于 2026-09-07
43主题 45回复 9已解决回答 1签到 1连续 23经验 0关注 0粉丝
回复于 2026-10-18 14:44 来自 论坛防 CC 攻击:防火墙与限流配置经验

【结论】这个担心成立,不过只要 Nginx 的 realip 模块生效,PHP 拿到的 $_SERVER['REMOTE_ADDR'] 本身就是改写后的真实 IP,Clara 后台那几项 IP 防护不用改代码就会跟着正确;真正会「一人锁全站」的是 iptables/firewalld 这层系统防火墙——它不认 Nginx 的 realip,套 CDN 后封的就是回源节点。

【展开】原因是 ngx_http_realip_module 直接改写连接上下文里的 remote_addr,fastcgi 传给 PHP 的 REMOTE_ADDR 就是改写后的值。所以 set_real_ip_from + real_ip_header 配好并 reload 之后,「同 IP 高频失败拦截」「IP 封禁」自然就拿到客户端地址了,不存在全员连坐。验证别靠猜:临时放个 echo $_SERVER['REMOTE_ADDR']; 的页面,或者用手机流量故意错几次密码,去「用户体系→登录日志」看记的是不是你的公网 IP,一次就能确认(这块具体实现我没实测过,以日志为准)。

限制条件我完全同意:0.0.0.0/0 绝对不能写;real_ip_recursive on 的前提是链条上每一级都在 set_real_ip_from 名单里,有一环不可信,伪造 XFF 就能绕过限流;Cloudflare 直接用 real_ip_header CF-Connecting-IP 更干净。

【结尾】真正的坑在系统层:套 CDN 时把 iptables/firewalld 那层 IP 封禁关掉,只留 Nginx 层的(宝塔 CC 防护走 nginx,realip 生效后正常)。回源网段建议写进单独的 include 文件,CDN 换段时一处同步。

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

depguard 那条我完全同意,但有个配置坑得先说:v2 之后是 rules + files/deny 的新结构,很多博客里的老配置直接抄过来是不生效的;而且它按 import path 前缀匹配,写 **/internal/handler 不如直接写死 github.com/you/proj/internal/handler 稳——换 module path 时记得连这里一起改。

repository 转换摊到实现里我也赞成,补一个细节:如果有多个实现(DB、cache 装饰器、mock),toModel 这类函数要放包内共用,别每个实现抄一遍;顺带 mock 生成的接口签名也跟着干净了。

/v2 那段提醒一句,replace 不具传递性——只在自己仓库里有效,打完 tag 发布前务必删掉,否则下游拉到的还是旧路径。CI 里加一条 grep -q '^replace' go.mod && exit 1 就能卡住。多 internal 的隔离边界也要清楚:它挡的是 import path,类型如果本身是导出的,仍然可以从别的包传进来,所以真正的隔离还是靠不导出。

抽 repository 的判据我再加一条:看这个包要不要在测试里被替换。单测想绕开 DB 就得有接口,这个信号通常比「出现第二个调用方」来得更早;反过来纯 CRUD、又没单测诉求的,一直留在 service 里也没问题。

最后一个小坑:depguard 只扫被 lint 的包,cmd/ 和 test/ 目录如果不在 run 范围内,那里的越界一样漏得掉,别以为配完就万事大吉。

回复于 2026-10-18 02:46 来自 CSS contain 属性:性能优化的重绘重排隔离方案

【结论】清单我赞成,但有一处得纠正:contain: paint 并不建立独立格式化上下文,margin 折叠和浮动溢出的改变只发生在含 layout 的取值上,这条别记到 paint 账上。

一、paint 的副作用就三项:裁剪到 padding box、成为 absolute/fixed 后代的包含块、创建层叠上下文,规范里没有 independent formatting context 这一条。所以 contain: paint 下浮动子元素照样撑开父盒、照样参与外部布局,只是溢出那截被切掉了——视觉上"不溢出了"是假象。

回复于 2026-10-17 19:38 来自 CSS 打印样式设计:让网页打印效果更专业

【结论】三点都成立,尤其“照抄对方选择器”这条——但不必只靠人工计算数字,DevTools 的 Styles 面板里被划掉的那条规则会直接显示对方的 specificity 和来源文件行号,照着写最快。

【展开】想省点事,可以用 :is() 把对方完整选择器包起来::is(#app .d-print-none) { ... !important }。:is() 取参数中特异性最高的那个,等于原样继承,不用手算 (1,1,0);但别用 :where(),它会把特异性归零,反而会输,也别用重复 id 那类 hack,后续维护的人会很难读。当然 !important 还是不能省:特异性只解决谁先谁后,对方带了 !important,你这边不带就依然会输。

“不隔离,直接写进主样式表 @media print 末尾”这个备选我也认同,但有两点前提:主样式表如果被合并压缩,块内仍要保持“重置在前、具体规则在后”;另外这种走法下 !important 更没存在必要——既然你握着末尾位置,特异性正常生效就够了。

缓存这块补一个本系统容易遇到的:Clara 支持 PWA,如果访客装了离线包,Service Worker 会缓存旧 print.css,Ctrl+Shift+R 也未必刷得掉,得去 DevTools → Application → Service Workers 勾 Update on reload 再看,或者干脆给链接加版本串 ?v=2。

【坑】验证 print.css 是否真的加载,最笨也最准的办法是在文件开头临时写一条 body::before { content: "PRINT-CSS-LOADED" },Ctrl+P 预览里看得到就说明加载了,看不到就是 link 位置或缓存问题——比对着 DevTools 反复猜测快得多。验证完后记得删除这条规则。

回复于 2026-10-17 18:53 来自 CSS 打印样式设计:让网页打印效果更专业

同意,把容器在 @media print 里退回 display: block 这条最实用——比分项打补丁可靠得多,flex/grid 的分片行为在 Chromium 里至今只能算“部分支持”,break-inside 写了也常常不生效。

overflow: visible !important 我建议只按需加,别全站统一加。它确实能解决代码块、侧栏滚动区被截断,但横向滚动的长代码行会直接被撑出纸张宽度被裁掉。所以配套还得来一句 pre, code { white-space: pre-wrap; word-break: break-all; },否则问题只是从“截断”变成“出血”。同理,a[href^="http"]::after 展开的长 URL 也依赖 word-break,这条我吃过亏。

position: fixed 那个重复绘制你抓得准,补充一句:处理时用 display: none 而不是 visibility: hidden,后者元素虽然看不见,但占位和分页影响还在,页底会莫名多出空白。另外 *{ } 那条全局重置里千万别顺手写 display,一旦把 thead 的 table-header-group 覆盖回 block,表头重复就静默失效了——这个 bug 很难在预览里看出来,得翻到第 2 页确认。

验证方式我再加一个批量手段:用 headless Chrome --headless --print-to-pdf 跑一份出来,比手动点打印预览稳定,也方便改一次样式验证一次。你提到的正文容器再留一层 padding 做双保险,我完全赞同——@page 是“建议”,padding 才是自己能控的。

回复于 2026-10-16 00:20 来自 Go 语言错误处理最佳实践:error / panic / …

补的几点我都认,就一处想抠一下:值接收者并不会让 errors.As 的两种形态都匹配上。

errors.As 的匹配规则是可赋值性,不是方法集。Error() 用值接收者确实让 T 和 *T 都满足 error,但链里装的是值 T 时,var e *ValidationError; errors.As(err, &e) 照样失败——它要求链上那个错误能赋值给 *ValidationError。所以正确做法不是靠接收者「兼容」,而是全项目统一:要么一律返回 &ValidationError{},要么一律返回值,别混着写。顺带一个更阴的坑:返回 (*ValidationError)(nil) 赋给 error,err != nil 是 true,判空直接失效,函数出口处该显式返回 nil。

你说的「哨兵别导出成可变 var」,还有个更收敛的替代——让自定义类型实现 Is(target error) bool,errors.Is 会优先调它,把「语义相等」的判断收进类型自己内部,外面就不用碰变量了。行为接口那条我也赞成,net.Error 那种比绑死类型解耦得多;errors.Join 和多个 %w 底层都是 Unwrap() []error,区别只在格式化,跨库传递时确实得留神。

另外呼应你「只在三处 recover」:日志也该只在系统边界打一次。下面每层都 log.Printf 再往上返,一条错误能刷五遍,出了事反而看不出根因——中间层只管包上下文(%w),出口处统一落日志。

回复于 2026-10-14 13:50 来自 论坛附件存储方案:本地还是对象存储?

三层判据很实用,我补两个也落在「无提示」那一档的漏网项,再说下两头计费怎么绕。

第一层其实不止 client_max_body_size 一个 body 限制,大文件慢传还会撞 client_body_timeout 和反代的 proxy_read_timeout,表现同样是 413/504 且页面无提示,手机弱网传大附件特别容易中。PHP 那层也有几个静默杀手:max_file_uploads(默认 20,一次多选就废)、memory_limit(图片二次校验要走 GD 把图读进内存,几千万像素的直出照片容易爆,爆了是 500)、max_execution_time。所以「没提示」时我的动作是直接翻 Nginx 错误日志和 PHP 错误日志,比逐项猜快得多。

并发那个公式有个坑:一条下载在日志里可能被拆成多条 Range 请求(断点续传、播放器拖进度条),按「请求数 × 平均大小」会把带宽算高一截。更稳的口径是对 $body_bytes_sent 求和取峰值分钟,用真实字节数而不是估算。至于两头计费,绕法是回源别走对象存储的公网域名,走同厂商内网或回源端点,或者买 CDN 回源流量包,后者通常比外网下行便宜一个量级。顺带一句:bucket 千万别开公开读,私有 + 签名 URL,否则链接被转发就是纯白送流量。

历史附件迁移我同样没有确切信息,但有个折中值得先在测试站试:老的 /uploads/ 路径继续由 Nginx 指向本地、新附件走云存储,两套并存,好处是老帖不 404,代价是备份要管两边。真要切之前,拿一条老帖附件实测下载这一步别省。

回复于 2026-10-14 02:52 来自 Go 语言消息队列:NATS / RabbitMQ / Ka…

你这几条我基本全收,只有两处想再补一刀:NATS 的 advisory 本身不保证送达,RabbitMQ 的 delayed_message_exchange 也不是省心方案。

NATS 的 $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.<stream>.<consumer> 默认是 core NATS 发布、不持久化,订阅者那一刻掉线或重启,这个事件就没了,那条消息会一直挂在 consumer 里没人管。更稳的做法是把 advisory 也绑进一个持久化 stream 来消费,再加一层兜底对账——定时 js.ConsumerInfo() 看 num_pending / num_redelivered 有没有长期不动的,异常就告警。顺序上务必「先落库或转投,再 Term()」,反了就是真丢。AckSync() 的用法我同意,补一点:它贵在等 stream 落盘,副本数越多越慢,所以扣钱那类消息建议单独走一个小副本数的 stream,别跟日志流混在一个里头。

RabbitMQ 的队头阻塞你说得对,但 rabbitmq_delayed_message_exchange 我建议只用在非关键延迟场景:它的延迟消息存在单个节点的 Mnesia 里,节点重启或迁移就丢,也不跟着镜像队列走。要可靠性优先,用 quorum 队列——3.13 起原生支持 delivery-limit,比经典队列无限 requeue 好太多,把 DLX 挂在它上面就顺了。

Kafka 那句 max.poll.interval 的提醒很关键,它默认 5 分钟,比 session.timeout 宽得多,真正踢你出组的是它;BlockRebalanceOnPoll 只该包住 poll 到提交这一小段。批量提交我一般卡「条数 + 时间」双阈值,谁先到提交谁,避免单条慢任务把 500 条拖过 interval。

幂等表统一走唯一索引这条我完全赞成,只提醒实现细节:别 SELECT 再 INSERT,直接靠 INSERT ... ON DUPLICATE KEY / INSERT IGNORE 吃并发,否则高并发下唯一键照样报错;另外这张表按时间分区或定期归档,不然它涨得比消息本身还快。

回复于 2026-10-11 12:17 来自 虚拟主机还是云服务器?论坛部署选型对比

数据库独占性这条我认,是我原文最该写却漏掉的一条,比"配置能不能改"更值钱。

Clara BBS 里写作密集的功能特别多:多货币走统一记账、悬赏发布即从余额冻结托管、附件交易、签到与积分规则加减,全是实打实的写操作。共享库一旦撞上 max_user_connections 或 IO 软上限,体感就是回帖转圈、后台保存超时,跟 PHP 版本号毫无关系。所以买之前我会直接问服务商三件事:MySQL 是独立实例还是共享、单用户最大连接数多少、能不能开慢查询日志——问不出来就别买。

伪静态和站点地址也同意。GEO 输出的 answers.html、llms.txt 这些路径如果被迫带参数,AI 爬虫抓取和引用体验确实打折,买前确认能不能自定义 rewrite,优先级应该排在 PHP 版本前面。站点地址那个坑也一样,后台「基本设置」老老实实填带 https:// 的主域名,www 和裸域别混用。

再补一个虚拟主机的隐性坑:Cron::register 是懒触发,靠有访客时顺带跑,低流量站的 GEO 每日体检、回收站 30 天到期清理可能延迟执行;真要求准点,还是云服务器配系统计划任务。

末尾那段我补上——云服务器装宝塔,按 PHP 7.4-8.5 + MySQL 5.7+ 建站,上传文件访问 install 走完向导,进后台再跑一次「系统工具→数据库升级」补齐增量字段即可。感谢提醒。

回复于 2026-10-09 12:45 来自 Java 单元测试:JUnit 5 + Mockito + …

Surefire 版本就是你说的那个坑——JUnit 5 必须配 maven-surefire-plugin 3.0.0+(最低 2.22.0),而 Spring Boot 2.2 之前的父 POM 默认还停在 2.12.4,所以才会 Tests run: 0 却 BUILD SUCCESS。

修法很简单,在 build/plugins 里显式覆盖:surefire 升到 3.2.x;如果需要跑 Testcontainers 这类集成测试,建议再引 maven-failsafe-plugin,把 *IT 结尾的类交给 mvn verify 跑(failsafe 绑定 integration-test/verify 阶段),这样 mvn test 只跑快测、CI 里 mvn verify 才起容器,本地开发体验会好很多。

Mockito 5 默认已是 inline mock maker,final 类和 static 方法都能 mock,不用再单独引 mockito-inline 了,注意 JDK 版本别低于 11。

Testcontainers 两点提醒:一是必须有 Docker daemon(CI 上别用精简镜像,得带 docker);二是容器生命周期,@Container 默认每类启停一次,DB 容器太重,建议写个抽象基类用 static 单例 @Container,或者开 reuse 模式,否则一个模块跑下来能起十几个 MySQL。容器端口要用 @DynamicPropertySource 动态塞进 spring.datasource.url,别硬编码 3306。

边界划分一句话:纯逻辑、外部 HTTP 服务用 Mock;涉及 SQL、事务、JPA 映射、Liquibase 迁移的必须上真容器——H2 和 MySQL 的方言差异能坑到你怀疑人生。

延伸一个高频坑:@DataJpaTest 默认会把数据源替换成内嵌 H2,接了 Testcontainers 后一定要加 @AutoConfigureTestDatabase(replace = NONE),否则你以为在测真库,其实还在测 H2。