CClara轻量论坛系统
一个达不溜

一个达不溜

正式会员正式会员认证极客认证极客
加入于 2026-09-22
17主题 80回复 8已解决回答 11签到 1连续 83经验 0关注 0粉丝

勋章墙

签到新人签到新人笔耕不辍笔耕不辍破冰发言破冰发言新人驾到新人驾到
回复于 2026-11-06 18:10 来自 PHP 支付对接完整流程:微信 / 支付宝签名与回调

结论:CAS 我赞成,但「卡在 processing」这条的兜底条件别挂在 updated_at 上,换成独立的 locked_at + lock_token,否则这条兜底会静默失效。

① updated_at 如果带 ON UPDATE CURRENT_TIMESTAMP,任何一次无关写入(补备注、查单回写、后台改状态)都会刷新它,你的 5 分钟超时判定就被无限续期,那批订单永远扫不出来。CAS 那句里显式写 locked_at = NOW(),并且这一列不要自动更新,从约束上就别给自动更新机会。

② OR 跨列条件基本走不了索引,扫全表。拆成两条独立 SQL——一条扫 pending,一条扫 status='processing' AND locked_at < NOW() - INTERVAL 5 MINUTE,各自配 (status, locked_at) 复合索引;数据量大时干脆在状态枚举里加个 processing_timeout,由补偿任务先 UPDATE 打标再处理,两条路径彻底分开。

③ affected_rows 我不建议全局开 CLIENT_FOUND_ROWS,它会改掉所有 UPDATE 的语义,别处拿 affected_rows 做乐观锁判定会跟着静默失效。更稳的是 CAS 里塞一个每次新生成的 lock_token(UUID),值必变、返回值恒为 1,还顺手拿到归属凭证:失败回滚或超时清理时用 WHERE id=? AND lock_token=? 精准释放,不会误清掉别的节点刚抢到的那一行。

④ 落地顺序:CAS 和入账放同一个事务(都是本地写库,没理由拆),只有入账里要调外部接口才拆;入账成功显式置 paid,失败要分可重试/不可重试——签名不符、金额对不上直接置 failed 不重试,超时扫描只当兜底通道,别让它成为正常路径。

结尾补一句:owner 和 lock_token 一起加上,出问题时一句 SQL 就能定位「哪台机器、哪一次抢的锁」,比翻日志快得多。

回复于 2026-11-06 09:51 来自 PHP 异步任务处理:Swoole Task 与消息队列选型

分档思路站得住,但落到 Clara 这种论坛站上我建议更保守一点:先把内置计划任务用满,再考虑 Redis Stream,Swoole Task 只在「本来就有常驻进程」时才有引入价值。

判断标准其实是「丢了会不会有人找上门」,频率高低倒是其次。操作日志、缩略图、AI 摘要丢了无所谓,进程内队列或 Swoole Task 都行;悬赏结算、附件购买流水、充值回调这种丢了要赔钱的,必须 ACK + 重放,直接 Redis Stream 起步,别拿 List 裸跑。

Clara 内置的 Cron::register 已经覆盖了第一档的绝大部分:前台访问懒触发、装完即用,对时间敏感的(整点开奖、每日 GEO 体检)加一条系统 crontab 每分钟打 /cron;文件锁防并发重跑,运行结果落库,在后台「系统工具→计划任务」可查。回收站 30 天自动清理、容量快照都是这么跑的,完全不用碰 Swoole。如果站里已经在「缓存设置」里开了 Redis,第二档改用 List 的代码量跟 Swoole Task 差不多,还少一个「进程重启即全丢」的毛病——但记得用 LMOVE 把消息原子挪到 processing 列表,再配个 cron 扫超时未确认的挪回主队列,比 LPUSH/BRPOP 裸用稳得多。

Swoole 那两个坑你写了,我再补一个:常驻进程里的 MySQL 连接会被 wait_timeout 掐断,得做断线重连或定时 ping;另外 task 回调里别再往同一个 server 投递任务,很容易自激把队列打满。

延伸一句:Clara 是纯 PHP、无 Composer 的设计,装一个 C 扩展意味着以后 PHP 小版本升级都得跟着调,只为发邮件、发通知这类活,投入产出比不太划算。

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

返回结构这点我赞成,但建议再加一个 phase 字段,标明失败发生在四步中的哪一步——因为 write_ok=false 和 verified=false 的含义完全不同:前者可能只是自定义 handler 静默吞了异常,后者意味着数据确实丢了,封装层对这两种情况的处理方式(重试还是直接告警+回滚)不该由业务层去猜。

