Files
laoyang 32e9e9ab84 v47:地址联想三处接入 / 读书页慢的真根因(会话锁)/ 笔记两处 / 日程状态下线
用户 2026-10-13 七条。拍板:影院只记地址、不存经纬度(后续用高德反查);
日程状态只删界面与展示、数据库列保留(「后续会有一个整体清理的工作」)。

①🔴 读书页慢 —— 真根因是**会话锁**,不是封面(我先判断错了方向)
  用户原话:「读书现在还是非常慢,就是进入读书这个页面,刚开始还是非常的慢。」
  线上实测(真公网、同一会话连打 3 次):
      第 1 次 TTFB 0.08s  ← 触发微信读书同步
      第 2 次 TTFB **8.73s**  ← 用户说的"刚开始非常慢"
      第 3 次 TTFB 0.07s
  插桩拿到的服务端时间线把因果钉死:
      21:18:05.483 QUEUED        ← 第 1 个请求排队
      21:18:05.501 FINISH  2ms   ← 响应确实提前发出去了(这步是好的)
      21:18:14.674 SYNC    9.2s  ← 但它仍占着这个人的会话文件锁跑完 9.2 秒
      21:18:14.682 QUEUED        ← 第 2 个请求此刻才真正开始执行
  ⇒ **`fastcgi_finish_request()` 只断开客户端连接、不释放 PHP 文件会话锁。**
    后台同步要 9.2 秒(真打微信读书接口),同一个人的下一个请求一路等到它跑完。
  修法(缺一不可):
    · `WeRead::queueBackgroundSync()` 回调里 **先 `session_write_close()`、
      再 `fastcgi_finish_request()`**(顺序不能反);
    · `index.php` 两处"渲染前排同步"的入口排完**立刻放锁**
      (shutdown 回调不一定执行:客户端中断/服务器超时才不放过);
    · 顺带修 **`Holiday::queueBackgroundFetch()` 的同型问题**(联网 12 秒)。
  为什么以前没发现:v29 引入"后台化"时只验证了"响应是不是提前发出去",
  **单次请求测量永远看不到这个 bug** —— 必须同一会话连打两次才现形。
  线上复验:修复后连打 4 次 = 0.15 / 0.15 / 0.09 / 0.15 秒(修复前第 2 次 8.73s);
  后台同步仍正常(`weread_sync_at` 已更新)。

② 封面:顺手也修了(但这不是用户说的那个问题)
  · `media.php` 读 OSS 前先放会话锁(原来从不放,35 张封面串行);
  · 新增 `Storage::cacheLocally()` 磁盘二级缓存(原来 `read()` 只读不写回);
  · 让 304 不再先拉整张图(原来先取回 body、再判 `If-None-Match`)。
  线上实测:冷取 149ms(走 OSS)→ 热取 35ms(本地缓存、字节一致)
  → 带 `If-None-Match` 返回 **304 / 19.8ms / 空体**;35 张封面 103ms → 20ms(5 倍)。

③ 地址联想:抽成全站共用一份(`assets/js/addr_suggest.js`)+ 接入三处
  · 影院管理页、观影新增页的「新建影院」弹框、日程地点、行程出发/到达;
  · `layout.php` 按需加载(`[data-addr]` 出现才拉 SDK + 我们的脚本),
    并暴露 `fbLoadAddr(scope)` 给 AJAX 片段(v42 踩过"内联 script 不执行");
  · 安全密钥在 SDK 之前设置;回填支持 `data-addr-fill` / `data-addr-for`、
    **只补空白字段**(不覆盖用户手写)、直辖市 `province==city` 去重;
  · 字段名有两套(影院 `address`/`place`,行程 `from_addr`/`to_addr` 缩写 `_addr`),
    `fieldFor()` 带同义词表 + 后缀匹配;
  · 影院**只存地址、不存经纬度**(按用户要求)。

④ 笔记两条
  · 「点保存没反应」真因:附件区是个 `<form>` **套在保存表单里** ——
    HTML 不允许 form 套 form,浏览器丢掉内层标签、保留它的 `</form>`,
    于是**保存按钮成了孤儿**(`button.form === null`),点了不提交也不报错。
    真浏览器实测:修前 `btnInsideForm:false` / 点后 POST=0;修后 true / POST=1。
    改法:附件区改 `<div id="noteAttForm">`,上传按钮用 `formaction`。
  · 详情页宽度:`.note-read { max-width: 820px }` → `none`
    (内容卡 820px vs 下方共享卡 1032px;修后两者都是 1032px)。

⑤ 日程状态下线
  · 删表单里的「状态」下拉、`Calendar::statusWord()`、浮层 JSON 的 `status_word`;
  · **保留** `events.status` 列与 `Calendar::statusOf()`(契约不动,等整体清理)。

验证(新增 5 个套件):
  · `_fb_v47_addr.py`(接线 24 条)/ `_fb_v47_addr_sim.py`(真浏览器 + 假高德,16 条)
  · `_fb_v47_readperf.py`(缓存与会话锁,23 条)
  · `_fb_v47_notes.py`(真浏览器,9 条)
  · `_b47_readperf_falsify.py`(反向验证 9 个注入)—— **9/9 全抓住**
  · 全量 e2e **1510/0**;vCard 规格+往返 40/0;TB 探针 44/0;
    v44 反向验证 17/0;全流水线 **PUSH_RC=0**(含"第47批形态"静态断言)
  · **线上终验**:读书页整页 81ms、封面 103→20ms、media 冷 149→热 35→304 19.8ms、
    修复前第 2 次请求 8.73s → 修复后 0.15s、地址联想 meta[fb-addr] 带真高德 key

