本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/docs/mobile_api插件开发方案_2026-10-11.md。
mobile_api 插件开发方案(2026-10-11,v2 已定稿)
依据:
docs/插件开发规范.md(v1.5)+ 需求指令《mobile_api 插件(对外 API 层)》。与规范冲突处,以规范为准。 本方案只做设计与评估,不写代码(作者已明确:先不着急动手)。
§14 的 6 个待确认问题已全部拍板,结论见 §2 决策表 D13–D16 与 §15 核心改动清单。
阅读顺序:§0 结论速览 → §2 决策表 → §3 架构 → §5.3 缺口处置 → §15 核心改动。
0. 结论速览(先看这段)
目标:给 App / H5 / 第三方客户端提供统一 REST API;核心提供「骨架 + 扩展点」,插件通过 mobile_api_routes 钩子把功能接进来;认证 / 权限 / 限流 / 审计统一在 API 层完成。
三条调研结论直接决定架构:
- 核心已存在
/api路由组(index.php:397起$router->group('/api', …)),内含一批站内内部接口:
/api/vote、/api/upload、/api/fetch-image、/api/captcha、/api/pow、/api/cover-preview、/api/favorites-count、/api/post/{id}/body。
→ 对外 API 与它们共用 /api 前缀,必须不冲突、不误伤(白名单放行,§3.5)。
- 核心
Router能力受限(app/Core/Router.php):只有get()/post()/any()(16–31 行),无put()/delete()/patch();路径参数仅支持单段{name}(addRoute()第 47 行),无法注册多段通配;方法不匹配不回 405。
→ 需求中的 PUT/DELETE /api/threads/{id} 无法用核心 Router 做真 RESTful。
- 架构定案(D1):挂核心钩子
route_before_dispatch(index.php:415,早于路由匹配)做早期拦截 + 自建ApiRouter子路由——命中本插件路由则自行分发(支持 GET/POST/PUT/DELETE/PATCH 与多段{param}),未命中原样放行。
→ 不改核心 Router、支持真 RESTful 与 OPTIONS/CORS 预检、统一中间件链、mobile_api_routes 扩展点自然落地。
唯一的「改核心」动作(已批准,见 §15):核心 Model/Helper 纯新增 3 个方法(通知单条已读、私信会话查询、私信发送)——这是需求「私信不能只收不发」的硬前提,比「功能降级」更值。
1. 目标与非目标
目标
- 统一认证(Bearer:access 短期 + refresh 长期)、统一权限(走 Model 层,钩子自然触发)、统一限流(按 Token)、统一审计。
- 提供需求 §四 全部核心接口(认证 / 版块 / 帖子 / 用户 / 搜索 / 通知 / 私信)。
- 提供插件扩展点
mobile_api_routes,插件自注册路径,认证/限流由 API 层兜底。 - 后台:总开关 / 限流 / CORS / 审计 / Token 管理;前台:设备管理(查看 + 撤销 + 重新生成)。
非目标
- 不做客户端 App/H5(P5,另立项)。
- 不改核心
Router;不改核心业务逻辑。核心仅纯新增 3 个 Model/Helper 方法(§15),零破坏。 - 不与 MCP 合表(D13)。
- 不做 OAuth2 授权码流程(第三方登录属
oidc_provider/third_party_login)。
2. 决策表(D1–D16,全部已拍板)
| # | 议题 | **冻结结论** | 理由 / 落地 |
|---|---|---|---|
| D1 | 路由实现 | 早期拦截 + 自建 ApiRouter(挂 route_before_dispatch) | 核心 Router 无 put/delete、无多段通配(§3.1) |
| D2 | Token 形态 | Bearer:access(2h)+ refresh(30d),前缀 fh_app_ | 需求 §三 |
| D3 | Token 存储 | 只存 sha256 hash,明文仅生成时展示一次;表 api_tokens | §6.1 |
| D4 | CSRF | 对外 API 一律不加 CSRF(纯 Token 认证,非浏览器 Session) | 与 /mcp 同口径 |
| D5 | 限流 | 按 Token(非 IP):写 5 次/分/Token、读 60 次/分/Token | 表 api_rate 固定窗口(§6.5) |
| D6 | CORS | 默认同源;后台可加白名单(**不允许 * 与凭据并存**) | 同源是安全基线;全开 * 在 Token 泄露时风险大(§7) |
| D7 | 权限对齐 | 只走 Model 层 → 等级/积分/审核/版块权限随核心 + 插件钩子自然触发 | 需求 §二-④ |
| D8 | 插件扩展点 | 钩子 mobile_api_routes,传 ['router' => $apiRouter] | §5.4 |
| D9 | API 注册 | 默认关闭;后台开启后强制 PoW(复用 /api/pow) | §6.4 |
| D10 | 封禁校验 | 每次调用校验(auth_login_check 钩子结果 + users.status) | 与核心登录同口径 |
| D11 | 审计 | 关键操作(登录/刷新/发帖/回帖/编辑/删除/撤销)落 api_logs,大字段剔除 | §6.6 |
| D12 | 分期 | P0 + P1 先做;P2 扩展点(示例 = daily_checkin);P3/P4 后续 | §10 |
| D13 | MCP 共用 Token | 先不合表:API 用 fh_app_/api_tokens,MCP 保持 fh_mcp_/mcp_tokens;api_tokens 预留 consumer TEXT DEFAULT 'app' | MCP 刚上线,改它是回归风险;留口子以后可合(§8) |
| D14 | API 登录口径 | 沿用核心口径(锁定计次 + PoW);折中:登录首次要 PoW,后续 refresh 不再要 PoW | 防爆破 + 防机器人,同时避免客户端每次都要解 PoW(§6.3) |
| D15 | 通知/私信缺口 | 走方案 B:核心纯新增 3 个方法(不降级) | 私信是客户端刚需,不能只收不发(§5.3 / §15) |
| D16 | P2 示例插件 | daily_checkin(已存在:plugins/daily_checkin/) | 轻量、有状态、正好验证扩展点 |
3. 架构设计
3.1 为什么是「拦截式」而非「核心 Router 注册式」
| 方案 | 能否真 RESTful | 改动核心 | 结论 |
|---|---|---|---|
A. 核心 Router 显式注册(any() + X-HTTP-Method-Override) | ❌ 只能 GET/POST | 否 | 备选,牺牲规范 |
B. route_before_dispatch 早期拦截 + 自建 ApiRouter | ✅ GET/POST/PUT/DELETE/PATCH | 否 | 采纳(D1) |
route_before_dispatch 在 index.php:415 触发(Plugin::hook('route_before_dispatch', ['router' => $router])),早于 $router->dispatch()(index.php:431)。插件在此读 REQUEST_METHOD + 路径,命中即自行处理并 exit,否则 return 放行。
⚠️ 该钩子会被触发两次(
index.php:415一次、Router::dispatch()内Router.php:93一次)。钩子必须幂等:非/api请求立即return;已处理请求直接exit,永远走不到第二次。
3.2 ApiRouter(自建子路由,仅本插件内)
class ApiRouter {
get/post/put/delete/patch/any($path, $handler) // $path 支持多段 {param}
match($method, $path): ?array // 返回 [handler, params, meta]
}- 路径参数编译为命名捕获组,允许多段(如
/threads/{id}/replies)。 $handler形式:[ControllerClass::class, 'method'](与核心一致,new $class()无 DI)。- 每条路由带元数据:
auth(是否需登录)、scope(read/write)、rate(限流桶名)。
3.3 请求生命周期
HTTP 请求
└─ index.php
├─ route_register(本插件:注册前台 /api-tokens;后台走 admin_route_register)
├─ route_before_dispatch ★
│ └─ MobileApi::intercept()
│ 1) 取 METHOD + PATH,剥离 BASE_PATH
│ 2) 路径不以 /api/ 开头 → return
│ 3) 路径命中核心内部接口白名单 → return(放行,绝不动它们)
│ 4) OPTIONS → 回 204 + CORS 头,exit
│ 5) ApiRouter::match() 未命中 → return(交核心 404)
│ 6) 中间件链:CORS → 总开关 → 认证 → 封禁 → 限流 → scope → 业务 → 审计
│ 7) 统一 JSON 输出,exit
└─ $router->dispatch()(仅非 API 请求走到这里)3.4 中间件链(顺序固定)
- CORS:
Origin校验(D6:默认同源,白名单可配),回Access-Control-*。 - 总开关:
api_enabled关闭 →503。 - 认证:解析
Authorization: Bearer fh_app_…→ sha256 → 查api_tokens→ 校验enabled/expires_at。 - 封禁:
Plugin::isBanned($uid)(auth_login_check钩子 +users.status)→403。 - 限流:
RateLimit::hit($tokenId,$action,$window)→ 超限429。 - scope:写操作要求
scope=write,否则403。 - 业务:调用控制器(内部走 Model 层)。
- 审计:关键操作写
api_logs。
3.5 与既有 /api 组的关系(放行白名单)
本插件内置核心内部接口白名单(写死,防误伤):
/api/vote、/api/upload、/api/fetch-image、/api/captcha、/api/pow、/api/cover-preview、/api/favorites-count、/api/post/{id}/body。
命中白名单 → 立即放行。对外 API 路径与白名单无重叠。规范 §11.34b:/api 无同名物理目录,安全。
4. 插件目录结构
plugins/mobile_api/
├── plugin.json # hooks: init_after / route_register / admin_route_register / layout_head_end
│ # category=integration, icon=mobile, admin_url=/admin/mobile-api
├── Plugin.php # 常量 / configDefaults / DDL / 配置读写 / loadUser / isBanned / class_alias
├── ApiRouter.php # 自建子路由(多段 {param})
├── MobileApi.php # intercept() + 中间件链 + 统一 JSON 输出
├── TokenManager.php # fh_app_ Token 生成/校验/刷新(轮换)/撤销(access+refresh)
├── RateLimit.php # 按 Token 固定窗口限流(api_rate)
├── Audit.php # api_logs 写入 + 大字段剔除
├── ApiResponse.php # {code,message,data} 统一响应 + 错误码
├── Controllers/
│ ├── AuthController.php # login/register/refresh/logout/me
│ ├── CategoryController.php # categories / categories/{id}
│ ├── ThreadController.php # threads CRUD + replies
│ ├── UserController.php # users/{id}、users/{id}/threads、users/{id}/replies、users/me
│ ├── SearchController.php # search
│ ├── NotificationController.php
│ └── MessageController.php
├── hook/
│ ├── init_after.php # 惰性建表兜底(标记文件守卫)
│ ├── route_register.php # 前台 /api-tokens(设备管理)
│ ├── admin_route_register.php# 后台 /mobile-api
│ └── layout_head_end.php # 前台设备管理页引 style.css
├── views/{tokens.php, admin.php}
├── assets/{style.css, script.js}
├── lang/{zh.php, en.php, zh_tw.php}
└── data/ # 独立 SQLite(api_tokens/api_logs/api_rate/api_config)— 不进发布包命名空间:Plugin\MobileApi\…;目录名 mobile_api 含下划线 → Plugin.php 末尾必须 class_alias(Plugin::class, 'Plugin\mobile_api\Plugin', false)(规范 §399-432;否则 activate/uninstall 静默跳过)。
自动加载:Plugin\MobileApi\Controllers\AuthController → mobile_api/Controllers/AuthController.php ✓。
5. 数据模型、接口映射与扩展点
5.1 DDL(落独立库)
api_tokens / api_logs / api_rate 三表照需求 §八原样,另加:
-- 本插件新增:总配置(KV),避免写全局 settings
CREATE TABLE IF NOT EXISTS api_config (
k TEXT PRIMARY KEY, v TEXT NOT NULL
);
-- 建议补充字段(D13 预留 + 设备管理/审计可读性)
-- api_tokens 增列 consumer TEXT NOT NULL DEFAULT 'app' -- D13:为将来与 MCP 合表留口
-- api_tokens 增列 last_ip_hash TEXT NOT NULL DEFAULT '' -- 设备管理展示
api_tokens.token_hash/refresh_hash均存 sha256(明文),绝不存明文。
5.2 接口 → 核心 Model / Helper 映射
| 接口 | 走的核心能力 | 状态 |
|---|---|---|
POST /api/auth/login | User::findByUsername + password_verify + auth_login_check + users.status | ✅(§6.3) |
POST /api/auth/register | User::create() + PoW | ✅(D9:默认关) |
POST /api/auth/refresh / logout | 本插件 TokenManager | ✅ |
GET /api/auth/me | Token→uid 载入(等效 Auth::getCurrentUser()) | ✅ |
GET /api/categories | Category::allOrdered() + getCategoryStats() | ✅ |
GET /api/categories/{id} | Category::allOrdered() 过滤(无 getById) | ✅(插件内筛选) |
GET /api/threads | Thread::getIndexCursor() / getIndexPaginated() | ✅ |
GET /api/threads/{id} | Thread::getById() + fillExternContent() | ✅ |
POST /api/threads | Thread::insert() → 触发 thread_create_after 等 | ✅ |
PUT /api/threads/{id} | Thread::update() + 归属校验 | ✅ |
DELETE /api/threads/{id} | Thread::softDelete() | ✅ |
GET /api/threads/{id}/replies | Post::getByThread() + countByThread() | ✅ |
POST /api/threads/{id}/replies | Post::insert() → 触发 post_create_after | ✅ |
GET /api/users/{id} | User::getProfileById() | ✅ |
GET /api/users/{id}/threads | User::getThreads($uid,$limit) | ✅ |
GET /api/users/{id}/replies | 核心新增 User::getReplies($uid,$limit,$offset) | ✅(§15-①) |
PUT /api/users/me | User::updateProfile($id,$email,$signature) | ✅ |
GET /api/search | Search::search($q,$type,$page,$perPage,$allowedCatIds,$timeRange) | ✅ |
GET /api/notifications | Notification::getList() + getTotal() + getUnreadCount() | ✅ |
POST /api/notifications/{id}/read | 核心新增 Notification::markRead($uid,$ids) | ✅(§15-②) |
GET /api/messages | Message::getInbox() + countInbox()(+ getSent()) | ✅ |
GET /api/messages/{user_id} | 核心新增 Message::getConversation($uid,$peerId,$limit,$offset) | ✅(§15-③) |
POST /api/messages | 核心新增 Message::send($from,$to,$content) | ✅(§15-③) |
5.3 缺口处置(D15 = 方案 B:核心纯新增 3 个方法)
需求 §四 有 3 处接口在核心 Model 层无对应能力;规范禁止插件直写核心表,故按 D15 由核心纯新增 3 个方法(签名与文件见 §15)。核心改动仅新增、不修改既有方法、不影响任何现有调用方。
| 缺口 | 结论 |
|---|---|
| 通知单条已读 | 核心加 Notification::markRead(int $uid, array $ids): int(复用现有 UPDATE 模式,调用方限定 user_id) |
| 私信会话查询 | 核心加 Message::getConversation(int $uid, int $peerId, int $limit, int $offset): array(双向 sender/receiver 组合,按时间排序) |
| 私信发送 | 核心加 Message::send(int $from, int $to, string $content): int(写入 messages 表;与核心 MessageController 同口径) |
5.4 插件扩展点 mobile_api_routes
核心(本插件)触发:
$apiRouter = new ApiRouter();
$apiRouter->get('/categories', [CategoryController::class, 'index']); // …核心接口
\app\Helpers\Plugin::hook('mobile_api_routes', ['router' => $apiRouter]);
$apiRouter->dispatch(); // 统一分发插件订阅(示例:daily_checkin,D16):
// plugins/daily_checkin/hook/mobile_api_routes.php
if (!isset($router)) return;
$router->get('/checkin/status', [\Plugin\DailyCheckin\ApiController::class, 'status']);
$router->post('/checkin', [\Plugin\DailyCheckin\ApiController::class, 'checkin']);- 插件在
plugin.json加一行:"hooks": { "mobile_api_routes": "hook/mobile_api_routes.php" }。 - 插件只注册「路径 + 控制器方法」;认证 / 权限 / 限流 / CORS / 审计全部由 API 层统一处理。
- 插件路径自动挂
/api/之下(/checkin→/api/checkin),避免与核心接口冲突。 - 钩子参数名必须是
$params且可读$router(Plugin::hook()用extract($params, EXTR_SKIP|EXTR_REFS),Plugin.php:715-722)。 - P2 需给
daily_checkin增加:1 个hook/mobile_api_routes.php+ 1 个ApiController.php(复用其现有业务方法),纯增量、不改既有逻辑。
6. 认证体系
6.1 Token 形态
- 明文:
fh_app_+ 32 字节随机(base64url),生成时返回一次,之后只留 sha256。 - access:
expires_at = now + 2h;refresh:refresh_expires_at = now + 30d。 - 刷新即轮换:旧 refresh 换新 access+refresh,旧 refresh 立即失效(防重放,参照 OIDC 插件轮换+复用检测思路)。
6.2 设备管理
- 前台
/api-tokens:列出本用户全部 Token(设备名 / scope / 最近使用 / 过期),可撤销单个、重新生成。 - 明文仅在建/刷新响应出现一次,列表页永不回显。
6.3 登录(D14:沿用核心口径 + 首次 PoW)
① 首次登录(POST /api/auth/login):
客户端先 GET /api/pow 取题 → 提交带 PoW 答案
→ 核心口径校验:失败锁定计次(防爆破,复用核心锁定逻辑)
→ User::findByUsername → password_verify(用户不存在也用陪跑 hash,防时序侧信道)
→ Plugin::hook('auth_login_check', ['user'=>$user]) → $GLOBALS['mod_system_login_blocked']
→ $user['status'] !== 'active' → 拒绝
→ 邮箱未验证(开关开启时)→ 拒绝
→ 通过:签发 access(2h) + refresh(30d)
② 后续续期(POST /api/auth/refresh):
只校验 refresh_token(**不再要求 PoW**)→ 轮换签发新 access+refresh
→ refresh 失效/过期 → 401,客户端回到 ①折中收益:机器人批量爆破被 ① 的 PoW + 锁定计次挡住;正常客户端只在首次登录解一次 PoW,续期无感。
6.4 注册(D9)
- 默认关闭(
api_allow_register=0)。 - 开启后:
GET /api/pow取题 → 带 PoW 提交 →User::create()→ 欢迎通知(Notification::notifyWelcome)。邮箱验证继承核心开关。
6.5 限流(D5)
- 按 Token(非 IP)固定窗口:写 5 次/分/Token、读 60 次/分/Token(表
api_rate,UNIQUE(token_id, action, window_at)UPSERT 原子计数)。 - 后台可调阈值。
6.6 审计(D11)
- 关键操作写
api_logs:登录 / 刷新 / 发帖 / 回帖 / 编辑 / 删除 / 撤销。 - 大字段(
content/body/text)剔除,其余截断(复用 mcp_serverAudit::flatten思路)。
7. CORS(D6:默认同源 + 白名单)
- 默认同源:仅允许与站点同源的
Origin;跨域需后台配置白名单。 - **不允许
*与凭据并存**(若未来需要 Cookie,则强制白名单,绝不放*)。 - 由插件自答
OPTIONS预检(204 +Access-Control-Allow-*),核心 Router 不参与(它本就不支持 OPTIONS)。 - 后台可配:
api_cors_origins(逗号分隔白名单)。
8. 与 MCP 的关系(D13:先不合表)
| 方案 | 说明 | 结论 |
|---|---|---|
| X. 不合表 | API 用 fh_app_/api_tokens;MCP 保持 fh_mcp_/mcp_tokens;api_tokens 预留 consumer 字段 | 采纳 |
| Y. 合表(MCP 改走 API) | MCP 内部改调 API 层,共用 api_tokens | 暂不做 |
理由:MCP 刚上线,改它是回归风险;两者限流/审计/协议语义不同,强合会耦合。api_tokens.consumer TEXT DEFAULT 'app' 预留口子,将来想合再合。用户建两套 Token 是可接受代价。
9. 后台 / 前台页面
后台 /admin/mobile-api(复用 .admin-tabs/.admin-tab-btn,各 tab 独立 CSRF):
- 总开关、限流配置、CORS 配置、审计日志(分页)、Token 管理(列表 + 强制撤销)。
permissions: ["route:admin"],BaseController三重门禁。
前台 /api-tokens(.mn-section 容器):
- 设备管理:Token 列表(设备名/scope/最近使用/过期)、撤销、新建(明文一次性展示)。
- 零内联:
layout_head_end引style.css,页尾引script.js(复制/二次确认)。
10. 分期
| 阶段 | 内容 | 交付 |
|---|---|---|
| P0 | Token 体系(login[PoW]/register[默认关]/refresh/logout/me)+ ApiRouter 骨架 + 中间件链 + 只读接口(categories / threads / users / search) | 可 curl 登录 + 拉列表 |
| P1 | 写接口(发帖 / 回帖 / 编辑 / 删除) | 完整 CRUD |
| P2 | mobile_api_routes 扩展点 + daily_checkin 示例接入 | 扩展点验收 |
| P3 | 通知 / 私信(依赖 §15 三个核心方法) | |
| P4 | 后台管理(限流 / 审计 / CORS / Token 管理) | |
| P5 | 客户端(uni-app / 第三方)—— 另立项 |
先做 P0 + P1。
11. 验收标准
php -l全绿;plugin_case_lint.py通过;i18n 三语键集合+键序一致、零缺失;零内联检查通过。- 真机测试:
curl走通登录(带 PoW) → 拉版块 → 发帖 → 回帖 → 编辑 → 删除,逐条核对统一响应格式与错误码。 - 扩展点测试:
daily_checkin接入后GET /api/checkin/status、POST /api/checkin生效,认证/限流由核心层兜底。 - 安全:Token 明文只出现一次;撤销后立即 401;封禁用户全接口 403;超限 429;CORS 预检 204。
- 核心新增方法:3 个方法单测(§15),确认既有调用方零回归。
- 出包:
cli/release_build.php --type=plugin --name=mobile_api --version=x.x.x9 项门禁全过;投稿包单根、无data/、无根级文件。
12. 风险与规避
| # | 风险 | 级别 | 规避 |
|---|---|---|---|
| 1 | 拦截 /api/* 误伤核心内部接口 | 高 | 显式白名单放行(§3.5),白名单写死 + 单测 |
| 2 | route_before_dispatch 双触发 | 中 | 钩子幂等:非 /api return,命中即 exit |
| 3 | Token 泄露 | 高 | 只存 hash、独立库 data/ 双重拦截、不进包、不入日志 |
| 4 | refresh 重放 | 高 | 刷新即轮换,旧 refresh 作废(§6.1) |
| 5 | 账号枚举时序侧信道 | 中 | 复刻核心陪跑 bcrypt(AuthController.php:30) |
| 6 | 爆破/机器人 | 中 | D14:首次登录 PoW + 锁定计次;D5:按 Token 限流 |
| 7 | 核心新增 3 方法引入回归 | 中 | 纯新增、不改既有方法;加单测 + php -l + 全量回归验收(§15) |
| 8 | 目录名含下划线 → 生命周期静默跳过 | 中 | class_alias 硬要求 + plugin_case_lint.py 门禁 |
| 9 | CORS 配置失误 | 中 | D6 默认同源;* 与凭据互斥(§7) |
| 10 | 缺口被「直写核心表」绕过 | 高 | 禁止直写;走 D15(核心补方法) |
13. 与规范的偏差(需求 vs 规范/现状)
| 项 | 需求 | 规范/现状 | 处理 |
|---|---|---|---|
| PUT/DELETE | 真 RESTful | 核心 Router 不支持 | D1 拦截式自建 ApiRouter(规范未禁止插件自建子路由) |
| 私信会话/发送、通知单条已读 | 需求要求 | 核心 Model 无方法 | D15:核心纯新增 3 方法(唯一「改核心」,已批准,纯新增零破坏) |
| Token 与 MCP 合并 | 需求「待定」 | — | D13:先不合表,预留 consumer |
| API 注册 | 需求列出 | 核心注册含 PoW + 邮箱验证 | D9:默认关闭,开启则强制 PoW |
| CORS | 需求「默认全开」 | 规范安全基线 | D6 修订为默认同源 + 白名单(比需求更严,符合安全优先) |
14. 逐条拍板结果(原待确认 → 已定)
| # | 问题 | **结论** |
|---|---|---|
| ① | 私信/通知缺口 | 走 B:核心纯新增 3 个方法(§5.3 / §15) |
| ② | MCP 共用 Token | 先不合表,预留 consumer 字段(D13) |
| ③ | API 登录口径 | 沿用核心(锁定计次 + PoW);折中:首次登录要 PoW,refresh 不要(D14) |
| ④ | 限流阈值 | 写 5 / 读 60(每 Token,D5) |
| ⑤ | CORS | 默认同源 + 后台白名单(D6) |
| ⑥ | P2 示例插件 | daily_checkin(已存在,D16) |
15. 核心改动清单(唯一「改核心」,纯新增)
均为新增方法,不修改任何既有方法签名与逻辑 → 对现有调用方零影响。需按核心测试口径补单测。
| # | 文件 | 新增方法(建议签名) | 用途 |
|---|---|---|---|
| ① | app/Models/User.php | public function getReplies(int $userId, int $limit = 20, int $offset = 0): array | GET /api/users/{id}/replies |
| ② | app/Helpers/Notification.php | public static function markRead(int $userId, array $ids): int | POST /api/notifications/{id}/read(限定本人) |
| ③ | app/Models/Message.php | public function getConversation(int $userId, int $peerId, int $limit = 20, int $offset = 0): array<br>public function send(int $fromUserId, int $toUserId, string $content): int | GET /api/messages/{user_id}、POST /api/messages |
② 的
markRead与核心AuthController/路由层无关;③ 的send需与核心MessageController的写入口径一致(含长度校验、防自寄、时间戳格式)。若你希望连这 3 个方法也不动核心,则退化为「功能降级」(通知仅全部已读、私信仅收件箱/发件箱列表)——但按 ① 的建议,不降级。
附:关键依据(文件:行)
- 既有
/api组:index.php:397-411 - 钩子触发点:
route_registerindex.php:414;route_before_dispatchindex.php:415与app/Core/Router.php:93;admin_route_registerindex.php:391-393 - Router 能力:
app/Core/Router.php:16-31(get/post/any)、:41-57(不支持多段)、:98-130(无 405) - 登录口径:
app/Controllers/AuthController.php:100-120、陪跑 hash:30 - Model:
app/Models/User.php(create:489 / getProfileById:455 / getThreads:269 / updateProfile:145 / batchGetNames:351)、Thread.php(insert:58 / getById:195 / update:447 / softDelete:686 / getIndexCursor:354)、Post.php(insert:52 / getByThread:365 / countByThread:387 / softDelete:541)、Category.php(allOrdered:50 / getCategoryStats:91)、Message.php(getInbox:17 / getSent:42 / markAsRead:65 / deleteOwned:76) - Helper:
app/Helpers/Notification.php(getList:98 / getTotal:118 / getUnreadCount:131 / markAllRead:167 / deleteByIds:181)、app/Helpers/Search.php:25、app/Helpers/Auth.php(getCurrentUser:29 / requireAdmin:303) - 插件机制:
app/Helpers/Plugin.php(hook:673-728 / db:113 / ensureSchema:150 / requirePermission:235) - MCP 可复用:
plugins/mcp_server/TokenManager.php、Audit.php、Plugin.php - P2 示例:
plugins/daily_checkin/(已存在:plugin.json、CheckinController.php、lang/)