[{"data":1,"prerenderedAt":1962},["ShallowReactive",2],{"\u002F2026-04-28-helpplay-27":3,"\u002F2026-04-28-helpplay-27-rel":477},{"id":4,"title":5,"body":6,"column":461,"date":462,"description":12,"extension":463,"hero_image":464,"meta":465,"navigation":466,"path":467,"seo":468,"series_id":464,"severity":464,"stem":469,"summary":470,"tags":471,"__hash__":476},"posts\u002F2026-04-28-helpplay-27个模块设计文档两天产出.md","两天，27 个模块的设计文档",{"type":7,"value":8,"toc":446},"minimark",[9,13,21,25,28,80,83,86,93,98,101,139,142,146,149,182,185,206,209,219,232,238,249,253,256,283,286,290,293,299,302,305,308,312,319,322,325,328,391,394,397,403,421,427,433,436,443],[10,11,12],"p",{},"去年 4 月底，地陪服务平台脚手架期启动一周后，需要在两天内产出 27 个业务模块的设计文档。10 项细分服务、多角色分账模型（平台\u002F商家\u002F代理\u002F分销\u002F地陪）、完整后台，每个模块一个独立设计文档——如果逐个手写，进度撑不住。",[10,14,15,16,20],{},"这篇记录批量产出这类文档的方法论。核心思路是",[17,18,19],"strong",{},"先定统一的文档模板与字段规范，再定跨模块共享的核心模型，最后按模板填充各模块","。要点在于：共享模型必须先定死，因为改一次就要改所有相关文档。",[22,23,24],"h2",{"id":24},"先定文档模板骨架",[10,26,27],{},"27 个模块的设计文档虽然内容不同，但结构可以统一。我设定的骨架如下：",[29,30,31,38,44,50,56,62,68,74],"ul",{},[32,33,34,37],"li",{},[17,35,36],{},"1. Goal","：这个模块解决什么",[32,39,40,43],{},[17,41,42],{},"2. Recommended Approach","：为什么采用这个方案而不是其他（这很关键，很多团队会漏掉）",[32,45,46,49],{},[17,47,48],{},"3. Scope","：包含什么、不包含什么（防止后续蠕虫式功能膨胀）",[32,51,52,55],{},[17,53,54],{},"4. Information Architecture","：如果涉及后台，菜单信息架构怎么组织",[32,57,58,61],{},[17,59,60],{},"5. Page Design","：UI 分区、筛选项、表格列、统计卡片等",[32,63,64,67],{},[17,65,66],{},"6. Data Model","：数据库表结构、字段、枚举、索引",[32,69,70,73],{},[17,71,72],{},"7. API Design","：接口地址、参数、响应格式",[32,75,76,79],{},[17,77,78],{},"8. Testing Strategy","：前后端各自怎么测",[10,81,82],{},"这个骨架对所有 27 个模块都适用。没有骨架时，每个模块文档的结构都不一样，审查和理解成本翻倍；有骨架后，新增模块只需要按位置填空。",[22,84,85],{"id":85},"定死五个跨模块共享的核心模型",[10,87,88,89,92],{},"更关键的是",[17,90,91],{},"定死核心数据模型","，特别是账本和订单。一旦这些模型后改，27 个文档都要返工。",[94,95,97],"h3",{"id":96},"_1-订单模型","1. 订单模型",[10,99,100],{},"订单需要覆盖所有场景：用户下单、服务提供者接单、支付、取消、改期、超时计费、位置追踪、评价。这 10 项服务都公用一套订单模型，字段只增不减。",[10,102,103,104,108,109,108,112,115,116,108,119,122,123,108,126,108,129,108,132,108,135,138],{},"核心字段：",[105,106,107],"code",{},"orderId","、",[105,110,111],{},"userId",[105,113,114],{},"escortId","（服务提供者）、",[105,117,118],{},"serviceId",[105,120,121],{},"status","（状态机）、",[105,124,125],{},"amount",[105,127,128],{},"createdAt",[105,130,131],{},"startTime",[105,133,134],{},"endTime",[105,136,137],{},"cancelledAt"," 等。",[10,140,141],{},"最容易踩的坑：不要为了\"简洁\"就给每种服务拆一个订单表。一开始拆，后面统计、对账、消息推送都是灾难。",[94,143,145],{"id":144},"_2-账本模型最复杂","2. 账本模型（最复杂）",[10,147,148],{},"这是文章的重点。27 个模块里，账本类模块有 5 个：",[150,151,152,158,164,170,176],"ol",{},[32,153,154,157],{},[17,155,156],{},"Platform Ledger","（平台收支）",[32,159,160,163],{},[17,161,162],{},"Merchant Ledger","（商家明细）",[32,165,166,169],{},[17,167,168],{},"Agency Ledger","（代理商明细）",[32,171,172,175],{},[17,173,174],{},"Distributor Ledger","（分销商明细）",[32,177,178,181],{},[17,179,180],{},"Escort Ledger","（地陪\u002F达人明细）",[10,183,184],{},"再加上 3 个交易流水类：",[150,186,188,194,200],{"start":187},6,[32,189,190,193],{},[17,191,192],{},"Recharge Records","（充值）",[32,195,196,199],{},[17,197,198],{},"Refund Records","（退款）",[32,201,202,205],{},[17,203,204],{},"Withdrawal Accounts","（提现账户）",[10,207,208],{},"这 8 个都遵循同一套流水结构，共通字段有：",[210,211,216],"pre",{"className":212,"code":214,"language":215},[213],"language-text","{\n  ledgerRecordId,      \u002F\u002F 唯一标识\n  role,                \u002F\u002F 账本角色 (user\u002Fescort\u002Fmerchant\u002Fdistributor)\n  direction,           \u002F\u002F 方向 (income\u002Fexpense)\n  amount,              \u002F\u002F 金额\n  remark,              \u002F\u002F 备注\n  orderNo,             \u002F\u002F 源单号（可追溯）\n  recordedAt,          \u002F\u002F 记账时间\n  createdAt,\n  updatedAt\n}\n","text",[105,217,214],{"__ignoreMap":218},"",[10,220,221,222,108,225,108,228,231],{},"角色特定字段会有差异（比如 Merchant Ledger 多了 ",[105,223,224],{},"paymentMethod",[105,226,227],{},"balanceAfter",[105,229,230],{},"sceneType"," 等），但这套基础字段集合对所有 8 个账本都适用。",[10,233,234,237],{},[17,235,236],{},"最关键的决策：每个角色独立一张表，不要合并。"," 很多人会想\"都是账本，能不能用一个大表加 role 字段区分？\"不行。原因有三：",[29,239,240,243,246],{},[32,241,242],{},"查询效率：同一个角色的流水集中在一张表，索引策略清晰；混在一张表里，where 条件变复杂",[32,244,245],{},"扩展性：未来某个角色的流水逻辑变复杂，新加字段只影响这张表",[32,247,248],{},"权限隔离：后台按角色展示不同的账本页面，数据表和权限边界对齐，防止权限越界的 Bug",[94,250,252],{"id":251},"_3-单笔订单如何拆成多条流水多角色分账","3. 单笔订单如何拆成多条流水（多角色分账）",[10,254,255],{},"这是设计中最容易出错的地方。用户下单一笔，金额是 100 元。这 100 元要在 5 个账本里都体现：",[29,257,258,263,268,273,278],{},[32,259,260,262],{},[17,261,156],{},"：+100（收入）",[32,264,265,267],{},[17,266,162],{},"：-70（服务提供者从平台结账）",[32,269,270,272],{},[17,271,180],{},"：+60（地陪佣金）",[32,274,275,277],{},[17,276,168],{},"：+5（所属代理分成）",[32,279,280,282],{},[17,281,174],{},"：+5（所属分销分成）",[10,284,285],{},"一笔订单，拆成 5 条流水，分别计入 5 个账本。这不能在前端聚合，必须在订单产生时就由 order-service 通过 MQ 广播给各个 ledger 消费者异步处理。",[94,287,289],{"id":288},"_4-幂等键设计","4. 幂等键设计",[10,291,292],{},"MQ 消息可能被重复消费。幂等键设计决定了\"重复消费时是否产生重复的流水记录\"。",[10,294,295,296],{},"常见的幂等键结构：",[105,297,298],{},"{orderId}-{ledgerRecordType}-{direction}-{roleId}",[10,300,301],{},"比如：\"order-12345-commission-income-escort-789\"",[10,303,304],{},"这个 key 存到 Redis，TTL 设为 24 小时。消费者拿到消息后，先检查这个 key 是否存在。如果存在，说明之前处理过，直接返回；如果不存在，才生成一条新的 ledger_record。",[10,306,307],{},"不这样做的后果：用户投诉\"我下单了，怎么平台账户进账两次\"，查日志发现订单服务发了两条相同的 MQ 消息，或者消费者重启期间消息重新投递了。",[94,309,311],{"id":310},"_5-对账口径","5. 对账口径",[10,313,314,315,318],{},"\"对账\"是财务部门关心的。账本数据必须能对应到源单据。每条流水都要有 ",[105,316,317],{},"orderNo"," 字段（或其他源单号），这样财务人员可以随时拿着订单号查到对应的账本记录。",[10,320,321],{},"后台的\"平台收支\"页面，最关键的操作是能按订单号检索流水。如果没有这个，财务无法核账。",[22,323,324],{"id":324},"按模板批量填充",[10,326,327],{},"定好模板和核心模型后，填充就快了。以 Merchant Ledger 为例，从模板复制到文档，然后：",[150,329,330,336,342,348,357,363,373,385],{},[32,331,332,335],{},[17,333,334],{},"Goal"," 部分：改成\"查看商家账户余额流水\"",[32,337,338,341],{},[17,339,340],{},"Recommended Approach","：解释为什么独立一张表而不是从商家余额字段反推",[32,343,344,347],{},[17,345,346],{},"Scope","：列出这个模块包括查询、删除，不包括批量删除、导出",[32,349,350,353,354],{},[17,351,352],{},"Information Architecture","：菜单路由 ",[105,355,356],{},"\u002Ffinance\u002Fmerchant-ledger",[32,358,359,362],{},[17,360,361],{},"Page Design","：筛选项（商家、支付方式、时间）、表格列（商户名、手机号、金额、余额、时间等）",[32,364,365,368,369,372],{},[17,366,367],{},"Data Model","：表名 ",[105,370,371],{},"merchant_ledger_records","，字段集合",[32,374,375,378,379,108,382],{},[17,376,377],{},"API Design","：",[105,380,381],{},"GET \u002Fadmin\u002Ffinance\u002Fmerchant-ledger",[105,383,384],{},"DELETE \u002Fadmin\u002Ffinance\u002Fmerchant-ledger\u002F:id",[32,386,387,390],{},[17,388,389],{},"Testing Strategy","：参数转发、筛选正确、金额计算正确",[10,392,393],{},"从模板到完成，一个模块 20-30 分钟。27 个模块，按这个节奏，两天内完全可以产出。",[22,395,396],{"id":396},"避免的陷阱",[10,398,399,402],{},[17,400,401],{},"陷阱 1：过度个性化","。如果每个模块都要求\"独特的设计\"，模板就没用了。要求统一：所有后台列表页遵循同一个 UI 结构，只改列和筛选项。",[10,404,405,408,409,412,413,416,417,420],{},[17,406,407],{},"陷阱 2：字段命名不一致","。A 账本叫 ",[105,410,411],{},"ledgerRecordId","，B 账本叫 ",[105,414,415],{},"recordId","，C 账本叫 ",[105,418,419],{},"id","。查询时容易出错。从一开始就定好规范，全部遵循。",[10,422,423,426],{},[17,424,425],{},"陷阱 3：索引规划滞后","。数据模型写完了，才想起来没有索引。到了后期\"查询慢\"才加索引，可能已经影响了代码逻辑。索引要在设计文档里就列出来。",[10,428,429,432],{},[17,430,431],{},"陷阱 4：忽略 Not Included","。设计文档里不写\"不包含什么\"，后续就有人问\"为什么没有批量删除\"\"为什么没有导出\"。明确列出 Scope 的边界，能省很多周期。",[22,434,435],{"id":435},"总结",[10,437,438,439,442],{},"批量产出设计文档的要诀是",[17,440,441],{},"复用而非重复编写","。文档模板保证了结构统一，核心数据模型保证了逻辑一致，两者结合，27 个模块的设计工作量从\"无法估算\"变成\"可预测\"。",[10,444,445],{},"最后的提醒：这套方法对脚手架期这样\"框架先行、业务后补\"的场景特别有效。但如果业务需求本身就不清楚，文档多了也没用——反而会变成\"写了一堆高保真但实际没用的文档\"。务必确保需求先冻结，再启动文档产出。",{"title":218,"searchDepth":447,"depth":447,"links":448},2,[449,450,458,459,460],{"id":24,"depth":447,"text":24},{"id":85,"depth":447,"text":85,"children":451},[452,454,455,456,457],{"id":96,"depth":453,"text":97},3,{"id":144,"depth":453,"text":145},{"id":251,"depth":453,"text":252},{"id":288,"depth":453,"text":289},{"id":310,"depth":453,"text":311},{"id":324,"depth":447,"text":324},{"id":396,"depth":447,"text":396},{"id":435,"depth":447,"text":435},"工程手记","2026-04-28","md",null,{},true,"\u002F2026-04-28-helpplay-27",{"title":5,"description":12},"2026-04-28-helpplay-27个模块设计文档两天产出","批量产出设计文档的方法——统一模板、共享核心模型、按模板填充。27 个业务模块若逐个手写撑不住进度，通过先定死文档骨架和跨模块通用数据结构，可以降低重复劳动。",[472,473,474,475],"文档工程","设计模板","模块化设计","分账系统","EmM4jsQgKaOyTtIZh6WmNxTg1Gre2siFrTEdsAHNqKI",[478,674,1030],{"id":479,"title":480,"body":481,"column":461,"date":659,"description":660,"extension":463,"hero_image":464,"meta":661,"navigation":466,"path":662,"seo":663,"series_id":464,"severity":464,"stem":664,"summary":665,"tags":666,"__hash__":673},"posts\u002F2026-09-10-Ai项目群的工程化分水岭.md","AI 项目做多以后，真正难的是把边界接起来",{"type":7,"value":482,"toc":648},[483,490,493,496,500,504,514,517,520,535,538,542,561,564,568,577,580,584,587,590,610,613,617,620,623,626,629,632,635,639,642,645],[10,484,485,486,489],{},"这段时间回看 ",[105,487,488],{},"\u002FUsers\u002Fxingye\u002FAi"," 下的项目，最明显的变化不是项目数量增加，而是问题的重心变了。",[10,491,492],{},"早期问题集中在“功能能不能跑起来”：语音推理、网页生成、桌面端启动、小程序页面、视频服务。进入平台化阶段以后，问题变成了另一组：任务是否可追踪，失败是否能恢复，费用是否能对账，版本是否能回滚，用户是否知道发生了什么。",[10,494,495],{},"这是一条从功能开发走向系统工程的分水岭。",[22,497,499],{"id":498},"一项目群已经形成几条清晰的产品线","一、项目群已经形成几条清晰的产品线",[94,501,503],{"id":502},"agent-与多租户平台","Agent 与多租户平台",[10,505,506,509,510,513],{},[105,507,508],{},"yun-claw"," 与 ",[105,511,512],{},"yun-claude"," 的记录显示，平台主线已经覆盖用户容器、模型接入、画布工作流、代理渠道、后台管理和运营工具。近期提交涉及画布自绘 Select、资产大图预览、代理激活码申请，以及渠道注册页的差异化展示。",[10,515,516],{},"这些功能看起来分属不同页面，底层却共享一套问题：用户身份、资源权限、异步任务和状态反馈必须保持一致。只改页面，不补状态契约，功能就会在跨页面操作时暴露断点。",[94,518,519],{"id":519},"内容生产流水线",[10,521,522,108,525,108,528,509,531,534],{},[105,523,524],{},"ai-write",[105,526,527],{},"instantlink-site",[105,529,530],{},"ai-auto-edit",[105,532,533],{},"ai-digital-human-workflow"," 分别覆盖写作、漫剧、自动剪辑和数字人视频。提交记录里反复出现几个关键词：任务网关、切片下载、共享卷、FFmpeg、提示词优化、失败提示和构建复现。",[10,536,537],{},"这说明内容生产已经不是单次调用模型，而是一条有中间产物的流水线。文本、图片、视频、音频和项目资产都有生命周期，任何一步的状态丢失都会让用户无法判断下一步该做什么。",[94,539,541],{"id":540},"交付网关与桌面端","交付、网关与桌面端",[10,543,544,108,547,108,550,108,553,556,557,560],{},[105,545,546],{},"newCodex",[105,548,549],{},"new-openclaw",[105,551,552],{},"openclaw",[105,554,555],{},"服务器管理"," 和 ",[105,558,559],{},"SSHTS"," 的记录，集中在更新、恢复、远程诊断、单实例生命周期和离线环境上。这里的核心指标不是“新版本发布了没有”，而是安装失败能否定位、更新中断能否恢复、客户机器能否留下可审计证据。",[10,562,563],{},"这也是为什么桌面端工程会逐渐靠近运维工程：安装器、后台服务、日志、配置和回滚必须一起设计。",[94,565,567],{"id":566},"网关计费与数据观察","网关、计费与数据观察",[10,569,570,509,573,576],{},[105,571,572],{},"NewAPI",[105,574,575],{},"new-api"," 的近期记录包括按模型统计 Token 消耗、扣费量、时间粒度聚合、昨日活跃用户和订单搜索。网关不只是转发请求，它还承担了模型选择、消耗记录、后台观察和问题追踪。",[10,578,579],{},"一旦计费数据进入用户体验，接口成功与业务成功就不再是同一个概念。请求返回 200，只能说明一次网络调用完成；扣费、产物落盘和任务终态是否一致，需要单独核对。",[22,581,583],{"id":582},"二git-记录里最有价值的变化是边界被写出来了","二、Git 记录里最有价值的变化，是“边界被写出来了”",[10,585,586],{},"多个项目的文档都在补同一类内容：部署手册、验收清单、测试报告、故障复盘、模型环境快照和用户协议。",[10,588,589],{},"这类文档的价值不在于把代码再讲一遍，而在于把系统边界写清楚：",[29,591,592,595,598,601,604,607],{},[32,593,594],{},"输入是什么，哪些字段必须存在；",[32,596,597],{},"状态有哪些，什么条件才能进入下一状态；",[32,599,600],{},"外部服务超时后，如何判断请求是否已经执行；",[32,602,603],{},"产物写入失败时，是否允许重试；",[32,605,606],{},"更新中断后，哪个版本可以安全恢复；",[32,608,609],{},"用户看到的成功，是否和后台记录的成功一致。",[10,611,612],{},"当这些问题没有答案时，系统只能靠人工经验维持。一旦项目数量增加，经验就会变成不可复制的隐性依赖。",[22,614,616],{"id":615},"三下一阶段的重点不是再加一个入口","三、下一阶段的重点不是再加一个入口",[10,618,619],{},"从现有项目的提交和文档可以看到，下一阶段更值得投入的是四个基础能力。",[10,621,622],{},"第一，统一任务状态。不同项目都在处理异步任务，但状态命名、失败原因和重试策略不应各自发明。",[10,624,625],{},"第二，统一产物账本。一个任务产生了哪些文件、消耗了哪些资源、是否对用户可见，需要能从提交到终态完整追踪。",[10,627,628],{},"第三，统一故障出口。401、超时、部分成功、未知执行和版本回滚，应该有明确的用户反馈与后台证据。",[10,630,631],{},"第四，统一发布验收。构建通过不代表线上可用，应该把健康检查、代理路径、真实端口、关键日志和浏览器操作放到同一份验收清单里。",[10,633,634],{},"项目多以后，工程能力不再体现在“能写多少功能”，而体现在“能否让不同功能遵守同一套边界”。",[22,636,638],{"id":637},"四我会保留的判断","四、我会保留的判断",[10,640,641],{},"AI 项目最容易被低估的部分，不是模型调用，而是模型调用之后的系统。",[10,643,644],{},"生成只是起点。身份、任务、费用、存储、交付、恢复和反馈，决定了一个功能能不能进入真实使用。",[10,646,647],{},"当 Git 提交开始频繁出现“补状态”“补恢复”“补验收”“补审计”时，这不是工程变慢了，而是产品从演示阶段进入了可运营阶段。",{"title":218,"searchDepth":447,"depth":447,"links":649},[650,656,657,658],{"id":498,"depth":447,"text":499,"children":651},[652,653,654,655],{"id":502,"depth":453,"text":503},{"id":519,"depth":453,"text":519},{"id":540,"depth":453,"text":541},{"id":566,"depth":453,"text":567},{"id":582,"depth":447,"text":583},{"id":615,"depth":447,"text":616},{"id":637,"depth":447,"text":638},"2026-09-10","这段时间回看 \u002FUsers\u002Fxingye\u002FAi 下的项目，最明显的变化不是项目数量增加，而是问题的重心变了。",{},"\u002F2026-09-10-ai",{"title":480,"description":660},"2026-09-10-Ai项目群的工程化分水岭","从多个 AI 项目的 Git 记录和设计文档看，工程难点已经从“能不能生成”转向任务、计费、交付、恢复与用户反馈能否形成闭环。",[667,668,669,670,671,672],"AI 平台","Agent","内容生产","网关","桌面端","工程化","DzOpOX92EsanPp8pLNTusbzBmKH3WOTf3qcaZAFmhM8",{"id":675,"title":676,"body":677,"column":461,"date":1017,"description":681,"extension":463,"hero_image":464,"meta":1018,"navigation":466,"path":1019,"seo":1020,"series_id":464,"severity":464,"stem":1021,"summary":1022,"tags":1023,"__hash__":1029},"posts\u002F2026-08-15-上线前我把整个仓库审了一遍.md","上线前，我把整个仓库审了一遍",{"type":7,"value":678,"toc":1006},[679,682,685,689,692,712,715,718,722,725,728,731,734,738,741,744,810,813,820,824,830,833,836,843,847,850,861,864,868,871,877,880,893,896,899,903,906,913,919,922,926,929,934,951,956,970,973,976,980,983,1003],[10,680,681],{},"上线前做了一次全量静态审查。",[10,683,684],{},"本文只讲方法和结论分布。审查报告本身不入库，也不在这里复述——那份报告带内网拓扑、密钥存放位置和尚未修复问题的文件行号证据，公开它就等于公开一份攻击者的地图。",[22,686,688],{"id":687},"一审查怎么进行","一、审查怎么进行",[10,690,691],{},"三个部分：",[150,693,694,700,706],{},[32,695,696,699],{},[17,697,698],{},"人工列清单。"," 先按「一个系统可能从哪些地方被打穿」列出检查面，而不是按目录扫。",[32,701,702,705],{},[17,703,704],{},"四个并行只读探查代理。"," 每个代理负责一组检查面，只读，不允许改任何文件。",[32,707,708,711],{},[17,709,710],{},"关键证据人工逐条复核。"," 代理解释现象可以，但「这条算不算风险、算多重」由人定。",[10,713,714],{},"第三步是必要的。代理很容易把「写法不理想」报成「高危」，也很容易漏掉需要跨文件才能看出的问题。它适合做的是穷举和取证，不适合定级。",[10,716,717],{},"审查方式记为：静态审查（人工 + 4 个并行只读探查代理）+ 关键证据人工逐条复核。",[22,719,721],{"id":720},"二只读约束的作用","二、只读约束的作用",[10,723,724],{},"让探查代理只能读，有两个好处。",[10,726,727],{},"一是没有副作用。审查过程中仓库保持原样，报告的结论可以对着同一个 commit 复现。",[10,729,730],{},"二是省掉了「代理顺手改一下」的干扰。审查和修改混在一起，会出现「报告说有问题的地方已经被改过」的状态，后续复核就无法对着证据走。",[10,732,733],{},"修改是审查之后单独一轮的事。",[22,735,737],{"id":736},"三20-条与四个级别","三、20 条与四个级别",[10,739,740],{},"产出 20 条风险，编号 R01 到 R20。每一条有：位置、触发条件、影响、修复优先级。",[10,742,743],{},"按修复优先级分四档：",[745,746,747,763],"table",{},[748,749,750],"thead",{},[751,752,753,757,760],"tr",{},[754,755,756],"th",{},"优先级",[754,758,759],{},"条数",[754,761,762],{},"排期",[764,765,766,778,789,800],"tbody",{},[751,767,768,772,775],{},[769,770,771],"td",{},"P0",[769,773,774],{},"2",[769,776,777],{},"本周内",[751,779,780,783,786],{},[769,781,782],{},"P1",[769,784,785],{},"6",[769,787,788],{},"两周内",[751,790,791,794,797],{},[769,792,793],{},"P2",[769,795,796],{},"5",[769,798,799],{},"一个月内",[751,801,802,805,808],{},[769,803,804],{},"P3",[769,806,807],{},"7",[769,809,799],{},[10,811,812],{},"另外有一个独立的「严重级别」列，按严重 \u002F 高 \u002F 中 \u002F 低分：1 \u002F 4 \u002F 7 \u002F 8。两列不对应是有意的——「有多严重」和「该多快修」是两件事。一条严重但需要停机才能修的问题，排期上反而不能放在本周。",[10,814,815,816,819],{},"整体评级：",[17,817,818],{},"高","。",[22,821,823],{"id":822},"四结论里最值得记的一句","四、结论里最值得记的一句",[825,826,827],"blockquote",{},[10,828,829],{},"未发现 SQL 注入、越权与 IDOR、存储型 XSS 等直接可利用的应用漏洞。",[10,831,832],{},"这一句和「整体评级高」摆在一起看，是这个月里最值得记的一条工程结论。",[10,834,835],{},"风险分布和直觉不一致。业务代码那一侧没查出可直接利用的漏洞——那部分每天在改、每天在跑、每天有人看。问题更多集中在业务代码之外：构建、部署、配置、凭据流转、依赖引入、运行时加固。",[10,837,838,839,842],{},"这些东西的共同点是",[17,840,841],{},"平时不跑","。部署脚本一天执行几次，Dockerfile 改一次放很久，集群配置是照着文档抄的。没有日常反馈的东西，问题会一直留在那里，而且不会有人因为「它今天没出事」而去核对它。",[22,844,846],{"id":845},"五修复的排期方式","五、修复的排期方式",[10,848,849],{},"路线图按三档排：P0 本周、P1 两周内、P2\u002FP3 一个月内。",[10,851,852,853,856,857,860],{},"排期依据不是严重级别，是两件事：",[17,854,855],{},"能不能立即修","，和",[17,858,859],{},"修它会不会动到线上","。一条严重但需要停机或改基础设施配置的问题，得等一个发布窗口；一条中等但改动只在一处代码里的问题，可以马上做。P2\u002FP3 是加固项，攒到下一个版本一起走。",[10,862,863],{},"这样做的好处是排期可执行。「两个月内修完 20 条」听起来完整，实际会卡在第一条需要停机的问题上。",[22,865,867],{"id":866},"六报告为什么不入库","六、报告为什么不入库",[10,869,870],{},"仓库的忽略规则里专门有一条：",[210,872,875],{"className":873,"code":874,"language":215},[213],"# 安全审查报告：含内网拓扑、密钥存放路径与未修复漏洞的 file:line 证据，绝不入库\n",[105,876,874],{"__ignoreMap":218},[10,878,879],{},"三条理由，最后一条最关键：",[29,881,882,885,888],{},[32,883,884],{},"内网拓扑",[32,886,887],{},"密钥存放位置",[32,889,890],{},[17,891,892],{},"未修复漏洞的精确定位",[10,894,895],{},"前两条是常识。第三条是这类文档的特例：一份「待修复清单」在修完之前，本身就是攻击说明书。而它又必须有精确定位，否则修不动。",[10,897,898],{},"所以这份文档的流转方式和代码相反。代码进仓库、留历史；它不进仓库，修完一条删一条，修完的结论改记到提交信息里。",[22,900,902],{"id":901},"七审查之后同一天修的东西","七、审查之后，同一天修的东西",[10,904,905],{},"审查和修复放在同一天做，这是这次的一个做法。",[10,907,908,909,912],{},"当天修的一批，主题集中在一类：",[17,910,911],{},"不报错的失败","。队列投递端断线后不重连、依赖返回的包装形状读错导致判定恒为假、入队不带权重插到最前面、参数格式不对被上游全部拒收、重试链被自己的去重压短、管理操作的幂等键缺失、用户可控的文件名直接进对象存储键、唯一漏掉来源校验的 IPC handler、并发删除后返回假成功、过期验证码被复活、临时文件不清理。",[10,914,915,916,819],{},"这些已经有单独一篇记录，这里只提它们的共性：",[17,917,918],{},"都不抛异常",[10,920,921],{},"审查报告里那些「不进仓库」的条目和这些「当天修完」的条目，其实是同一批检查过出来的。区别只在于前者需要动基础设施、要等窗口，后者可以立刻改。",[22,923,925],{"id":924},"八静态审查的边界","八、静态审查的边界",[10,927,928],{},"这一轮做的是静态审查，能覆盖和不能覆盖的东西都比较明确。",[10,930,931],{},[17,932,933],{},"能覆盖：",[29,935,936,939,942,945,948],{},[32,937,938],{},"配置与依赖：镜像标签、依赖来源、加密参数、权限配置。",[32,940,941],{},"错误处理：异常的吞掉与上抛、失败路径有没有留痕。",[32,943,944],{},"凭据流转：什么地方读、什么地方写、会不会进构建产物。",[32,946,947],{},"部署清单：运行用户、资源限制、网络策略、滚动更新参数。",[32,949,950],{},"代码层面的模式：路径拼接、命令拼接、输入校验的缺失。",[10,952,953],{},[17,954,955],{},"覆盖不到：",[29,957,958,961,964,967],{},[32,959,960],{},"运行时竞态。两个请求同时到、并发删除、租约过期，这些要在真实并发下才暴露。",[32,962,963],{},"真实的流量特征。哪些入口真的被外部打到、参数实际长什么样，静态看只能推测。",[32,965,966],{},"业务逻辑漏洞。越权取数据这类问题需要理解「这个人该不该看到这条」，而业务意图不在代码里。",[32,968,969],{},"组合链。单看每一步都合规，串起来才是问题——这类要动态测试或者真实对抗。",[10,971,972],{},"第三类是最容易被静态审查漏掉的：代码里所有校验都写了，但校验的规则和业务规则不一致。",[10,974,975],{},"所以静态审查的产出不是「系统安全了」，而是「在它能看见的那一面里，还剩哪些要做」。",[22,977,979],{"id":978},"九一次审查真正留下的东西","九、一次审查真正留下的东西",[10,981,982],{},"审完当天修掉的那批，留下的是代码。报告里那些没修的，留下的是三样东西：",[150,984,985,991,997],{},[32,986,987,990],{},[17,988,989],{},"一份带精确定位和优先级的清单","，不进仓库，逐条消掉。",[32,992,993,996],{},[17,994,995],{},"一个判断","：风险集中的地方和日常注意力集中的地方不一样。业务代码天天改，反而干净；构建部署配置平时没人动。",[32,998,999,1002],{},[17,1000,1001],{},"一次复核的习惯","：审查的结论要能对着一个 commit 复现，所以探查只读、修改单独一轮。",[10,1004,1005],{},"第 2 条比第 1 条重要。清单会修完，但「平时不跑的东西没人核对」这件事不会自己消失。",{"title":218,"searchDepth":447,"depth":447,"links":1007},[1008,1009,1010,1011,1012,1013,1014,1015,1016],{"id":687,"depth":447,"text":688},{"id":720,"depth":447,"text":721},{"id":736,"depth":447,"text":737},{"id":822,"depth":447,"text":823},{"id":845,"depth":447,"text":846},{"id":866,"depth":447,"text":867},{"id":901,"depth":447,"text":902},{"id":924,"depth":447,"text":925},{"id":978,"depth":447,"text":979},"2026-08-15",{},"\u002F2026-08-15",{"title":676,"description":681},"2026-08-15-上线前我把整个仓库审了一遍","一次上线前的静态安全审查：四个并行只读代理加人工逐条复核，产出 20 条分级风险与三档修复路线图。本文记录方法，不复述条目。",[1024,1025,1026,1027,1028],"安全审查","代码审查","风险分级","修复排期","Agent 协作","bnkC_2bMe8OfgB00HIfDUS9bdRwLUZb_WqDBvXtq__M",{"id":1031,"title":1032,"body":1033,"column":461,"date":1948,"description":1037,"extension":463,"hero_image":464,"meta":1949,"navigation":466,"path":1950,"seo":1951,"series_id":464,"severity":464,"stem":1952,"summary":1953,"tags":1954,"__hash__":1961},"posts\u002F2026-08-02-首页改成八屏.md","首页改成八屏",{"type":7,"value":1034,"toc":1935},[1035,1038,1044,1047,1051,1054,1064,1073,1076,1079,1087,1094,1100,1106,1171,1174,1193,1198,1203,1206,1212,1216,1219,1222,1230,1233,1236,1242,1245,1273,1276,1390,1393,1399,1403,1406,1422,1436,1439,1455,1458,1461,1465,1471,1474,1477,1483,1486,1536,1540,1543,1546,1549,1555,1558,1561,1566,1572,1576,1579,1582,1588,1592,1597,1638,1648,1651,1680,1685,1688,1738,1744,1747,1804,1816,1819,1823,1826,1829,1834,1837,1840,1845,1849,1852,1922,1925,1931],[10,1036,1037],{},"这个博客的首页原来是连续滚动。改成了整屏分页，一页一件事，共八屏：",[210,1039,1042],{"className":1040,"code":1041,"language":215},[213],"首屏 \u002F 自我介绍 01 \u002F 自我介绍 02 \u002F 做过的事 \u002F 精选复盘 \u002F 项目 \u002F 专栏 \u002F 联系\n",[105,1043,1041],{"__ignoreMap":218},[10,1045,1046],{},"一屏容纳是逐档实测调出来的，不是估的。",[22,1048,1050],{"id":1049},"一吸附用谁的","一、吸附用谁的",[10,1052,1053],{},"第一个决定是吸附用哪套机制。",[10,1055,1056,1057,1060,1061,378],{},"原生有 ",[105,1058,1059],{},"scroll-snap","，CSS 几行就能写。但这里用不了——页面上的平滑滚动是 Lenis 提供的，它用 JS 驱动 ",[105,1062,1063],{},"scrollTop",[825,1065,1066],{},[10,1067,1068,1069,1072],{},"吸附用 Lenis 自带的 Snap 而不是 CSS scroll-snap——Lenis 是用 JS 驱动 scrollTop 的，和原生吸附会互相抢控制权、滚起来发抖。Snap 挂在同一个 Lenis 实例上没有这个问题，",[105,1070,1071],{},"onSnapComplete"," 还能驱动指示器。",[10,1074,1075],{},"两个机制都想控制滚动位置，结果就是抖。",[10,1077,1078],{},"挂在同一个实例上之后，吸附完成后还能拿到回调，右侧的页码指示器直接跟着它更新。",[22,1080,1082,1083,1086],{"id":1081},"二mandatory-是错的","二、",[105,1084,1085],{},"mandatory"," 是错的",[10,1088,1089,1090,1093],{},"第一版用 ",[105,1091,1092],{},"type: 'mandatory'","。用户反馈手感是「被抢走了」。",[10,1095,1096,1097,1099],{},"原因是 ",[105,1098,1085],{}," 在用户还在滑的时候就强行把页面拽向最近一页。滑动过程被插手，手感不是吸附，是失控。",[10,1101,1102,1103,378],{},"改成 ",[105,1104,1105],{},"proximity",[210,1107,1111],{"className":1108,"code":1109,"language":1110,"meta":218,"style":218},"language-ts shiki shiki-themes github-light github-dark","type: 'proximity',\ndistanceThreshold: '30%',\nduration: 1.1,\ndebounce: 500,\n","ts",[105,1112,1113,1133,1145,1158],{"__ignoreMap":218},[1114,1115,1118,1122,1126,1130],"span",{"class":1116,"line":1117},"line",1,[1114,1119,1121],{"class":1120},"sScJk","type",[1114,1123,1125],{"class":1124},"sVt8B",": ",[1114,1127,1129],{"class":1128},"sZZnC","'proximity'",[1114,1131,1132],{"class":1124},",\n",[1114,1134,1135,1138,1140,1143],{"class":1116,"line":447},[1114,1136,1137],{"class":1120},"distanceThreshold",[1114,1139,1125],{"class":1124},[1114,1141,1142],{"class":1128},"'30%'",[1114,1144,1132],{"class":1124},[1114,1146,1147,1150,1152,1156],{"class":1116,"line":453},[1114,1148,1149],{"class":1120},"duration",[1114,1151,1125],{"class":1124},[1114,1153,1155],{"class":1154},"sj4cs","1.1",[1114,1157,1132],{"class":1124},[1114,1159,1161,1164,1166,1169],{"class":1116,"line":1160},4,[1114,1162,1163],{"class":1120},"debounce",[1114,1165,1125],{"class":1124},[1114,1167,1168],{"class":1154},"500",[1114,1170,1132],{"class":1124},[10,1172,1173],{},"三条参数各管一件事：",[29,1175,1176,1181,1187],{},[32,1177,1178,1180],{},[105,1179,1105],{}," 只在停下来且已经接近边界时才轻推一把。",[32,1182,1183,1186],{},[105,1184,1185],{},"distanceThreshold: '30%'"," 定义「接近」——落点离边界不到 30% 视口高才吸附，停在页面中间就让它停着。",[32,1188,1189,1192],{},[105,1190,1191],{},"debounce: 500"," 等惯性完全停下来再判断，不在滑行中途插手。",[10,1194,1195,1197],{},[105,1196,1163],{}," 从 220 提到 500：",[825,1199,1200],{},[10,1201,1202],{},"debounce 太小会在惯性还没停时就抢着吸附，手感发涩。",[10,1204,1205],{},"实测结果：",[210,1207,1210],{"className":1208,"code":1209,"language":215},[213],"滑动全程 0 反向帧（原来会被拽回）\n停在离边界 306px（超过 30% 阈值）时保持不动\n停在边界附近才对齐整页\n",[105,1211,1209],{"__ignoreMap":218},[22,1213,1215],{"id":1214},"三一屏装不装得下","三、一屏装不装得下",[10,1217,1218],{},"分页的前提是内容能装进一屏。这一条逐档量过。",[10,1220,1221],{},"首屏原高 921px：",[29,1223,1224,1227],{},[32,1225,1226],{},"1440×900 超出 89px",[32,1228,1229],{},"1366×768 超出 201px",[10,1231,1232],{},"精选复盘 788px，在 1366×768 超出 88px。",[10,1234,1235],{},"两个方向可选：砍内容，或者按档收紧间距。选了后者，分三档：",[210,1237,1240],{"className":1238,"code":1239,"language":215},[213],"min-width: 1024px and max-height: 820px   深压\nmin-width: 1024px and max-height: 900px   只压首屏\nmin-width: 1024px and min-height: 1000px  放大间距吃大屏留白\n",[105,1241,1239],{"__ignoreMap":218},[10,1243,1244],{},"中间那档的调整方式值得记一笔。为了挤出 27px，第一版直接砍了字号：",[210,1246,1250],{"className":1247,"code":1248,"language":1249,"meta":218,"style":218},"language-css shiki shiki-themes github-light github-dark","font-size: clamp(46px, 9.2vw, 132px)  →  clamp(30px, 5.4vw, 62px)\n","css",[105,1251,1252],{"__ignoreMap":218},[1114,1253,1254,1258,1261,1264,1267,1270],{"class":1116,"line":1117},[1114,1255,1257],{"class":1256},"s9eBZ","font-size",[1114,1259,1260],{"class":1124},": clamp(46px, 9",[1114,1262,1263],{"class":1120},".2vw",[1114,1265,1266],{"class":1124},", 132px)  →  clamp(30px, 5",[1114,1268,1269],{"class":1120},".4vw",[1114,1271,1272],{"class":1124},", 62px)\n",[10,1274,1275],{},"砍掉一半多。首屏标题是整页的主体，砍字号会让整屏塌下来。改成全部从内边距要：",[210,1277,1279],{"className":1247,"code":1278,"language":1249,"meta":218,"style":218},"@media (min-width: 1024px) and (max-height: 900px) {\n  .page-hero :deep(.hero) { padding-top: 20px; padding-bottom: 24px; }\n  .page-hero :deep(.hero-foot) { margin-top: 26px; }\n}\n",[105,1280,1281,1322,1362,1385],{"__ignoreMap":218},[1114,1282,1283,1287,1290,1293,1295,1298,1301,1304,1307,1309,1312,1314,1317,1319],{"class":1116,"line":1117},[1114,1284,1286],{"class":1285},"szBVR","@media",[1114,1288,1289],{"class":1124}," (",[1114,1291,1292],{"class":1154},"min-width",[1114,1294,1125],{"class":1124},[1114,1296,1297],{"class":1154},"1024",[1114,1299,1300],{"class":1285},"px",[1114,1302,1303],{"class":1124},") ",[1114,1305,1306],{"class":1285},"and",[1114,1308,1289],{"class":1124},[1114,1310,1311],{"class":1154},"max-height",[1114,1313,1125],{"class":1124},[1114,1315,1316],{"class":1154},"900",[1114,1318,1300],{"class":1285},[1114,1320,1321],{"class":1124},") {\n",[1114,1323,1324,1327,1330,1333,1336,1339,1341,1344,1346,1349,1352,1354,1357,1359],{"class":1116,"line":447},[1114,1325,1326],{"class":1120},"  .page-hero",[1114,1328,1329],{"class":1124}," :deep(",[1114,1331,1332],{"class":1120},".hero",[1114,1334,1335],{"class":1124},") { ",[1114,1337,1338],{"class":1154},"padding-top",[1114,1340,1125],{"class":1124},[1114,1342,1343],{"class":1154},"20",[1114,1345,1300],{"class":1285},[1114,1347,1348],{"class":1124},"; ",[1114,1350,1351],{"class":1154},"padding-bottom",[1114,1353,1125],{"class":1124},[1114,1355,1356],{"class":1154},"24",[1114,1358,1300],{"class":1285},[1114,1360,1361],{"class":1124},"; }\n",[1114,1363,1364,1366,1368,1371,1373,1376,1378,1381,1383],{"class":1116,"line":453},[1114,1365,1326],{"class":1120},[1114,1367,1329],{"class":1124},[1114,1369,1370],{"class":1120},".hero-foot",[1114,1372,1335],{"class":1124},[1114,1374,1375],{"class":1154},"margin-top",[1114,1377,1125],{"class":1124},[1114,1379,1380],{"class":1154},"26",[1114,1382,1300],{"class":1285},[1114,1384,1361],{"class":1124},[1114,1386,1387],{"class":1116,"line":1160},[1114,1388,1389],{"class":1124},"}\n",[10,1391,1392],{},"只有 700px 可用高度那一档才让一档字号。三档实测：",[210,1394,1397],{"className":1395,"code":1396,"language":215},[213],"1440×900    132px\n1920×1080   132px\n1366×768     92.9px\n",[105,1398,1396],{"__ignoreMap":218},[22,1400,1402],{"id":1401},"四检测判据写错过一次","四、检测判据写错过一次",[10,1404,1405],{},"判断页面有没有溢出，第一版用的是：",[210,1407,1409],{"className":1108,"code":1408,"language":1110,"meta":218,"style":218},"scrollHeight > clientHeight\n",[105,1410,1411],{"__ignoreMap":218},[1114,1412,1413,1416,1419],{"class":1116,"line":1117},[1114,1414,1415],{"class":1124},"scrollHeight ",[1114,1417,1418],{"class":1285},">",[1114,1420,1421],{"class":1124}," clientHeight\n",[10,1423,1424,1425,1428,1429,556,1432,1435],{},"这个判据永远为假。页面用的是 ",[105,1426,1427],{},"min-height","，内容超了页面会自己长高——",[105,1430,1431],{},"scrollHeight",[105,1433,1434],{},"clientHeight"," 一起变大，比值不变。",[10,1437,1438],{},"改成直接比可用高度：",[210,1440,1442],{"className":1108,"code":1441,"language":1110,"meta":218,"style":218},"\u002F\u002F 检测判据从 scrollHeight > clientHeight 改为直接比可用高度——\n\u002F\u002F 页面用的是 min-height，内容超了页面会自己长高，前者永远比不出溢出\n",[105,1443,1444,1450],{"__ignoreMap":218},[1114,1445,1446],{"class":1116,"line":1117},[1114,1447,1449],{"class":1448},"sJ8bj","\u002F\u002F 检测判据从 scrollHeight > clientHeight 改为直接比可用高度——\n",[1114,1451,1452],{"class":1116,"line":447},[1114,1453,1454],{"class":1448},"\u002F\u002F 页面用的是 min-height，内容超了页面会自己长高，前者永远比不出溢出\n",[10,1456,1457],{},"这个错误值得单独说：判据本身写错了，所以「三档零溢出」这个结论在修正之前是不成立的。",[10,1459,1460],{},"修正之后三档均为 8\u002F8 页零溢出。",[22,1462,1464],{"id":1463},"五不分页的三种情况","五、不分页的三种情况",[210,1466,1469],{"className":1467,"code":1468,"language":215},[213],"窄屏（\u003C 1024px）\n矮视口（\u003C 620px）\n减弱动效（prefers-reduced-motion）\n",[105,1470,1468],{"__ignoreMap":218},[10,1472,1473],{},"三种都退回连续滚动。",[10,1475,1476],{},"前两种有实测依据：",[210,1478,1481],{"className":1479,"code":1480,"language":215},[213],"实测 390px 下自我介绍 1448px、精选复盘 1114px，强行一屏只会截断内容\n",[105,1482,1480],{"__ignoreMap":218},[10,1484,1485],{},"第三种是硬要求。整屏吸附会强制改变滚动位置，这和「尊重减弱动效偏好」直接冲突。CSS 里也要同步禁用：",[210,1487,1489],{"className":1247,"code":1488,"language":1249,"meta":218,"style":218},"@media (prefers-reduced-motion: reduce) {\n  .page { min-height: 0; }\n  .pager { display: none; }\n}\n",[105,1490,1491,1498,1515,1532],{"__ignoreMap":218},[1114,1492,1493,1495],{"class":1116,"line":1117},[1114,1494,1286],{"class":1285},[1114,1496,1497],{"class":1124}," (prefers-reduced-motion: reduce) {\n",[1114,1499,1500,1503,1506,1508,1510,1513],{"class":1116,"line":447},[1114,1501,1502],{"class":1120},"  .page",[1114,1504,1505],{"class":1124}," { ",[1114,1507,1427],{"class":1154},[1114,1509,1125],{"class":1124},[1114,1511,1512],{"class":1154},"0",[1114,1514,1361],{"class":1124},[1114,1516,1517,1520,1522,1525,1527,1530],{"class":1116,"line":453},[1114,1518,1519],{"class":1120},"  .pager",[1114,1521,1505],{"class":1124},[1114,1523,1524],{"class":1154},"display",[1114,1526,1125],{"class":1124},[1114,1528,1529],{"class":1154},"none",[1114,1531,1361],{"class":1124},[1114,1533,1534],{"class":1116,"line":1160},[1114,1535,1389],{"class":1124},[22,1537,1539],{"id":1538},"六分页之后每页只剩半屏内容","六、分页之后，每页只剩半屏内容",[10,1541,1542],{},"八屏分配完之后，出现一个反向问题：每页内容太少。",[10,1544,1545],{},"实测 1440×900 下多数页只填了 47%~63%，「做过的事」那页 63% 是空的。",[10,1547,1548],{},"既然分了页，每页就得撑得住。逐页补实：",[210,1550,1553],{"className":1551,"code":1552,"language":215},[213],"做过的事：加页首概览条（项目数\u002F时间跨度\u002F人手），每项补详细说明、关键指标与技术标签。308px → 548px\n项目：3 张卡补到 6 张，两行三列。362px → 511px\n联系：补引言、about 与 timeline 两个入口、站点信息带。452px → 791px\n专栏：每张卡补该栏最新一篇标题。512px → 597px\n自我介绍 01：四张能力卡各补首组的真实条目。504px → 596px\n自我介绍 02：补技术栈横带。522px → 603px\n",[105,1554,1552],{"__ignoreMap":218},[10,1556,1557],{},"填充率从 47%~63% 提到 61%~100%。",[10,1559,1560],{},"同一轮里把两个区块的入场动画也补了常驻循环，因为：",[825,1562,1563],{},[10,1564,1565],{},"实测入场动画本来就在跑（轴线 scaleX 0→0.99、圆点带回弹、卡片 clip-path 从 78% 揭到 4%、指标读数往上跳），问题是太隐蔽：一次性、1 秒内结束、触发点又在区块刚露头时，等正眼看过去通常已经播完。",[210,1567,1570],{"className":1568,"code":1569,"language":215},[213],"触发点 top 88% → 95%，轴线 0.9s → 1.25s，圆点与节点错峰 0.11 → 0.16\n",[105,1571,1569],{"__ignoreMap":218},[22,1573,1575],{"id":1574},"七断点从-900-降到-820","七、断点从 900 降到 820",[10,1577,1578],{},"第一版把「矮视口」定在 900px 高。1440×900 的屏幕因此被当成矮视口，白白砍掉了列表摘要——精选复盘从 788px 缩到 389px。",[10,1580,1581],{},"而 1440×900 明明有空间。断点降到 820：",[210,1583,1586],{"className":1584,"code":1585,"language":215},[213],"820 以下   深压\n900 以下   只压首屏\n1000 以上  放大间距吃掉大屏留白\n",[105,1587,1585],{"__ignoreMap":218},[22,1589,1591],{"id":1590},"八两个-css-优先级问题","八、两个 CSS 优先级问题",[10,1593,1594],{},[17,1595,1596],{},"首屏被挤成一个框。",[210,1598,1600],{"className":1247,"code":1599,"language":1249,"meta":218,"style":218},".page { padding: 24px 0; }\n.page-hero { padding: 0; }\n",[105,1601,1602,1623],{"__ignoreMap":218},[1114,1603,1604,1607,1609,1612,1614,1616,1618,1621],{"class":1116,"line":1117},[1114,1605,1606],{"class":1120},".page",[1114,1608,1505],{"class":1124},[1114,1610,1611],{"class":1154},"padding",[1114,1613,1125],{"class":1124},[1114,1615,1356],{"class":1154},[1114,1617,1300],{"class":1285},[1114,1619,1620],{"class":1154}," 0",[1114,1622,1361],{"class":1124},[1114,1624,1625,1628,1630,1632,1634,1636],{"class":1116,"line":447},[1114,1626,1627],{"class":1120},".page-hero",[1114,1629,1505],{"class":1124},[1114,1631,1611],{"class":1154},[1114,1633,1125],{"class":1124},[1114,1635,1512],{"class":1154},[1114,1637,1361],{"class":1124},[10,1639,1640,1643,1644,1647],{},[105,1641,1642],{},".page-hero { padding: 0 }"," 写在媒体查询之前，被后面同优先级的 ",[105,1645,1646],{},".page { padding: ... }"," 覆盖了。首屏于是四边都留出内边距，看起来像一个框。",[10,1649,1650],{},"修法是在每一档里重新声明一次：",[210,1652,1654],{"className":1247,"code":1653,"language":1249,"meta":218,"style":218},"\u002F* 首屏必须满幅出血。这行不能省——上面那条 .page 同优先级且更靠后，\n   会把顶部声明的 .page-hero { padding: 0 } 覆盖掉。 *\u002F\n.page-hero { padding: 0; }\n",[105,1655,1656,1661,1666],{"__ignoreMap":218},[1114,1657,1658],{"class":1116,"line":1117},[1114,1659,1660],{"class":1448},"\u002F* 首屏必须满幅出血。这行不能省——上面那条 .page 同优先级且更靠后，\n",[1114,1662,1663],{"class":1116,"line":447},[1114,1664,1665],{"class":1448},"   会把顶部声明的 .page-hero { padding: 0 } 覆盖掉。 *\u002F\n",[1114,1667,1668,1670,1672,1674,1676,1678],{"class":1116,"line":453},[1114,1669,1627],{"class":1120},[1114,1671,1505],{"class":1124},[1114,1673,1611],{"class":1154},[1114,1675,1125],{"class":1124},[1114,1677,1512],{"class":1154},[1114,1679,1361],{"class":1124},[10,1681,1682],{},[17,1683,1684],{},"标题被大屏规则压小。",[10,1686,1687],{},"大屏档里有一条：",[210,1689,1691],{"className":1247,"code":1690,"language":1249,"meta":218,"style":218},".page :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }\n",[105,1692,1693],{"__ignoreMap":218},[1114,1694,1695,1697,1699,1702,1704,1706,1708,1711,1714,1717,1719,1722,1725,1728,1730,1733,1735],{"class":1116,"line":1117},[1114,1696,1606],{"class":1120},[1114,1698,1329],{"class":1124},[1114,1700,1701],{"class":1120},".big",[1114,1703,1335],{"class":1124},[1114,1705,1257],{"class":1154},[1114,1707,1125],{"class":1124},[1114,1709,1710],{"class":1154},"clamp",[1114,1712,1713],{"class":1124},"(",[1114,1715,1716],{"class":1154},"34",[1114,1718,1300],{"class":1285},[1114,1720,1721],{"class":1124},", ",[1114,1723,1724],{"class":1154},"3.4",[1114,1726,1727],{"class":1285},"vw",[1114,1729,1721],{"class":1124},[1114,1731,1732],{"class":1154},"56",[1114,1734,1300],{"class":1285},[1114,1736,1737],{"class":1124},"); }\n",[10,1739,1740,1741,1743],{},"首屏主标题的类名也是 ",[105,1742,1701],{},"。1920×1080 下它从 132px 被压到 56px，首屏整个塌掉。",[10,1745,1746],{},"改成排除首屏：",[210,1748,1750],{"className":1247,"code":1749,"language":1249,"meta":218,"style":218},"\u002F* 必须排除首屏——hero 里的 .big 是那个巨大的主标题，\n   被这条命中会从 132px 压到 56px，整个首屏塌掉 *\u002F\n.page:not(.page-hero) :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }\n",[105,1751,1752,1757,1762],{"__ignoreMap":218},[1114,1753,1754],{"class":1116,"line":1117},[1114,1755,1756],{"class":1448},"\u002F* 必须排除首屏——hero 里的 .big 是那个巨大的主标题，\n",[1114,1758,1759],{"class":1116,"line":447},[1114,1760,1761],{"class":1448},"   被这条命中会从 132px 压到 56px，整个首屏塌掉 *\u002F\n",[1114,1763,1764,1767,1769,1771,1774,1776,1778,1780,1782,1784,1786,1788,1790,1792,1794,1796,1798,1800,1802],{"class":1116,"line":453},[1114,1765,1766],{"class":1120},".page:not",[1114,1768,1713],{"class":1124},[1114,1770,1627],{"class":1120},[1114,1772,1773],{"class":1124},") :deep(",[1114,1775,1701],{"class":1120},[1114,1777,1335],{"class":1124},[1114,1779,1257],{"class":1154},[1114,1781,1125],{"class":1124},[1114,1783,1710],{"class":1154},[1114,1785,1713],{"class":1124},[1114,1787,1716],{"class":1154},[1114,1789,1300],{"class":1285},[1114,1791,1721],{"class":1124},[1114,1793,1724],{"class":1154},[1114,1795,1727],{"class":1285},[1114,1797,1721],{"class":1124},[1114,1799,1732],{"class":1154},[1114,1801,1300],{"class":1285},[1114,1803,1737],{"class":1124},[10,1805,1806,1807,819,1810,1812,1813,1815],{},"两个问题的成因相同：",[17,1808,1809],{},"同一个类名在两种语义下被复用",[105,1811,1606],{}," 既是容器也是列表页的内边距来源，",[105,1814,1701],{}," 既是区块小标题也是首屏主标题。分开之后就没这个问题。",[10,1817,1818],{},"修完实测：hero 左 0、底 1、右 10（右侧那 10px 是站点滚动条本身的宽度），标题 132px 满值，8\u002F8 页仍零溢出。",[22,1820,1822],{"id":1821},"九加载页期间的一个时序漏洞","九、加载页期间的一个时序漏洞",[10,1824,1825],{},"这个站有一个启动加载页，首次访问时盖几秒。加载页盖着的时候，入场动画被暂停了——否则动画会在遮挡期间自己跑完。",[10,1827,1828],{},"副作用是页面切换的幕布动画也被冻住：",[825,1830,1831],{},[10,1832,1833],{},"实测（加载页 3s 时点击）：t=745 路由已切到 \u002Fcolumns 而幕布 display:none，t=1406 幕布才出现并扫过。",[10,1835,1836],{},"路由在没有幕布的情况下切换了，等加载页结束、时间线恢复之后，被冻住的补间才补播。",[10,1838,1839],{},"结论是补一行守卫：加载页盖着时直接放行，不放幕布。理由是：",[825,1841,1842],{},[10,1843,1844],{},"真实用户点不到——加载页是全屏遮罩。但浏览器前进\u002F后退、程序化跳转仍可能在窗口内触发导航。",[22,1846,1848],{"id":1847},"十整屏分页的账","十、整屏分页的账",[10,1850,1851],{},"这套改动最后收敛成一张数字表：",[745,1853,1854,1864],{},[748,1855,1856],{},[751,1857,1858,1861],{},[754,1859,1860],{},"项目",[754,1862,1863],{},"数值",[764,1865,1866,1874,1882,1890,1898,1906,1914],{},[751,1867,1868,1871],{},[769,1869,1870],{},"页数",[769,1872,1873],{},"8",[751,1875,1876,1879],{},[769,1877,1878],{},"吸附类型",[769,1880,1881],{},"proximity，阈值 30%，防抖 500ms",[751,1883,1884,1887],{},[769,1885,1886],{},"吸附时长",[769,1888,1889],{},"1.1s",[751,1891,1892,1895],{},[769,1893,1894],{},"分页生效条件",[769,1896,1897],{},"宽 ≥ 1024px 且 高 ≥ 620px 且 未开启减弱动效",[751,1899,1900,1903],{},[769,1901,1902],{},"溢出",[769,1904,1905],{},"三档均 0\u002F8",[751,1907,1908,1911],{},[769,1909,1910],{},"填充率",[769,1912,1913],{},"61%~100%（改前 47%~63%）",[751,1915,1916,1919],{},[769,1917,1918],{},"首屏标题",[769,1920,1921],{},"132px（1440×900、1920×1080）、92.9px（1366×768）",[10,1923,1924],{},"每一条都有一次实测在后面。这类改动没别的办法——「一屏装不装得下」这件事只能量，量完才知道该压间距还是该砍内容。",[10,1926,1927,1928,1930],{},"中间还有两处是判断上的错，不是量的问题：一是用 ",[105,1929,1085],{}," 吸附（把用户的滑动抢走了），二是把「矮视口」定在 900（把有空间的屏幕当成没空间）。前者的修法是换模式，后者的修法是挪断点。",[1932,1933,1934],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":218,"searchDepth":447,"depth":447,"links":1936},[1937,1938,1940,1941,1942,1943,1944,1945,1946,1947],{"id":1049,"depth":447,"text":1050},{"id":1081,"depth":447,"text":1939},"二、mandatory 是错的",{"id":1214,"depth":447,"text":1215},{"id":1401,"depth":447,"text":1402},{"id":1463,"depth":447,"text":1464},{"id":1538,"depth":447,"text":1539},{"id":1574,"depth":447,"text":1575},{"id":1590,"depth":447,"text":1591},{"id":1821,"depth":447,"text":1822},{"id":1847,"depth":447,"text":1848},"2026-08-02",{},"\u002F2026-08-02",{"title":1032,"description":1037},"2026-08-02-首页改成八屏","把博客首页从连续滚动改成八屏整屏分页。吸附用 Lenis 自带能力而不是 CSS 原生，断点按实测调整，每一档的溢出与填充率都有数字。",[1955,1956,1957,1958,1959,1960],"GSAP","Lenis","滚动吸附","响应式断点","页面结构","prefers-reduced-motion","2o9dKTJYPwCLxF__bLLN46O2x_0PMJoQgyZSxZpO428",1789212573203]