# OpenSpec 变更记录:PocketBase Hooks 认证链路加固 ## 日期 - 2026-03-23 ## 范围 本次变更覆盖 `pocket-base/` 下 PocketBase hooks 项目的微信登录、平台注册/登录、资料更新、token 刷新、认证落库、错误可观测性、索引策略、字典管理、页面辅助操作、健康检查版本探针、OpenAPI 鉴权与统一响应规范。 --- ## 一、认证模型调整 ### 1. 认证体系 - 保持 PocketBase 原生 auth token 作为唯一正式认证令牌。 - 登录与刷新响应统一为项目标准结构:`code`、`msg`、`data`,认证成功时额外返回顶层 `token`。 - `authMethod` 统一使用空字符串 `''`,避免触发不必要的 MFA / login alerts 校验。 ### 2. Header 规则 - 正式认证 Header 为:`Authorization: Bearer ` - 非标准 Header `Open-Authorization` 不属于本项目接口定义。 - `users_wx_openid` Header 已从 active hooks 鉴权链路移除。 --- ## 二、身份字段与数据模型约束 ### 1. openid 作为唯一业务身份锚点 - `tbl_auth_users` 统一保留 `openid` 作为全平台身份锚点。 - 微信用户:`openid = 微信 openid` - 平台用户:`openid = 服务端生成的 GUID` - 业务逻辑中不再使用 `users_wx_openid`。 - 用户查询、token 刷新、资料更新均基于 auth record 的 `openid`。 ### 2. auth 集合兼容字段 由于 `tbl_auth_users` 当前为 PocketBase `auth` 集合,登录注册时为兼容 PocketBase 原生 auth 校验,新增以下兼容策略: - `email` 使用占位格式: - 微信用户:`@wechat.local` - 平台用户:`@manage.local` - 自动生成随机密码 - 自动补齐 `passwordConfirm` 说明: - 占位 `email` 仅用于满足 auth 集合保存条件,不代表用户真实邮箱。 - 业务主身份仍然是 `openid`。 ### 3. 自定义字段可空策略 - `tbl_auth_users` 的自定义字段目标约束为:除 `openid` 外,其余业务字段均允许为空。 - 已将 schema 脚本中的 `user_id` 改为非必填。 - 其余业务字段保持非必填。 --- ## 三、查询与排序修复 ### 1. 移除无意义的 `created` 排序 在 hooks 查询中,以下查询原先使用 `'-created'` 排序: - 按 `openid` 查询用户 - 按 `company_id` 查询公司 - 按 `users_phone` 查询重复手机号 该写法在 PocketBase 当前运行场景下触发: - `invalid sort field "created"` 现已统一移除排序参数,改为空排序字符串,因为这些查询本质上均为精确匹配或去重检查,不依赖排序。 --- ## 四、错误可观测性增强 ### 1. 登录路由显式错误响应 `POST /api/wechat/login` 新增局部 try/catch: - 保留业务状态码 - 返回 `{ code, msg, data }` - 写入 `logger.error('微信登录失败', ...)` ### 2. 全局错误包装顺序修正 - `routerUse(...)` 全局错误包装提前到路由注册前。 - 统一兼容 `err.statusCode` / `err.status`。 ### 3. auth 保存失败透传 新增 `saveAuthUserRecord(record)` 包装 `$app.save(record)`: - 失败时统一抛出 `保存认证用户失败` - 附带 `originalMessage` 与 `originalData` 目的: - 避免 PocketBase 默认 `Something went wrong while processing your request.` 吞掉具体原因。 --- ## 五、数据库索引策略修复 ### 1. users_phone 索引调整 原设计: - `users_phone` 唯一索引 问题: - 新用户注册阶段手机号为空,多个空值会触发唯一约束冲突,导致注册失败。 现调整为: - `users_phone` 普通索引 说明: - 手机号唯一性改由业务逻辑在资料完善阶段校验。 - 允许多个未完善资料用户以空手机号存在。 --- ## 六、接口契约同步结果 当前 active PocketBase hooks 契约如下: - `POST /api/system/test-helloworld` - `POST /api/system/health` - `POST /api/system/refresh-token` - `POST /api/platform/register` - `POST /api/platform/login` - `POST /api/wechat/login` - `POST /api/wechat/profile` - `POST /api/dictionary/list` - `POST /api/dictionary/detail` - `POST /api/dictionary/create` - `POST /api/dictionary/update` - `POST /api/dictionary/delete` 其中平台用户链路补充为: ### `POST /api/platform/register` - body 必填:`users_name`、`users_phone`、`password`、`passwordConfirm`、`users_picture` - 自动生成 GUID 并写入统一身份字段 `openid` - 写入 `users_idtype = ManagePlatform` - 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token` ### `POST /api/platform/login` - body 必填:`login_account`、`password` - 仅允许 `users_idtype = ManagePlatform` - 前端使用邮箱或手机号 + 密码提交 - 服务端先通过 PocketBase `auth-with-password` 校验身份,再由当前 hooks 进程签发正式 token - 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token` 其中: ### `POST /api/wechat/login` - body 必填:`users_wx_code` - 自动以微信 code 换取微信侧 `openid` 并写入统一身份字段 - 若不存在 auth 用户则尝试创建 `tbl_auth_users` 记录 - 写入 `users_idtype = WeChat` - 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token` ### `POST /api/wechat/profile` - 需 `Authorization` - 基于当前 auth record 的 `openid` 定位用户 - 服务端用 `users_phone_code` 换取手机号后保存 ### `POST /api/system/refresh-token` - body 可选:`users_wx_code`(允许为空) - `Authorization` 可选: - 若 token 仍有效:基于当前 auth record 续签 - 若 token 已过期:回退到微信 code 重签流程 - 若 token 已过期且未提供 `users_wx_code`,返回:`token已过期,请上传users_wx_code` - 返回统一结构:`code`、`msg`、`data`,并在顶层额外返回新 `token` - 属于系统级通用认证能力,不限定为微信专属接口 ### 字典管理接口 新增 `dictionary` 分类接口,统一要求平台管理用户访问: - `POST /api/dictionary/list` - 支持按 `dict_name` 模糊搜索 - 返回字典全量信息,并将 `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 组装为 `items` - `POST /api/dictionary/detail` - 按 `dict_name` 查询单条字典 - `POST /api/dictionary/create` - 新增字典,`system_dict_id` 自动生成,`dict_name` 唯一 - `POST /api/dictionary/update` - 按 `original_dict_name` / `dict_name` 更新字典 - `POST /api/dictionary/delete` - 按 `dict_name` 真删除字典 说明: - `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 已统一改为 JSON 字符串持久化,其中 `dict_word_sort_order` 已改为 `text` 类型。 - 查询时统一聚合为:`items: [{ enum, description, sortOrder }]` --- ## 七、页面与运维辅助能力新增 ### 1. PocketBase 页面 新增页面: - `/web` - `/web/dictionary-manage` 页面能力: - 首页支持跳转到子页面 - 字典管理页支持: - Bearer Token 粘贴与本地保存 - `dict_name` 模糊搜索 - 指定字典查询 - 行内编辑基础字段 - 弹窗编辑枚举项 - 新增 / 删除字典 - 返回主页 ### 2. 健康检查版本探针 `POST /api/system/health` 新增: - `data.version` 用途: - 通过修改 `APP_VERSION` 判断 hooks 是否已成功部署并生效 配置来源: - 进程环境变量 `APP_VERSION` - 或 `runtime.js` --- ## 八、OpenAPI 与 Apifox 调试策略调整 ### 1. 统一返回结构 所有对外接口统一返回: - `code` - `msg` - `data` 认证成功类接口额外返回: - `token` 不再返回以下顶层字段: - `record` - `meta` ### 2. 鉴权文档策略 OpenAPI 文档中已取消 `bearerAuth` 鉴权组件与接口级重复 `Authorization` 参数。 统一约定: - 在 Apifox 环境中配置全局 Header:`Authorization: Bearer {{token}}` - 不再依赖文档中的 Bearer 组件自动注入 此举目的是: - 避免接口页重复出现局部 `Authorization` - 统一依赖环境变量完成鉴权注入 --- ## 九、当前已知边界 1. `tbl_auth_users` 为 PocketBase `auth` 集合,因此仍受 PocketBase auth 内置规则影响。 2. 自定义字段“除 openid 外均可空”已在脚本层按目标放宽,但 auth 集合结构更新仍可能触发 PocketBase 服务端限制。 3. 若线上仍返回 PocketBase 默认 400,需要确保最新 hooks 已部署并重启生效。 4. 平台登录通过回源 PocketBase REST 完成密码校验,因此 `POCKETBASE_API_URL` 必须配置为 PocketBase 进程/容器内部可达地址,不应使用外部 HTTPS 域名。 5. Apifox 环境中需自行维护全局 Header:`Authorization: Bearer {{token}}`,否则鉴权接口不会自动携带 token。 --- ## 十、归档建议 部署时至少同步以下文件: - `pocket-base/bai-api-main.pb.js` - `pocket-base/bai-web-main.pb.js` - `pocket-base/bai_api_pb_hooks/` - `pocket-base/bai_web_pb_hooks/` - `pocket-base/spec/openapi.yaml` - `script/pocketbase.js` 并在 PocketBase 环境中执行 schema 同步后重启服务,再进行接口验证。 建议归档后的发布核验顺序: 1. `POST /api/system/health`:确认 `data.version` 2. `POST /api/platform/login`:确认返回统一结构与顶层 `token` 3. `POST /api/dictionary/list`:确认鉴权与字典接口可用 --- ## 十一、归档状态 - 已将本轮字典管理、PocketBase 页面、统一响应、平台登录兼容修复、健康检查版本探针、OpenAPI/Apifox 鉴权策略调整并入本变更记录。 - 本记录可作为当前 PocketBase hooks 阶段性归档基线继续维护。