v28 第 28 批(12 条):名册分组改标签 / 资源合并为两按钮 / 首页定位与主题色 / 笔记富文本双模式

其中 4 条是**上批遗漏的真 bug**(1/4/8/9),根因已逐条定位:

1. 首页日程没定位到红线 —— 真根因:刻度区间**不含当前时刻** → $showNow=false
   → 红线**整个不渲染**,而定位脚本用同一个越界比例算目标,被 clamp 到底部。
   修:把当前小时并进区间端点(现在必然落在区间内)。
2. 「看整月」→「更多日程」+ 日程主题色;3. 「手头在做」→「近期任务」、
   「任务看板」→「全部任务」+ 任务主题色(注入 --hd-c,不再写死白底)。
4. 任务复选框提示还在 —— 真根因:**JS 在点击后**给状态徽标挂 title
   (`badge.setAttribute('title',…)`),上批只去了静态渲染,静态断言永远抓不到。
5. 名册「分组」→「标签」(用户:一个人可能既是小学同学又是初中同学,没法分组):
   新增 person_tags(每人一份、一人多标签),名册改**平铺 + 顶部标签筛选**;
   🔴 迁移用**一次性哨兵**(第一版用 NOT EXISTS,实测被"用户清空标签"绕过 → 旧分组名被灌回)。
6. 名册卡片按钮加语义底色。
7. 资源四按钮 → **编辑 / 删除**两颗:三块内容进一个带页签的弹框(信息/附件/共享)。
8. 财务多选数字不变 —— 真根因:前端发 `kinds=a,b`(逗号串),服务端 `(array)` 收成
   单元素 → 求交为空 → **静默回落整本账**。修:两种形状都收。
9. 微信读书偶发很慢 —— 真根因:syncNow 扫 60 本 × 每本 1~3 次网关调用(单次超时 20s),
   全同步阻塞在列表页请求里。修:总时长预算 + 单次超时 8s + 自动/手动分批。
10. 新增全局可见的联想接口库(6 个实测可用来源)+ 三种新解析类型。
11. 导航右上角「设置」:默认进"添加网址 + 全部网址",四类设置收进 ?set=1 视图。
12. 笔记:段落/清单支持 Markdown(自研渲染器,**不允许原始 HTML**),
    实时预览走服务端(前后端只一份规则);底部附件上传;清单多级;
    列表改**定尺寸卡片网格**(溢出显示摘要、图片缩放下方);按用户要求**去掉标签侧栏**。