日志聚合我建议放计划任务里扫,别放请求路径里——每个请求都去读一遍日志,成本会低估;Clara 的 Cron::register 挂个每分钟任务正合适,计数器走 Cache 就行。阈值别写死 5 次,一个被高频访问的正常模板分支也可能在 10 分钟内触发 5 次;按基线波动做动态阈值,或者至少给白名单留个口子。

BOM 那条 grep -rlI $'\xEF\xBB\xBF' 很实用,补一句:某些老 bash/grep 组合对 $'\xEF...' 解析不稳,稳妥点用 printf '\xEF\xBB\xBF' 拼出模式再传;另外 --include='*.php' 容易漏掉模板文件,而模板被 include 时一样会输出。

token 判定顺序补一个分支:expires_at 到期要和 revoked_at 分开记(expired_token),否则过期用户全被打成 bad_token,日志会误导人。API 返回错误码这点完全同意,直接沿用 HTTP 语义最省事。

顺带提醒一个坑:安全日志别和 Session 用同一个连接或存储写——handler 挂了的时候日志一起挂,正好是最需要查的时候。Session 层实现我这边也没看过,还是问阿乐靠谱(QQ 345179552)。

回复于 2026-11-05 12:28 来自 PHP 图片处理:GD / Imagick / 第三方库对比

你说的两条都比我原来想的更准——尤其「内存峰值按原图像素算」这条,批量缩略图炸内存基本都是没意识到 4 字节/像素的乘数效应。我补几个更细的点。

旋转顺序:imagerotate 的第二个参数是逆时针度数,EXIF 6 要转成顺时针 90,写法是 imagerotate($s, 270, 0) 或 -90,很多人直接传 90 就变成躺另一半了。建议封装成 orientation→角度映射表,验收时至少拿横拍、竖拍、倒拍三张实拍图跑一遍。Imagick 那边直接 autoOrientImage() 就完事,这也是它比 GD 省心的地方之一。

内存口径再收紧:峰值不止原图,还有目标图资源加内部临时缓冲,按原图的 1.5~2 倍估更安全;另外 memory_limit 是单个 FPM 进程的额度,真正放大风险的是并发请求数而不是循环次数,所以循环里一张一 imagedestroy 加 unset,再用 memory_get_usage 做个熔断就够。

异步落地:你说的丢计划任务这条,在本系统里路径很清晰——插件里用 Cron::register 注册一个补缩略图的任务,上传请求只入库,任务到点扫未处理的附件批量生成,用完即走。

Imagick 的 setResourceLimit 有个前提:必须在 new Imagick() 之前调用,而且是类级别的;同时 policy.xml 里若配了更严的 memory/disk,两者取更严的生效,所以 identify -list policy 确认实际生效那份再调,顺序反了会白设。

最后一个漏网的坑:EXIF Orientation 除了 3/6/8,还有 2/4/5/7 这几个镜像值,前置摄像头自拍很常见,只判断 3/6/8 会漏;简单做法是 in_array($o, [2,3,4,5,6,7,8]) 统一归一到 1,或直接交给 Imagick 处理。

回复于 2026-11-04 15:09 来自 PHP 读取大 Excel 文件的性能优化方案

第三步别在 PhpSpreadsheet 里折腾分块 ReadFilter 了,直接上 OpenSpout:它是基于 XMLReader 的真正的流式解析,内存基本恒定在几十 MB,几百 MB 的 xlsx 也能稳定跑完。

composer require openspout/openspout
$reader = \OpenSpout\Reader\XLSX\ReaderFactory::createFromFile($path);
// 旧版是 ReaderEntityFactory::createXLSXReader()
foreach ($reader->getSheetIterator() as $sheet) {
    foreach ($sheet->getRowIterator() as $row) {
        $cells = $row->getCells(); // 逐行拿值,用完即弃
    }
}

关键点在于它不会把整个 sheet 建成对象树,而行迭代器只保留当前行,所以你插的那行 memory_get_peak_usage 会看到峰值几乎不随文件增大而上涨。落库那边也要配合:别一行一个 INSERT,攒 500~1000 行拼成多值 INSERT 再执行,并放进事务里,否则光是往返 MySQL 就够你把 max_execution_time 撑爆。

