Files
FamilyBook/api.php
T

79 lines
3.5 KiB
PHP
Raw Normal View History

<?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();