This commit is contained in:
2026-10-08 02:20:19 +08:00
parent 404b1afee0
commit 6852b4ee2c
19 changed files with 2273 additions and 587 deletions
+58 -4
View File
@@ -65,6 +65,32 @@ final class WeRead
*/
public const SYNC_TTL = 21600;
/**
* 一次同步的**总时长预算**(秒)。
*
* 🔴🔴 v28 用户第 9 条:「读书,同步微信读书后,感觉有时候点击进来很慢,
* 需要加载很长时间,后来就好了,过一段时间又慢。」
*
* 根因(实测推演,症状完全吻合):
* 自动同步挂在**读书列表页**上(见 index.php 的 books 路由),
* 而 `syncNow()` 最多扫 60 本书,**每一本**都要打 1~3 次网关
* (bookInfo / progress / notes,缺一座就补一次封面还要再下图片)。
* 单次 `call()` 的超时是 **20 秒**(见 call() 的 CURLOPT_TIMEOUT)——
* 于是"6 小时到期的那一次"最坏能挂几分钟:用户点进来就是白屏等待,
* 过后(时间戳已写、间隔未到)再进来只读一个时间戳,立刻就快了;
* 再过 6 小时又是那一下 —— 这正是用户描述的"时快时慢、过一阵又慢"。
*
* ⇒ 修法:给整轮同步一个**总预算**。到点就**停下来、正常返回**,
* 已处理的部分照常落库,剩下的等下一轮接着做(游标从头再来一遍也无妨:
* `matchLocalBook` 与 `store` 都是幂等的,"已存在的会跳过")。
* ⚠️ 为什么设 8 秒:这是"用户能忍的一次页面等待"的量级。
* 真实耗时取决于网关,宁少勿多 —— 慢一点同步完,比页面打不开好得多。
*/
public const SYNC_BUDGET = 8;
/** 每次同步最多扫几本(与预算双保险:预算防"慢",条数防"多") */
public const SYNC_MAX_BOOKS = 25;
// ================================================================
// Key 的存取(加密)
// ================================================================
@@ -140,8 +166,14 @@ final class WeRead
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 8,
// 🔴 v28 用户第 9 条:单次调用超时从 20 秒收到 **8 秒**。
// 同步一轮要打好几次网关,20 秒 × N 次可以直接把页面拖垮;
// 而网关正常时是零点几秒就回的(见 nav_suggest.php 里那批实测:
// 同类接口 0.1~1.6 秒)。8 秒足以覆盖"网关偶发慢",又不会让
// 一次页面请求长时间挂着。慢的代价只是"这一轮少同步几本",
// 下一轮会接着做(幂等)。
CURLOPT_TIMEOUT => 8,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $key,
@@ -685,6 +717,10 @@ final class WeRead
* 绝不让"同步失败"把读书页也打不开 —— 外部依赖不能拖死主流程。
*
* @param bool $force true = 忽略间隔,立刻同步(用户点「立即同步」时用)
* 🔴 v28 用户第 9 条:$force 时给**更大的扫描量**(用户是
* 主动点的,愿意等;而且手动同步本来就该一次做完整)。
* 页面上的自动同步则用小量 —— 它挂在读书列表页上,
* 每多扫一本就多一次"打开页面就等"。
* @return array{ok:bool,ran:bool,error:string,books:int,notes:int,covers:int,at:string}
*/
public static function maybeSync(int $familyId, int $personId, bool $force = false): array
@@ -698,12 +734,16 @@ final class WeRead
if (!$force && $last > 0 && (time() - $last) < self::SYNC_TTL) {
return $empty;
}
// ⚠️ 先记时间再干活:万一下面 20 秒里超时/抛错,也不会在同一个页面
// ⚠️ 先记时间再干活:万一下面某一轮超时/抛错,也不会在同一个页面
// 反复重试("每次打开读书页都卡 20 秒"是最糟的症状)。
// 🔴 v28:现在同步**有总预算**(SYNC_BUDGET),不会再长时间卡住;
// 这条"先记时间"依然要留 —— 它是"网关整个挂掉时别每次访问都重试"的兜底。
UserSetting::set($personId, self::SYNC_AT_KEY, (string)time());
try {
$res = self::syncNow($familyId, $personId);
// 自动同步只扫一小批(页面等待),手动同步扫完整批(用户愿意等)
$limit = $force ? 60 : self::SYNC_MAX_BOOKS;
$res = self::syncNow($familyId, $personId, $limit);
} catch (\Throwable $e) {
return ['ok' => false, 'ran' => true, 'error' => $e->getMessage(),
'books' => 0, 'notes' => 0, 'covers' => 0, 'at' => date('Y-m-d H:i:s')];
@@ -727,6 +767,16 @@ final class WeRead
*/
public static function syncNow(int $familyId, int $personId, int $maxBooks = 60): array
{
/* 🔴 v28 用户第 9 条:**整轮的总时长预算**(见 SYNC_BUDGET 的注释)。
* 到点就停、正常返回已处理的部分,剩下的下一轮继续。
* ⚠️ 用"开始时刻 + 每轮循环都检查"而不是"包一个超时异常":
* PHP 的 curl 超时是按**单次调用**算的,没有"整个函数超时"这种机制;
* 要在中途刹车,只能在循环里自己判。
* ⚠️ 判据放在**每本书的开头**(不是每本书之间某处):一本书内部还会打
* 2~3 次网关,若在书中途判断就会留下半本书(字段补一半)。
* 整本为单位地停,重来时那本书会被幂等地重做一遍。 */
$deadline = microtime(true) + self::SYNC_BUDGET;
$nb = self::notebooks($personId);
if (!$nb['ok']) {
return ['ok' => false, 'error' => (string)$nb['error'], 'books' => 0,
@@ -738,6 +788,10 @@ final class WeRead
if ($scanned >= $maxBooks) {
break;
}
// 总预算到点 → 停在这里(已处理的都已落库;剩下的下一轮再做)
if (microtime(true) >= $deadline) {
break;
}
$title = trim((string)$b['title']);
if ($title === '') {
continue;