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。
This commit is contained in:
2026-10-12 15:31:03 +08:00
parent 8c651b43eb
commit 075ac6bbeb
21 changed files with 2886 additions and 185 deletions
+79
View File
@@ -0,0 +1,79 @@
<?php
/**
* 🔴 v44(用户 2026-10-12 第 6 条)—— **移动端 API 入口**。
*
* 路径形态:`/api/v1/<资源>[/<id>][/<子动作>]`
* GET /api/v1/ping
* GET /api/v1/me
* GET /api/v1/tasks
* POST /api/v1/tasks
* PATCH /api/v1/tasks/12
* POST /api/v1/tasks/12/status
* DELETE /api/v1/events/9?scope=future&occ=2026-10-20
*
* ⚠️ 鉴权**不在这个文件里**(见 `App\Api::actor()`):
* 这里只负责"把请求交进去",鉴权与出参格式都在 Api 内核里统一做。
* (分散在入口做鉴权,是"某个入口忘了加"的经典温床。)
*
* ⚠️ 与 DAV 那套(dav.php / carddav.php)**完全并列**,互不影响:
* DAV 走 Basic + XML,是给第三方同步客户端的;
* API 走 Bearer + JSON,是给自家 App 的。
*
* ⚠️ `.htaccess` / `router.php` 里各有一条把 `/api/` 交给本文件的重写 ——
* 两条都要有:Apache 线上走 .htaccess,本地 `php -S` 走 router.php。
* (只写一条的话,会出现"本地能调、上线 404"或者反过来。)
*/
declare(strict_types=1);
require __DIR__ . '/app/bootstrap.php';
// App 端常常会发 OPTIONS 预检(跨域或自定义头)—— 直接给 204,别让它落到业务里。
if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'OPTIONS') {
header('Allow: GET, POST, PATCH, PUT, DELETE, OPTIONS');
// ⚠️ 这里**不**开 CORS 通配:API 给自家 App 用,浏览器跨域不是目标场景。
// 真需要时由用户在站点配置里显式加,而不是我们默认放开(放开会让
// "浏览器里的任意页面拿着用户令牌调我们的 API"变得可能)。
http_response_code(204);
exit;
}
if (!\App\Database::hasTables()) {
\App\Api::fail('还没安装。', 'not_installed', 503);
}
/* 🔴 v44:**API 入口自己也要跑一次结构升级** ——
网页那条路(index.php 开头)用"源码指纹"判要不要跑 `Schema::ensure()`;
而 App 可能**先于任何人打开网页**就发请求(新装的库、或手机先连上)。
那时 `api_tokens` 表还不存在 ⇒ 谁都没法签发令牌 ⇒ 死锁。
⚠️ 指纹判据是**每个家庭**一份(`Setting::get($familyId, …)`),
所以这里按"当前令牌属于谁"取家庭;没令牌时退回第 1 个家庭/全局。
⚠️ 与 index.php 那段同样**容错**:升级失败不该把 API 整个挡住
(失败会在下次请求重试,且错误进 error_log)。 */
$schemaFam = 0;
$tok = \App\Api::bearer();
if ($tok !== '') {
$hit = \App\ApiToken::verify($tok);
if ($hit !== null) {
$p = \App\Person::find((int)$hit[0]);
$schemaFam = (int)($p['family_id'] ?? 0);
}
}
try {
if ($schemaFam > 0 ? \App\Schema::needsUpgrade($schemaFam) : true) {
\App\Schema::ensure();
if ($schemaFam > 0) { \App\Schema::markUpgraded($schemaFam); }
}
} catch (\Throwable $e) {
error_log('[familybook] API 侧结构升级失败(已跳过):' . $e->getMessage());
}
/* ⚠️ 走路由 query 传进来的 `r`(`?r=api/v1/tasks`)也要认:
个别老网络库 / 代理会把 PATH_INFO 吃掉,只留 query。
两条入口共用一个 dispatch,不复制逻辑。 */
$viaQuery = trim((string)($_GET['r'] ?? ''));
if ($viaQuery !== '' && strpos($viaQuery, 'api/') === 0) {
// `api/v1/tasks` → `/api/v1/tasks`(与上面那条重写、以及 index.php 的兜底同一口径)
$_SERVER['REQUEST_URI'] = '/api/' . substr($viaQuery, 4);
}
\App\Api::dispatch();