顺带修掉三处**探针自身**的假红/假绿(详见 pitfalls.md):
  · `_b44_falsify.py` 的"整小时"注入在**整点**与正确实现等价 ⇒ 换成恒错值;
  · `_fb_b44_ui.py` 3b 的并发计时断言在单进程 `php -S` 下必然假红 ⇒ 改断言确定事实;
  · 多处探针的"清理删除"撞沙箱删除门禁 ⇒ 探针**中途死掉**看起来像产品坏了
    ⇒ 统一改成 `_safe_rm()` / 不删。
2026-10-14 01:20:40 +08:00

85 lines
4.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 地址输入(高德联想 + 详细地址 + 坐标),v24 第 17 批 用户第 3 条。
*
* 用户原话:「出发地和到达地,都用**高德地图的接口**,记录详细地址,
* 这个功能,triplog 也有,如果之前有数据,里面记录了行程的详细地址,
* 也都把数据迁移过来。」
*
* 界面照 TripLog 的 nodes/form.php:
* 一个可见的文本框(人能看到、能改、能自己敲)+ 四个隐藏字段
* (name / address / lng / lat —— 选中联想项后由 JS 填上)。
* **有坐标时**旁边出一个「🧭 导航到这里」链接(高德 URI API)。
*
* ⚠️ 三个必须做对的细节:
* ① **没配 key 也要能用**:SDK 不加载时文本框就是普通输入框,
* 用户手敲地名照样能存(只是没有详细地址与坐标)。
* 降级路径不能是"功能消失",只能是"辅助变少"。
* ② **assoc 用 map 而不是 select**:TripLog 用 AMap.AutoComplete 直接挂到 input 上,
* 能边打边出候选,比"点一下弹搜索框"顺手得多。
* ③ **不要 `<label for>`**:这个片段在一个页面里会出现两次(出发地/到达地),
* for/id 对不上就会互相抢焦点。用包裹式 label。
*
* 用法(require 前设好):
* $locPrefix 前缀('from' | 'to'),决定所有 id 与字段名
* $locLabel 标签文字(「出发地」「到达地」)
* $locIcon 标签前的小图标(可选)
* $locPlaceholder 占位提示
* $locRow 行数据(可选;编辑时回填):name/address/lng/lat 四个键
*/
declare(strict_types=1);
$locPrefix = (string)($locPrefix ?? 'from');
$locLabel = (string)($locLabel ?? '地点');
$locIcon = (string)($locIcon ?? '');
$locPlaceholder = (string)($locPlaceholder ?? '输入地址,如「青岛北站」');
$locRow = is_array($locRow ?? null) ? $locRow : [];
$locName = (string)($locRow['name'] ?? '');
$locAddr = (string)($locRow['address'] ?? '');
$locLng = (string)($locRow['lng'] ?? '');
$locLat = (string)($locRow['lat'] ?? '');
// 导航链接:有坐标走 navigation,没坐标退化成"按名字搜"
$locNav = \App\Geo::navUrl($locLng, $locLat, $locName);
$locId = 'loc-' . $locPrefix;
?>
<div class="loc-field" data-loc="<?= e($locPrefix) ?>">
<label class="loc-label" for="<?= e($locId) ?>-input">
<?php if ($locIcon !== ''): ?><span class="loc-ico"><?= e($locIcon) ?></span><?php endif; ?>
<?= e($locLabel) ?>
</label>
<input class="input loc-input" type="text"
id="<?= e($locId) ?>-input"
name="<?= e($locPrefix) ?>_place"
value="<?= e($locName) ?>"
placeholder="<?= e($locPlaceholder) ?>"
autocomplete="off"
<?php /* 🔴 v47(用户 2026-10-13):地址联想改用**全站共用模块**
(`assets/js/addr_suggest.js`,由 layout.php 按需拉起高德 SDK)。
`data-addr-fill` 列出选中候选后要回填哪几个字段。
⚠️ 字段查找范围限定在**本 .loc-field 内**:一个行程表单里有
from / to 两组,不限定会把出发地的地址写到到达地上。 */ ?>
data-addr data-addr-fill="address,lng,lat,city" data-addr-for=".loc-field"
data-loc-input>
<?php /* 高德没加载 / 用户自己敲的地名:这四个字段就是兜底存放处 */ ?>
<input type="hidden" name="<?= e($locPrefix) ?>_addr" value="<?= e($locAddr) ?>">
<input type="hidden" name="<?= e($locPrefix) ?>_lng" value="<?= e($locLng) ?>">
<input type="hidden" name="<?= e($locPrefix) ?>_lat" value="<?= e($locLat) ?>">
<?php /* 城市由前端选中时带回(高德 addressComponent 直接给城市名),
带不回就由服务端 App\Geo 解析 —— 两条路都通 */ ?>
<input type="hidden" name="<?= e($locPrefix) ?>_city" value="<?= e((string)($locRow['city'] ?? '')) ?>">
<div class="loc-extra">
<span class="loc-detail" data-loc-detail><?= e($locAddr !== '' && $locAddr !== $locName ? $locAddr : '') ?></span>
<a class="loc-nav" data-loc-nav
href="<?= e($locNav !== '' ? $locNav : '#') ?>"
target="_blank" rel="noopener"
<?= $locNav === '' ? 'hidden' : '' ?>>🧭 导航到这里</a>
</div>
</div>
<?php
// 只 unset 本片段自己的变量,宿主模板一个都不碰(与 _datetime.php 同一约定)
unset($locPrefix, $locLabel, $locIcon, $locPlaceholder, $locRow,
$locName, $locAddr, $locLng, $locLat, $locNav, $locId);