Commit Graph
12 Commits
Author SHA1 Message Date
laoyang f927d7fb23 v79 服务端:端上任务面板需要的三个口子
配合鸿蒙端 v79(任务面板 / 任务模块功能补齐),服务端只补三处,
其余全部复用既有路由。

① GET /api/v1/tasks?view=boards —— 只回看板 + 分类元数据,**不带任务**。
   端上原来是从已加载任务里反推看板,那样只能看到"有任务的看板":
   用户刚建好、还没放任务的看板在端上永远选不到(表现为"网页能建,
   端上挪不过去")。

② POST /api/v1/tasks/<id>/attach(multipart 字段 file)与
   DELETE /api/v1/tasks/<id>/attach?attachment_id=<aid> —— 都要先
   requireWrite() 再 Task::canWrite(),两者都回整条任务(端上直接重画)。

③ ApiHandler::taskOut() —— Task::find() 只 SELECT t.*,不含看板名/色
   ⇒ 详情与各动作回包统一补 board_name / board_color_calc(7 个返回点)。

🔴 两条踩过的坑写进注释:
   · Api::dispatch() 只解析「资源/id/子动作」三段,附件 id 不能拼进路径;
   · DELETE 带 JSON body 时 Api::param 读不到(实测),id 必须走 query。

另新增 Attachment::removeFrom():删附件前先确认它确实挂在这条任务上,
拿别条任务的附件 id 来删自己的 → 404(已由 _fb_v79_api.py 钉死)。

验证:php -l 全过、e2e 1558/0、_fb_v79_api.py 26/0、已部署并冒烟。
2026-10-22 16:40:52 +08:00
laoyang 036e208809 v77 服务端:旅程题图上传端点 + 修「PATCH 把题图清空」的真 bug
新增 POST /api/v1/trips/<id>/cover(multipart 字段 cover),
槽位走 ImageSlot 的 cover(1920×1080 / 16:9),与网页版
trips/form.php 同一条 Upload::image 管线;换图顺手删旧对象。

修一个已经发生过的静默数据丢失:端上编辑旅程走
PATCH /api/v1/trips/<id>,那个分支的 $d 里没有 cover_key
⇒ saveDetails() 里 $d['cover_key'] ?? '' 得到空串 ⇒ 题图被写成
NULL。用户在手机上只改了个名字,题图就没了。
⇒ 语义改成「没传 = 不动」,要清空必须显式传空串。

另加 Trip::setCover():换图只动 cover_key 一列,不复用
Trip::update()(那条要求 title 必填、还会重写 summary/is_public,
下次加字段漏一个就又静默清空一次)。

线上已部署并复核:ApiHandler.php / Trip.php 均含新代码、php -l 通过。
2026-10-22 10:14:54 +08:00
laoyang b2b103df0f v68:修好日程「地点」的高德联想(v54 起静默失效)+ 账本四条动作路由
用户 2026-10-20:「网页端添加日程的时候,输入地点,应该调用高德接口,
获得准确的详细地址,现在这个功能不好用了,先修一下。」

## 根因不是 key、不是域名白名单 —— 是插件从来没被拉起来
`layout.php` 里 `fb-amap` meta 给的 SDK 地址**不带 `plugin`**,而加载器优先用它
⇒ `window.AMap.AutoComplete` **恒为 undefined** ⇒ 联想实例建不出来。
更糟的是 `addr_suggest.js` **一进函数就打 `data-addr-bound='1'`**,
于是"绑定失败"被记成"已经绑定过",此后**再也不重试** —— 全链路静默,
界面上就是"输了字什么也不弹"。
(用真浏览器逐环量过才定位:key 有效、`inputtips` 真能返回 10 条候选,
  问题全在我们自己这一侧。)

修法(双保险):
- `layout.php` 新增 `needAutoComplete()`:SDK 到位后 `AMap.plugin('AMap.AutoComplete', cb)`,
  超时也放行;启动顺序改成 **SDK → 插件 → 我们的脚本**。
- `addr_suggest.js` 就绪判据收紧成 `!!(AMap && AMap.AutoComplete)`;
  `data-addr-bound` 改为 **new 成功之后**才打;新增 `ensurePlugin()`
  并监听 `fb-amap-ready` 自愈(插件晚到也能接上)。

## 「准确的详细地址」
日程的地点**只有一栏**(`_event_fields.php` 只有 location),只把名字写回去,
回头要导航/找人还是查不到门牌。⇒ 地点框加 `data-addr-detail="1"`,
选中候选后写「名字 · 详细地址」(名字在前,人认的是名字),
并在这一行下面给一条「高德:省市区 · 门牌」提示。
⚠️ 提示行必须插在**整行之后**,塞进 `.mv-row` 会把 62px 标签列挤掉(v24 踩过)。
⚠️ 详细地址必须**延后一拍**写:AutoComplete 在 select 回调之后还会把 `poi.name`
   塞回输入框,同步写会被它覆盖(第一版就因此白写)。

## 账本:服务端补四条路由
`Ledger::rename / setShare / unshare / shares / updateEntry / removeEntry`
**模型一直都有**,`ApiHandler::ledger()` 却只开了「建账本 / 记一笔 / 删账本」
⇒ 端上只能把「共享给谁」「账本名字」画成灰条,账目也改不了删不掉。
新增:`PATCH /ledger/<id>`(改名)、`/ledger/<id>/shares`(GET/POST/DELETE)、
`PATCH|DELETE /ledger/<id>/entries`(id 走 `Api::int('id')` ——
`Api::dispatch()` 只解析 3 段,`/ledger/3/entries/17` 的第 4 段会被丢掉)。
`Ledger::mine/find` 补 `is_owner`(`my_perm` 只有 read/write,分不出主人);
改共享只允许主人(422)。

## 验证
- `familybook_push.sh` 全量流水线跑到 v47 停在 1 条失败:
  「③ 选中候选后地点被换成规范地名」—— 因为这条旧契约是 `loc == '万象城 CGV'`,
  而我们现在**有意**写成「名字 · 详细地址」。
  ⇒ 断言**升级**(不是放宽):既要求名字在前、又要求门牌进来,比原来更严。
- `_fb_v47_addr_sim.py` 复验 **16 / 0**;
  并做注入验证:把 `data-addr-detail="1"` 删掉 → ③ 立刻变红(证明断言真守着)。
- `_fb_v68_addr.py` 三模式 5/0(`--old` 拆掉两处时候选 0 条、`--one` 只拆 layout 仍 5/0)。
- 线上核对:`/api/v1/ledger/1/shares` 返回 **401**(不是 404,路由已生效)。
2026-10-20 21:35:47 +08:00
laoyang 1bc313adc6 v65+v66 服务端:笔记/纪念日两处数据丢失 + 旅程车辆与节点类型 API
## v65:两处**静默数据丢失**(网页端凑巧没暴露,走 API 的客户端全中)
1. 笔记:`Note::create()` / `Note::update()` 读的是 **`blocks`**,而 `notes()`
   传的是 `body` ⇒ 新建的笔记**只有标题、一个字都没有**;`PATCH` 只改标题时
   更是把**整条正文的块全删掉**(`update()` 是整组替换)。
   ⇒ 新增 `noteBlocksIn()`:请求给 `blocks`/`body` 就按请求替换,
      都没给(只想改标题/标签)就**沿用库里的块**。
2. 纪念日:`Anniversary::find()` 的 SQL 不含 people,原来那个兜底
   `$cur['people'] ?? []` 恒为空 ⇒ **只改标题的 PATCH 会把关联人全清掉**。
   ⇒ 新增 `annivPeopleOf()` 真去查一次。

## v66:旅程的三项设置接口(端上原来无 API 可调)
- `vehicles`:GET / POST / PATCH / DELETE(`/api/v1/vehicles[/<id>]`)
  · `vehicleUsedCount()`:还有行程在用就不让删
  · `vehicleInput($cur)`:请求优先、缺了沿用库里(部分更新不把别的字段清空)
- `nodeTypes`:同上;列表走 `NodeType::all()`(内部自愈补内置类型)
  · 内置类型不让删(`unprocessable` 422 + 服务端原话,不是 500)
- 两条路由**与 trips 平级**(`case 'vehicles':` / `case 'nodetypes':`)——
  `Api::dispatch()` 只解析 `/<res>/<id>/<sub>` **3 段**,写成 `trips/vehicles`
  会被当成列表请求
- `/ping` 的 features 加 `trips_vehicles` / `trips_nodetypes`(端上可探测老服务端)

## 验证
- `php -l app/ApiHandler.php` 无错
- `familybook_push.sh`:部署完成;线上核对 `vehicles: 1` / `nodetypes: 1`,
  无令牌打 `/vehicles` 回 **401**(不是 404 ⇒ 路由真的存在)
- local smoke:v43…v66 全部版本套件逐个绿(含几何/交互/DAV/vCard)
- ⚠️ 流水线最后三步因**工具问题**假红,已修工具并单独复跑全绿(与本次代码无关):
  · `_fb_login_api.py` 收尾删自己的 273 文件副本,撞上执行环境的批量删除钩子
    ⇒ 进程被砍、退出码 1。`familybook_push.sh` 现在 `export FB_NO_PURGE=1`
    (副本改名挪走不真删)⇒ 复跑 **30 / 0**
  · `_fb_v57_falsify.py` 只拷 `ets/`,而守卫 v66 起要读 `resources/…/form_config.json`
    ⇒ 副本基线不绿。改成拷整个 `src/main` ⇒ 复跑 **12 / 0**
  · 另两条单独复跑:设备会话接口 **56 / 0**、设备管理网页渲染 **23 / 0**
2026-10-20 18:32:06 +08:00
laoyang 3459eea8f3 v64:观影豆瓣自动获取 + 海报/多图上传(移动端 API)
端上「记一场」要与网页端同步实现(用户 2026-10-19 六条),
服务端这一半只加**移动端入口**,网页端(index.php 那条路由)一行没动。

- ApiHandler:新增 `/movies/douban` 子路由(action=suggest / action=thumb /
  默认抓详情+海报落盘),逻辑与网页端 `movies/douban` 逐条对齐;
  新增 `/movies/<id>/poster` 与 `/movies/<id>/photos`(multipart 上传),
  字段名与端上一致(poster / photo);
  新增 `normalizeFiles()` 把 `photo[]` 的多文件从"按字段竖切"转成"一行一个";
  `movieData()` 认 `poster_key` 并**校验形态**(只接受 posters/xxx 形式);
  features 声明 movies_douban / movie_media(端上可探测老服务端)。

- Douban:新增 `apiThumbUrl()` —— 端上打不到 `index.php?r=...`,
  把豆瓣图片代理换成 `/api/v1/...` 形式(拼法只此一处);
  联想候选带上**过白名单后**的原始图片地址(未过白名单给空串,不下发 SSRF 跳板)。

- Movie:新增 `setPoster()`(单字段更新 —— 端上是"先建记录再传图"两段式,
  用 update() 要拼一份完整草稿,漏一个字段就静默覆盖用户刚填的内容)与
  `addPhotos()`(**追加**语义,与网页表单用的 savePhotos() 替换语义并存不混用)。

验证:接口 e2e 57/0(自带豆瓣 mock,离线可跑)· 反向验证 18/0 ·
本地全量 e2e 1521/0 · 线上部署 + 站点自检 19 条全过 +
`/api/v1/movies/douban` 401(路由通)· ping.features 已含两条新能力
2026-10-19 23:41:15 +08:00
laoyang 2e40133d8f v62:影院管理补齐接口(统计 / 年度报告 / 高级搜索)+ 名册带工作经历
用户 2026-10-19:「设置里面的影院管理,这些功能都还没有,应该补充,
就是增加影院呢、修改影院呢、高级搜索、统计、年度报告,这些全部都应该补充进去。」

一、三个新子路由(都是**家族级**,必须在 `$id > 0` 分支之前处理)

  GET /api/v1/movies/stats   总览 + 按影院 / 按人 / 按月 / 按年
  GET /api/v1/movies/report  年度报告(以登录者为中心,?year= 可切年)
  GET /api/v1/movies/search  片名 / 影院 / 人 / 起止日期的组合搜索

⚠️ 全部复用**网页端那一份 `MovieStats`**(overview/byCinema/byPerson/byMonth/
   byYear/annual/years/search)—— 统计数字两边必须一致,否则用户会发现
   "网页 124 部、App 121 部"。
⚠️ 三个路由都做了 **`$id === 0` 守卫**:`/movies/stats` 不能被当成"id=0 的详情"。
⚠️ `search` 在条件全空时**不查全库**(返回 has_cond=0),与网页 `hasCond` 同行为 —
   否则"搜索"等于"列出全部观影"。

二、features 声明 movies_stats / movies_report / movies_search
    老端上据此隐藏入口(新端上不用探测,但让能力可发现是这套约定的一部分)。

三、名册带 `jobs`(用户:「名册的列表太空了,应该显示更多内容,例如工作单位、职务」)
    新增 `PersonJob::mapFor(array $personIds)` —— **一次 `IN (…)` 批量取回**。
    ⚠️ 不能在装饰循环里逐人 `PersonJob::list()`:名册上千人就是上千次查询(N+1)。
    ⚠️ 排序与 `list()` 逐字一致(当前段在前 → 开始时间倒序 → id 倒序),
       否则"列表显示的单位"和"详情里第一段"会是两个不同的单位。
    ⚠️ 端上列表实际用的是 `persons.org` / `persons.title`(本地库有那两列),
       `jobs` 是给"接口直出"的场景预备的 —— 两条链路都保留。

验证:_fb_v62_cinema_api.py 45/0(独立副本 + 真 MariaDB);
      覆盖影院 CRUD 10 条 + 统计 8 + 年报 7 + 搜索 7 + 名册带 jobs 4;
      php -l 全量通过。
线上:本提交对应的代码**已在线上**(13:17 那次部署带上去的,非本次操作),
      真机实测统计页显示的是真实数据(横店电影城 4 / 上影影城 3…)。
2026-10-19 15:27:08 +08:00
laoyang ad7f43af68 v61:新增账号自助接口(GET /account + POST /account/password)
端上新增「个人设置」页要用:用户 2026-10-19 要求把抽屉里的
「通知消息 / 个人设置 / 修改密码 / 系统设置」都收进"点右上角头像"那一页,
所以端上必须能自己改密码,不能再指着用户去网页改。

  GET  /api/v1/account            → 我的资料 + 账号信息 + 当前设备
  POST /api/v1/account/password   → 改密码(old_password/password/password2)

规则与网页那条改密码路由逐条对齐(不能端上比网页松):
  · 必须验旧密码;
  · 两次新密码一致;
  · 过 Account::validatePassword()(≥8 位 + 字母数字都要有)。
额外收紧一条:新密码不能与原密码相同(网页没拦,端上拦了没坏处)。

两处刻意为之:
  · 改密码**不查写权限**(网页那条路由只要求登录,guest 也能改自己的密码)
    —— 加了写权限检查就会出现"网页能改、App 不能改"的权限裂缝;
  · 改完**不动现有令牌** —— 用户在自己手机上改密码不该被踹下线。

features 声明补 account / account_password,端上可探测。

验证:_fb_v61_account_api.py 20/0(独立副本 + 自检 + 随机端口 + 真 MariaDB,
覆盖 5 种错误路径 + 改完旧密码登不进/新密码能登/原令牌仍有效);
php -l 全量通过;线上部署已完成。
2026-10-19 12:19:06 +08:00
laoyang 97e963e2ee v59 服务端:标语(个人→家庭→默认)/ 最近纪念日 / 真实头像
配合端上「界面六条」,服务端只补三样数据:

① Family::slogan(fam, pid) —— 三层优先级
   用户:「如果这个用户自己有标语,他可以写一句自己的标语;如果他有自己的标语,
   这个地方就是用户自己自定义的标语。如果他没有标语,就用我们给他的这个标语。」
   优先级:persons.slogan → families.motto → Family::DEFAULT_SLOGAN。
   DEFAULT_SLOGAN = '一家人用岁月写成的一本书'(用户明确指定,别再改)。
   与网页端 templates/home.php 的 heroSlogan 同一套规则,两边永远一致。
   /login 与 /me 都返回 slogan —— 每次启动都会打 /me,所以网页端改了 slogan
   手机下次启动就跟上,不必为此另加接口。

② /home 新增 recent_days —— 首页「最近纪念日」
   用户:「未来最近的三个纪念日,包括每个人的生日,以及手动填写的各个纪念日。」
   = Anniversary::upcoming(fam, pid, 370) 取前 3。
   · 为什么是 370 天而不是网页端概述卡那样的 30 天:只取 30 天时,
     一个"刚过完生日"的家庭会**一个都取不到**,模块直接空着 ——
     而用户要的是一个永远的倒计时。取满一年保证"接下来的三个"一定有。
   · 数据源 all() = 派生生日 + 自建纪念日(正好覆盖用户说的两样,
     不必再去 Birthday:: 那套单独取一遍,否则迟早口径不一)。
   · 每条带 days(倒计天数)、source(custom / birthday_solar / birthday_lunar),
     顺序由服务端排好(最近的在前),端上不再排一遍。

③ 真实头像
   用户:「右上角的头像应该是真实的。」
   · /home 新增 me_avatar(当前登录者的 avatar_url());
   · /login 的 person 增加 avatar_url。
   avatar_url() 负责按线上存储驱动决定走 /uploads 还是 OSS 直链 ——
   端上不自己拼路径(拼错了就是永远破图)。

验证:_fb_home_api.py 60/0(新增 16 条:recent_days 真数据/天数/排序/≤3 条、
me_avatar、slogan 三层优先级实测 —— 给 persons#1 写一句个人 slogan,
必须优先返回他那句);php -l 通过;部署脚本增「第59批形态」9 条。
真机实测:抽屉显示「修身,齐家」= 杨永全自己的 slogan(个人优先生效)。
2026-10-18 20:14:50 +08:00
laoyang 9348dd3796 v58:变更指纹接口(端上增量同步的服务端一半)
用户 2026-10-18:首次全量、之后增量同步;进来先展示本地数据,一边同步
服务器最新数据一边更新(附件走后台队列)。

服务端只补一件事:GET /api/v1/changes —— 每个模块返回一个变更指纹,
端上只做相等比较,不相同才拉。

指纹三件套 COUNT(*) : MAX(id) : MAX(updated_at):
- 61 张表里只有 16 张有 updated_at,单用它抓不到新增与删除;
- 只有行数则抓不到"改已有行"(靠端上 24h 强制全量兜底,响应带 stale_after_ms)。

CHANGE_TABLES 里 home 故意不在(派生页,端上跟着别的模块重拉);
kin 与 roster 同源(persons/relations/nicknames),名册一变家谱跟着拉。

必须带令牌(Api::me() 开头):指纹本身不算机密,但没理由让匿名者试探
"家里有没有新数据"。

验证:_fb_changes_api.py 33/0(新增/修改/删除各自改变指纹、12 模块齐全、
无令牌 401、两设备拿到同一个 all、被踢令牌 401);本地 e2e 1519/0;
部署静态断言补「第58批形态」8 条;全量流水线 PUSH_RC=0。
2026-10-18 16:01:08 +08:00
laoyang ebaed21920 v57:令牌升级为「设备会话」(24h 滑动续期 / 每账号 10 台 / 网页端远程退出)
用户 2026-10-17 定的模型:
  · 一次登录 = 一台设备一条会话;同一台设备重登,旧令牌立即作废
  · 24 小时滑动续期(一直联网就不用每天输密码);离线超 24h 只锁界面、不删数据
  · 网页端「个人设置 → 设备管理」能看见都有谁在登录,并能把丢了的那台踢掉
  · 每账号最多 10 台,超了让用户先踢旧的

服务端配套:
  · api_tokens 加 device_id / device_name / platform / app_version / last_ip
    + idx_api_device;历史令牌回填 legacy-<id>(不回填 = 踢不掉的幽灵会话)
  · ApiToken:issueDevice / listDevices / revokeDevice / revokeOthers / revokeCurrent
    + 滑动续期(剩余不足 4h 才写库);verify 同时返回数字键与关联键(三处调用方在用)
  · 新增 /api/v1/logout、/api/v1/devices;ping 报 features.device_session
  · 登录不再读 days(0 = 永不过期会绕过 24h 这条线);超限回 409 且**不动任何一行**
  · 老客户端兜底固定槽位 auto-unknown(随机设备号会撞满 10 台把账号锁死)
  · Schema 34 → 35

顺带:首页近期任务/往年的今天(Home::memories)、名册派生字段口径统一。

验证:设备会话探针 56/0、设备管理渲染 22/0、登录全链路 30/0、反向验证 12/0、
      e2e 1519/0、静态断言本地空跑 357/0、线上 /api/v1/{ping,devices,logout} 复核通过。
2026-10-18 03:34:47 +08:00
laoyang 13a5d08696 v45 API 补录:手机号登录换令牌 + 我的名册 / 家谱接口 + 出口层敏感字段脱敏
🔴 为什么现在才提交(这是「补录」,代码早已上线并验证):
   v45 第三/第四批当时,同一工作区里有**并行的鸿蒙端工作流**正在改这个文件
   (手机号登录换令牌、media.php 的 ?token=)。按「绝不把别人正在写的裹进
   自己的提交」的纪律,那次只提交了自己的 5 个文件,本文件被**故意留下**。
   此后文件静止约 25 小时、端上工作流也已收尾 ⇒ 现在补录,让仓库与线上一致
   (线上 md5 = 本地 md5,deploy 是从工作树打包的,所以线上一直是这份)。

① `POST /api/v1/login` —— 手机号 + 密码 → API 令牌
   原先 App 只能「先去网页签发令牌、再手动粘贴」,能用但没法给别人用。
   与网页登录有意不同:不建会话(API 无状态);限流落**文件**
   (`sys_get_temp_dir()/fb_login_<sha256(ip|phone)>.json`,60s 窗口 / 5 次)
   而不进 $_SESSION;失败**统一**提示(不区分「没这个人」与「密码错」,
   否则接口成了手机号是否已注册的探测器);有效期用**服务端时间**算。
   成功时顺带 `Calendar::seedDefaults()`(全新安装的兜底)。

② `GET /api/v1/roster` —— 「**我**名册里的人」(我加的 + 别人分享的)
   与 `/people`(全家 801 人 = 通讯录底账)区分开:App 名册页要用这个,
   否则一屏 800 人且是「别人的通讯录」。组装**照抄 Web 端 index.php 的
   people 分支**(同一套类/判据):Person::roster → Kinship::label /
   Nickname::display / PersonQuery::phonePlain / PersonQuery::birthdaysOf /
   PersonTag::mapFor / Pinyin::initials,外加标签云与标签词表。
   ⚠️ 复用这些类而非另写,否则会「网页搜得到、App 搜不到」。

③ `GET /api/v1/kin` —— 家谱(以我为中心的关系图)
   用 `Kinship::familyMap($fam,$pid,$people,2,2)`,与 Web 端 kin/tree.php
   同一套 ⇒ 端上与网页的亲属关系必然一致。按代数排序,拍平直接关系字段。

④ 🔴 信息泄露修复:`stripSensitivePerson()`
   `Person::all()` / `Person::roster()` 都是 `SELECT p.*`,把
   `phone_hash` / `phone_encrypted` / `id_card_encrypted` / `birthday_id_card` /
   `birthday_lunar*` 一路吐给 App。前两个是 AES 密文(离线爆破材料),
   `phone_hash` 是「手机号是否属于这个家」的判定键。
   ⇒ 在 **API 出口层**脱敏(列表 / 详情 / 新建 / PATCH 四个出口都套)。
   ⚠️ **不能改 `Person::all()` 本身**:网页端确实在用(ContactImporter 靠
   phone_hash 去重、PersonQuery 靠 phone_encrypted 按手机号搜)
   ⇒ 网页要、App 不要 ⇒ 差异落在出口层。

线上验证(全绿):GET login→405 / 缺参→422 / 错密码→401「手机号或密码不对」
/ 不存在的手机号→**同样的** 401 与提示 / 连续错→第 6 次起 429 / 名册 800 人 /
标签云 15 个 / 称谓正确(爸爸→父亲…)/ 手机号解密正确 / 生日阴阳历 /
首字母 ypzbb / 6 个敏感字段全去除(列表与详情都验)。
2026-10-14 09:41:34 +08:00
laoyang 075ac6bbeb v44 第 44 批:界面 5 条 + 移动端 API v1 全量
界面(用户 2026-10-12 第 1~5 条)
1. 新建日程「日历本」并入 .cs 自绘下拉,颜色画进候选项,三字与下拉同行。
2. 新建日程「开始/结束」改「日期范围:」,删旧日期控件;
   重复相关三行仅选重复时出现;删「(含第一次)」与「这段时间算忙碌」。
3. 条目浮层加「地点」、去掉「状态」行;首页红线按分钟定位(原只取整小时);
   去掉左侧红点只留红线。
4. 任务页删掉状态说明提示(STATUS_HINT 三值清空)。
5. 新建任务:标题在前树框在后;截止时间改最新日历控件;负责人默认自己;
   删冗余提示;「上面要做什么」→「任务名称」;清理括号说明。

移动端 API(用户第 6 条:导航模块除外,其余全量)
- 新增 Bearer 令牌鉴权(api_tokens 表,Schema v32),与登录口令 / DAV 口令三套分立。
- Api.php 内核(鉴权/取参/信封/分页/分发 + _method 覆写)+
  ApiHandler.php 17 条资源 + ApiToken.php + api.php 入口。
- 路由:router.php / .htaccess / index.php 三处 /api/ 兜底(部署脚本自动追加线上规则)。
- 个人设置新增「App 接口令牌」卡;docs/API.md 接口文档;
  docs/API-QUESTION-LIST.md 自主决策与待确认缺口清单。

验证:本地 e2e 1510/0、v43 真机 33/0、v43 反向 9/0、
v44 真机 41/0、v44 API 63/0、v44 反向 19/0;全流水线 PUSH_RC=0。
2026-10-12 15:31:03 +08:00