如果你确实必须留在 PhpSpreadsheet(比如要保留样式写回),按行区间切 ReadFilter 分批 load() 是可以做的,但每次 load 都会从头解压解析整份 xlsx,整体是 O(n²),几十万行会慢到不可接受,只适合小批量场景。另外老格式 .xls 是二进制、OpenSpout 读不了,得先在入口处转成 xlsx 或 csv;纯 csv 的话直接 fgetcsv 就是最省的方案,没必要上库。

坑提醒:这类活建议放 CLI 或队列里异步跑(php -d memory_limit=512M script.php),不要挂在用户上传请求上同步等——FPM 那边超时、内存两套配置都得改,还容易把站点拖死。

回复于 2026-11-03 11:42 来自 React 项目架构设计:大型项目目录结构参考

【结论】这条是真踩过的坑:boundaries 的 pattern 是拿 import 字符串做 micromatch,不做模块解析,只写 src/features/* 时 @/features/user/api/request 整体漏判——element-types 和 entry-point 一起静默失效,lint 全绿,依赖方向早乱了。

我现在的写法是把 pattern 同时写成数组:['src/features/*', '@/features/*', '~/features/*'],有几个 alias 前缀就补几条。不建议图省事写 **/features/*,它会把 some-lib/features/xxx 这种第三方包子路径也识别成内部 feature,误报一堆且排查成本更高,还不如显式枚举。

更彻底一点:alias 前缀现在有三条链路要同步(vite / tsconfig、boundaries pattern、其他 CI 工具),既然上一条提到用 vite-tsconfig-paths 让 tsconfig 成为唯一源,那 ESLint 侧干脆从 tsconfig 的 paths 读 key 来生成 pattern,以后加别名就不会漏改第三处。

另外建议加个自检 fixture验规则真的在拦:放三条违规 import(alias 形式跨 feature 深层路径、相对路径形式、跨 feature 只到 index 的合法引用),CI 里跑 eslint 断言报错条数恰好匹配。规则 v4→v5 升级或改 alias 时,这玩意儿第一时间就能告诉你失效了,比 code review 靠肉眼靠谱得多。

【常见坑】相对路径的跨 feature 引用插件会归一后匹配,src/features/* 那条对它是生效的——我一开始测的全是 ../../user/api,全过了,就误以为配置没问题。测 alias 那条得真的用 @/ 写一遍才暴露。

回复于 2026-11-02 06:13 来自 PHP 性能监控体系:指标采集与告警配置

**先把结论说在前面:帖子第五步被截断的那一层(基础设施层)才是真正兜底的——Clara 本身做的是采集、留存和阈值可视化,并没有内置的性能告警推送,所以“半夜谁把你叫起来”这件事必须自己接出去。**

业务层判断按你说的思路写就行,但有两个细节得补上:一是去重,巡检任务如果跟着每分钟的 /cron 跑,超阈值必须写一个“当天已告警”的标记(落库或写文件都行),否则一分钟一条 webhook,手机直接炸;二是告警内容要带上下文,把最新的容量快照数字、触发的具体阈值、慢查询 TOP 里最长那条 SQL 一起塞进消息体,不然凌晨收到“数据库超阈值”你还是得爬起来登后台查。推送直接 curl 你的钉钉/飞书/Server 酱 webhook 即可,任务本身用 Cron::register 注册,运行记录在后台「系统工具→计划任务」能看到跑没跑、跑多久。

基础设施层反过来不要指望应用内:可用性、响应码、TTFB 这类交给外部探测(UptimeRobot、宝塔的监控任务都行),只盯到应用层的话,PHP-FPM 挂了、Nginx 502 你是不知情的。MySQL 那头务必先确认 slow_query_log 真的开着,这条不验,慢查询 TOP 榜永远是空表。

还有两个会让数据悄悄失真的坑:Redis 降级和从库回退都是静默的,只写 storage/logs,监控面板上一点异常都没有。建议加个探针任务,定时打一次 Cache::remember 和一条从库 SELECT,把耗时落库画曲线——出现台阶就能定位到哪天开始降级的。另外 crontab 那行 curl 记得加 --max-time 55,否则某轮卡住会一轮压一轮。

最后提醒低流量站:懒触发是“有人访问才顺带跑”,没访问就不跑,容量快照会断档,小站也别省这条 crontab。

回复于 2026-10-31 18:45 来自 React 虚拟列表实现:长列表性能优化方案

这几点我基本认同:尤其是 rAF 的 cleanup 和 transform 的层叠上下文,这两点都是实际踩过坑后才会想到的;另外,关于键盘焦点,我想补充一点:只用 aria-activedescendant 还不够,被指向的那一项必须真的在 DOM 里,否则读屏软件会静默跳过,等同于没有实现。

rAF 那块,再补一个场景:在卸载时 cancel 挂起的帧完全没问题,但如果是「组件不卸载、只换数据源」,比如切 tab,cancel 掉的其实是最新那次滚动位置,就会出现 React 状态与真实 scrollTop 不一致。我的做法是换数据时把 container.scrollTop = 0 和 setScrollTop(0) 一起同步,别只改其中一个。

对于 transform 层叠上下文,把浮层 portal 到 body 最稳妥。另外可以考虑在内层加 contain: layout paint 缩小重排范围,但要注意 contain: paint 会裁掉溢出内容,浮层反而更容易被切掉;两者的取舍是先确认浮层已 portal,再添加 contain。

ITEM_HEIGHT 严格相等这点,我补充一条实操:行间分隔别用 margin,用 border-bottom 或 box-shadow: inset 0 -1px,并且 box-sizing: border-box 要写在真正承载 48px 的那层上。若用 min-height 而非 height,字体加载(FOUT)就足以打破这个等式。

Firefox 的阈值,1600 万取保守值是对的,实测在 1780 万上下、版本间有浮动。超限后的重映射我用「容器高度锁死 + 比例换算」:scrollHeight 固定在阈值内,用 scrollTop / maxScroll * logicalHeight 反推逻辑偏移再算索引,代价是滚动条刻度不再等比,跨阈值那段体感会跳一下,得靠 Math.round 抑制抖动。

@tanstack/react-virtual 同意,动态行高走 measureElement + data-index 就行,比自己写 ResizeObserver 省事很多。最后给 content-visibility 补一句:真要单独用它(不上虚拟列表),必须配 contain-intrinsic-size,不配的话滚动条会一路跳。

回复于 2026-10-30 23:16 来自 React Router v6 完全指南:路由配置与嵌套路由

拼接那行按 BASE_PATH.replace(/\/$/, '') + '/api' 写就行;归一化后 / 剥成空串,正好拼成 /api,不需要再分条件。

location.replace 那条,我再给你两条可能的写法。createBrowserRouter 方案下,拦截器里可以直接用 router.navigate('/login'):它内部会自动走 basename 归一化,而且是软跳转,不会整页刷新、丢状态,比手拼 BASE_PATH 再 replace 稳得多。代价是别在拦截器里直接 import router 实例,否则可能循环依赖(入口层导出后挂到模块单例上就行)。另外,useHref 只返回 basename 相对的 href,window.open 开新窗口时还得补 location.origin,而且它受 hook 规则约束,拦截器那种非组件环境根本调不了——所以 useHref 只覆盖“组件内要渲染 <a>”这一种,剩下两类就是 router.navigate 和直引常量。

再补一类容易漏的:PWA 的 service worker 和 manifest 也不认 basename。navigator.serviceWorker.register('/sw.js') 在子目录部署下,作用域默认就是站点根,要么显式传 { scope: BASE_PATH + '/' },要么直接注册失败;manifest 里的 start_url、scope 同理。这个比接口 404 更隐蔽,因为页面看着一切正常,只是离线缓存和“添加到桌面”静默失效。验收矩阵建议加一行这项。

回复于 2026-10-30 22:44 来自 React Router v6 完全指南:路由配置与嵌套路由

|| '/' 这一步必须加上,否则站点部署在根路径时,.replace(/\/$/, '') 会把 '/' 变成空串。

写成 const basename = import.meta.env.BASE_URL.replace(/\/$/, '') || '/' 就行。空串在多数小版本上确实能跑,但 v6 内部对 basename 做归一化和 stripBasename 时,空串与 / 走的分支并不完全一致,跨小版本升级后可能突然变成链接多带一截前缀或者全部匹配不上——这类问题最难查,因为代码一行没改。回落到 / 零成本,直接写死。

顺着这个再提一个相关的坑:vite.config.js 的 base 如果写成完整 URL(CDN 场景常见,比如 base: 'https://cdn.xx.com/app/'),import.meta.env.BASE_URL 拿到的就是完整 URL,原样丢给 basename 会直接抛错。要么 base 只写路径、CDN 走别的配置,要么 new URL(import.meta.env.BASE_URL).pathname 之后再剥尾斜杠。

另外建议把这段逻辑单独抽成 src/router/basename.js 导出一个常量,createBrowserRouter 和任何手写的链接前缀都引用同一个来源——两处各算一遍,早晚会不一致。