[/][/<子动作>]` * 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();