Files
laoyang 075ac6bbeb 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。
2026-10-12 15:31:03 +08:00

16 KiB
Raw Permalink Blame History

家书 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):

  1. 填一个设备名(如"我的手机");
  2. 点「生成新令牌」;
  3. 明文只显示这一次,立刻复制到 App 里(库里只存 sha256,取不回来);
  4. 随时可以逐枚「吊销」,或「吊销全部」。

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 的开发建议

  1. 启动顺序:GET /ping(探连通)→ 本地有令牌就 GET /me(探令牌是否还有效)→ 无效则回登录/配置页。
  2. 令牌存储:iOS 存 Keychain,Android 存 EncryptedSharedPreferences;不要放进 UserDefaults / 明文文件。
  3. 多设备:每台设备一枚令牌(label 写设备名),这样"手机丢了"只吊销一枚,不影响平板。
  4. 错误码处理:
    • 遇到 401 → 清本地令牌、回到配置页;
    • 遇到 403 且 GET /me 的 role 是只读 → 隐藏写入口,别让用户白点。
  5. 时间:响应里的时间戳是服务器时区的 YYYY-MM-DD HH:MM:SS(不是 UTC、没有时区后缀)。 若设备时区与服务器不同,请按"这是本地时间"解析(DateTimeFormatter 不套 UTC)。
  6. 列表刷新:/calendar 按区间取(月视图一次一个月);其余列表支持 page/per_page。
  7. 图片:接口返回的 avatar / poster 等都是完整可访问 URL,直接给图片组件用。

6. 版本与兼容

  • 版本号写在路径里(/api/v1)。不兼容的改动会开 v2,v1 在可见期内保持兼容。
  • 新增字段不算破坏性改动(客户端应忽略不认识的键)。
  • 本文件是接口的唯一权威来源;实现见 app/Api.php(内核)、app/ApiHandler.php(资源路由)、api.php(入口)。