79 lines
3.5 KiB
PHP
79 lines
3.5 KiB
PHP
<?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();
|