界面(用户 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。
16 KiB
家书 FamilyBook · 移动端 API(v1)
面向前端(iOS / Android / 小程序)与任何第三方程序。 所有写操作与网页共用同一套业务类,权限规则完全一致 —— App 里建的东西与网页里建的完全一样。
1. 总览
| 项目 | 说明 |
|---|---|
| 根地址 | https://<你的站点>/api/v1 |
| 鉴权 | Authorization: Bearer <token>(见 §2) |
| 请求体 | Content-Type: application/json(也接受 application/x-www-form-urlencoded) |
| 响应 | JSON 信封,Content-Type: application/json; charset=utf-8 |
| 时间 | 请求里两种格式都收:2026-11-03 14:30 与 2026-11-03T14:30:00+08:00(ISO 8601);响应里统一是 YYYY-MM-DD HH:MM:SS(服务器本地时区) |
| 分页 | 列表接口支持 ?page=1&per_page=50;带 paginate=1 时返回 {items,total,page,per_page},不带则返回裸数组 |
1.1 响应信封
成功:
{ "ok": true, "data": <任意> }
失败:
{ "ok": false, "error": "人话错误信息", "code": "bad_request" }
| HTTP | code |
含义 |
|---|---|---|
| 200 | — | 成功 |
| 201 | — | 已创建(data 是新资源) |
| 400 | bad_request |
请求本身不合法 |
| 401 | unauthorized |
缺令牌 / 令牌无效、过期、被吊销 / 账号停用 |
| 403 | forbidden |
令牌有效但没权限(如只读账号写数据) |
| 404 | not_found |
资源不存在,或者不属于你(刻意不区分,不泄露"别人家有没有这个 id") |
| 405 | method_not_allowed |
该资源不支持这个方法 |
| 409 | conflict |
唯一性冲突(如同地方同名影院) |
| 422 | unprocessable |
字段校验不过(缺必填、格式错) |
| 503 | not_installed |
站点还没安装 |
⚠️ 一定要按
ok字段判成败,不要只看 HTTP 码 —— 用 201/200 区分"新建/更新"是约定,但业务成败以ok为准。
1.2 方法模拟(给不方便发 PATCH/DELETE 的客户端)
POST /api/v1/tasks/12
{ ... }
X-HTTP-Method-Override: DELETE
或把 _method 放进请求体:
POST /api/v1/tasks/12
{ "_method": "DELETE" }
两者等价于 DELETE /api/v1/tasks/12。
1.3 连通性自检(不需要令牌)
GET /api/v1/ping
{ "ok": true, "data": { "service": "familybook", "api": "v1", "time": "2026-10-12T08:20:07+08:00" } }
2. 鉴权:拿到并保管令牌
2.1 在网页上签发
个人设置 → App 接口令牌(/index.php?r=settings/me#api):
- 填一个设备名(如"我的手机");
- 点「生成新令牌」;
- 明文只显示这一次,立刻复制到 App 里(库里只存 sha256,取不回来);
- 随时可以逐枚「吊销」,或「吊销全部」。
2.2 客户端怎么带
Authorization: Bearer fb_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- 令牌以
fb_开头,43 位 base64url(约 256 bit 熵)。 - ⚠️ 只走请求头,不支持
?token=这种 query 形式 —— URL 会进日志、浏览器历史、Referer,把令牌放进去等于半公开。 - ⚠️ 令牌与 登录密码、DAV 口令是三样东西,互不代用(吊销 DAV 口令不会影响 App;反之亦然)。
2.3 典型错误
| 症状 | code |
处理 |
|---|---|---|
没带 Authorization |
unauthorized |
检查请求头 |
| 令牌拼错 / 已被吊销 / 已过期 | unauthorized |
让用户重新登录(App 应清掉本地令牌并回到登录态) |
| 账号被管理员停用 | unauthorized |
提示用户联系家人 |
| 只读(guest)账号在写 | forbidden |
App 侧按 GET /me 返回的 role 隐藏写入口 |
3. 接口一览
| 资源 | 路径 | 说明 |
|---|---|---|
| 我 | /me |
当前身份、家庭、模块主题色 |
| 令牌 | /tokens[/{id}] |
查看 / 吊销自己的令牌 |
| 首页 | /home |
聚合:今日日程 + 待办 + 未读数 |
| 名册 | /people[/{id}] |
家庭成员与联系人 |
| 通知 | /notifications |
未读、列表、标记已读、清空 |
| 日程 | /calendar、/calendar/books、/events[/{id}] |
月视图、日历本、日程增删改(含重复三档) |
| 纪念日 | /days[/{id}] |
生日、结婚纪念日… |
| 任务 | /tasks[/{id}] |
看板 / 状态 / 已完成三种视图;状态、搬看板、评论 |
| 观影 | /movies[/{id}]、/cinemas[/{id}] |
观影记录、评价、影评评论、影院 |
| 旅程 | /trips[/{id}]、/trips/{id}/nodes |
旅程与行程节点 |
| 记账 | /ledger[/{id}]、/ledger/{id}/entries |
账本与账目 |
| 资源 | /resources[/{id}] |
证件、账号等资料 |
| 读书 | /books[/{id}]、/books/{id}/notes |
书与读书笔记 |
| 笔记 | /notes[/{id}] |
随手记(带标签) |
⚠️ 不含「导航」模块 —— 那个模块本身就是给网页外链用的,App 侧用不到(用户 2026-10-12 明确)。
4. 逐个接口
4.1 GET /me
{
"ok": true,
"data": {
"id": 1,
"name": "老杨",
"avatar": "https://.../media.php?k=avatars%2F1.jpg",
"role": "管理员",
"family": { "id": 1, "name": "杨家" },
"colors": { "calendar": "#4A7C82", "tasks": "#A84A28", "...": "..." }
}
}
colors 是模块主题色 —— App 顶栏 / 分类色块直接用它,不必自己维护一张表。
4.2 GET /tokens · DELETE /tokens/{id} · DELETE /tokens
{ "ok": true, "data": [ { "id": 3, "label": "我的手机", "created_at": "2026-10-12 08:00:00",
"last_used_at": "2026-10-12 08:20:00", "expires_at": null } ] }
DELETE /tokens/3 吊销单枚;DELETE /tokens(不带 id)吊销全部,返回 {"revoked": 2}。
4.3 GET /home
{
"ok": true,
"data": {
"greeting": "早上好,老杨",
"date": "2026-10-12",
"weekday": "星期一",
"today_events": [ /* 与 /calendar 同形状,只含今天 */ ],
"tasks": [ /* 进行中的 + 已过期的待办 */ ],
"unread": 2
}
}
4.4 名册 /people
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /people?q=&kind=member|contact |
列表(q 搜姓名/拼音/电话) |
| POST | /people |
新建:name(必填)、kind、gender、note、phone |
| GET | /people/{id} |
详情 |
| PATCH | /people/{id} |
改:name/gender/birthday/note/phone(只改给了的) |
| DELETE | /people/{id} |
删除 |
4.5 通知 /notifications
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /notifications |
{unread, items} |
| POST | /notifications/read |
全部标记已读,返回 {marked:N} |
| DELETE | /notifications/{id} |
删一条 |
| DELETE | /notifications |
清空全部 |
4.6 日程
日历本
GET /calendar/books
返回我可见的日历本(带 perm:owner / write / read、kind:custom/movie/trip/lunar/…)。
⚠️
kind != custom的是派生只读本,不能往里写日程。
视图
GET /calendar?from=2026-11-01&to=2026-11-30
返回区间内所有条目(自定义日程 + 观影 + 行程节点 + 农历/节假日/纪念日),
每条带 kind / ref_id / date / time / end_time / all_day / color / calname / href。
kind=lunar的是农历刻度,列表里通常应过滤掉(首页就是这么做的)。
新建
POST /events
{
"calendar_id": 3,
"title": "家长会",
"starts_at": "2026-11-03T14:30:00+08:00",
"ends_at": "2026-11-03 16:00",
"all_day": false,
"location": "学校三楼会议室",
"note": "带成绩单",
"reminder_minutes": [5, 30],
"status": "confirmed",
"freq": "weekly", "rep_end": "count", "rep_count": 3
}
返回 201 + 新日程(含生成的 rrule)。reminder_minutes 只认 5/10/15/30/60/120/240/480/720/1440(分钟),其余忽略。
重复日程既可以给
freq三件套,也可以直接给 RFC 5545 原文:"rrule": "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=6"。freq取值:daily/weekly/monthly/yearly/weekday(传空串=不重复)。rep_end取值:never(一直)/count(配rep_count)/until(配rep_until)。
改 / 删(重复日程的三档范围)
PATCH /events/{id}?scope=once&occ=2026-11-10
DELETE /events/{id}?scope=future&occ=2026-11-10
scope |
含义 |
|---|---|
all(默认) |
整条系列 |
once |
只这一次(改=拆出独立一条;删=把这一天标记为跳过) |
future |
这一次及以后(老系列截断,改的话再从这天起建新系列) |
⚠️
once/future必须带occ(YYYY-MM-DD,用户点开的是哪一天)—— 重复日程每一天都是同一条记录 id,没有occ就定位不到"这一次"。
4.7 纪念日 /days
| 方法 | 路径 | 字段 |
|---|---|---|
| GET | /days |
全部(含名册生日派生的那几条) |
| POST | /days |
title、repeat_type(solar/lunar/once)、month、day、is_leap、kind、note、person_ids |
| GET | /days/{id} |
详情 |
| PATCH | /days/{id} |
同上,只改给了的 |
| DELETE | /days/{id} |
删除 |
4.8 任务 /tasks
三种视图
GET /tasks # 看板视图(默认)
GET /tasks?view=status&status=doing # 跨看板按状态(todo/doing/done)
GET /tasks?view=done&category_id=2 # 已完成(可带 keyword)
看板视图返回:
{
"ok": true,
"data": {
"boards": [
{ "id": 1, "name": "家务", "color": "#A84A28",
"tasks": [ { "id": 9, "title": "...", "status": "todo", "due_at": "...",
"children": [ /* 子任务 */ ], "child_total": 3, "child_done": 1 } ] }
],
"categories": [ { "id": 1, "name": "装修" } ],
"status": { "todo": "待办", "doing": "进行中", "done": "已完成" },
"repeat": { "": "不循环", "day": "每天", "...": "..." }
}
}
写操作
| 方法 | 路径 | 字段 |
|---|---|---|
| POST | /tasks |
title(必填)、note、due_at、repeat_rule(''/day/week/month/year)、parent_id、assignee_id(默认=你)、category_id、board_id、status、shares(共享给谁的 id 数组) |
| PATCH | /tasks/{id} |
同上去改;状态不在这里改 |
| POST | /tasks/{id}/status |
status(todo/doing/done)、position(可选) |
| POST | /tasks/{id}/move |
board_id、before(排在这条之前;0=排到末尾) |
| DELETE | /tasks/{id} |
删除 |
| GET / POST | /tasks/{id}/comments |
评论列表 / 发评论(content) |
⚠️ 状态只能走
/status子动作(与网页一致)—— 那条路还要处理"循环任务的下一次也排上"。 在PATCH里传status会被忽略。
4.9 观影
| 方法 | 路径 | 字段 |
|---|---|---|
| GET | /movies?keyword=&page=&per_page= |
列表 |
| POST | /movies |
title(必填)、starts_at(必填)、ends_at、location、note、info(简介)、cinema_id、attendees(同行人的 id 数组) |
| GET | /movies/{id} |
详情(含影院、同行人、海报、我的评价) |
| PATCH | /movies/{id} |
只改给了的字段 |
| DELETE | /movies/{id} |
删除 |
| POST | /movies/{id}/review |
写/改我的评价:rating(1–10)、content、feelings(数组) |
| GET | /movies/{id}/comments |
影评下的评论 |
| POST | /movies/{id}/comments |
发评论:content、parent_id、target_person |
| GET | /cinemas |
影院列表 |
| POST | /cinemas |
name(必填)、place、address、color(#RRGGBB) |
| DELETE | /cinemas/{id} |
删除 |
4.10 旅程
| 方法 | 路径 | 字段 |
|---|---|---|
| GET | /trips?keyword= |
活动列表 |
| POST | /trips |
title(必填)、note、summary、is_public |
| GET | /trips/{id} |
详情 |
| PATCH | /trips/{id} |
title/note/summary/is_public |
| DELETE | /trips/{id} |
删除(连带各段行程) |
| GET | /trips/{id}/nodes |
行程节点列表 |
| POST | /trips/{id}/nodes |
加一段:title、starts_at、ends_at、place、detail、node_type、status、category、stage(新建才认)、members、travel_mode、vehicle_id、line_name、carrier、trip_no |
| GET | /trips/{id}/summary |
起止汇总、已展示/未成行的段数 |
⚠️ 编辑行程不能改
stage—— 阶段只由"新建"和详情页那几个显式动作决定 (与网页同一条纪律,防止"改着改着实际行程变回计划")。
4.11 记账
| 方法 | 路径 | 字段 |
|---|---|---|
| GET | /ledger |
我的账本(含共享给我的) |
| POST | /ledger |
name(留空自动命名) |
| GET | /ledger/{id} |
账本 + 汇总 |
| DELETE | /ledger/{id} |
删除 |
| GET | /ledger/{id}/entries |
{items, summary} |
| POST | /ledger/{id}/entries |
kind、amount(元,字符串或数字)、title、note、person_id |
kind 取值(与网页一致):
kind |
名称 | 方向 |
|---|---|---|
deposit / fund / stock / insurance / house / car / cash |
存款/基金/股票/保险/房子/车子/现金 | 资产 |
loan / credit / owe / other |
贷款/信用卡/欠款/其它 | 负债 |
amount传元(如"38.50")—— 服务端自己转分,不要预先 ×100。
4.12 资源 /resources
| 方法 | 路径 | 字段 |
|---|---|---|
| GET | /resources?owner_id=&keyword=&sort=used |
列表 |
| POST | /resources |
title(必填)、content、category |
| GET | /resources/{id} |
详情 |
| PATCH | /resources/{id} |
title/content/category |
| DELETE | /resources/{id} |
删除 |
4.13 读书
| 方法 | 路径 | 字段 |
|---|---|---|
| GET | /books?status=&category=&owner_id=&keyword= |
列表 |
| POST | /books |
title(必填)、author、category、status |
| GET | /books/{id} |
详情(含章节) |
| PATCH | /books/{id} |
status(wish/reading/done/paused)、current_page、rating |
| DELETE | /books/{id} |
删除 |
| GET | /books/{id}/notes |
读书笔记 |
| POST | /books/{id}/notes |
content(必填)、kind(quote/note/…)、chapter、loc |
4.14 笔记
| 方法 | 路径 | 字段 |
|---|---|---|
| GET | /notes?keyword=&tag=&page=&per_page= |
{tags, items}(tags 是标签云,供筛选栏) |
| POST | /notes |
title(必填)、body、tags(字符串或数组) |
| GET | /notes/{id} |
详情 |
| PATCH | /notes/{id} |
title/body/tags |
| DELETE | /notes/{id} |
删除 |
5. 给 App 的开发建议
- 启动顺序:
GET /ping(探连通)→ 本地有令牌就GET /me(探令牌是否还有效)→ 无效则回登录/配置页。 - 令牌存储:iOS 存 Keychain,Android 存 EncryptedSharedPreferences;不要放进
UserDefaults/ 明文文件。 - 多设备:每台设备一枚令牌(
label写设备名),这样"手机丢了"只吊销一枚,不影响平板。 - 错误码处理:
- 遇到
401→ 清本地令牌、回到配置页; - 遇到
403且GET /me的role是只读 → 隐藏写入口,别让用户白点。
- 遇到
- 时间:响应里的时间戳是服务器时区的
YYYY-MM-DD HH:MM:SS(不是 UTC、没有时区后缀)。 若设备时区与服务器不同,请按"这是本地时间"解析(DateTimeFormatter不套 UTC)。 - 列表刷新:
/calendar按区间取(月视图一次一个月);其余列表支持page/per_page。 - 图片:接口返回的
avatar/poster等都是完整可访问 URL,直接给图片组件用。
6. 版本与兼容
- 版本号写在路径里(
/api/v1)。不兼容的改动会开 v2,v1 在可见期内保持兼容。 - 新增字段不算破坏性改动(客户端应忽略不认识的键)。
- 本文件是接口的唯一权威来源;实现见
app/Api.php(内核)、app/ApiHandler.php(资源路由)、api.php(入口)。