# 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 /pb/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 /pb/api/system/test-helloworld` - `POST /pb/api/system/health` - `POST /pb/api/system/refresh-token` - `POST /pb/api/platform/register` - `POST /pb/api/platform/login` - `POST /pb/api/wechat/login` - `POST /pb/api/wechat/profile` - `POST /pb/api/dictionary/list` - `POST /pb/api/dictionary/detail` - `POST /pb/api/dictionary/create` - `POST /pb/api/dictionary/update` - `POST /pb/api/dictionary/delete` - `POST /pb/api/attachment/list` - `POST /pb/api/attachment/detail` - `POST /pb/api/attachment/upload` - `POST /pb/api/attachment/delete` - `POST /pb/api/document/list` - `POST /pb/api/document/detail` - `POST /pb/api/document/create` - `POST /pb/api/document/update` - `POST /pb/api/document/delete` - `POST /pb/api/document-history/list` 其中平台用户链路补充为: ### `POST /pb/api/platform/register` - body 必填:`users_name`、`users_phone`、`password`、`passwordConfirm`、`users_picture` - 自动生成 GUID 并写入统一身份字段 `openid` - 写入 `users_idtype = ManagePlatform` - 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token` ### `POST /pb/api/platform/login` - body 必填:`login_account`、`password` - 仅允许 `users_idtype = ManagePlatform` - 前端使用邮箱或手机号 + 密码提交 - 服务端先通过 PocketBase `auth-with-password` 校验身份,再由当前 hooks 进程签发正式 token - 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token` 其中: ### `POST /pb/api/wechat/login` - body 必填:`users_wx_code` - 自动以微信 code 换取微信侧 `openid` 并写入统一身份字段 - 若不存在 auth 用户则尝试创建 `tbl_auth_users` 记录 - 写入 `users_idtype = WeChat` - 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token` ### `POST /pb/api/wechat/profile` - 需 `Authorization` - 基于当前 auth record 的 `openid` 定位用户 - 服务端用 `users_phone_code` 换取手机号后保存 ### `POST /pb/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 /pb/api/dictionary/list` - 支持按 `dict_name` 模糊搜索 - 返回字典全量信息,并将 `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 组装为 `items` - `POST /pb/api/dictionary/detail` - 按 `dict_name` 查询单条字典 - `POST /pb/api/dictionary/create` - 新增字典,`system_dict_id` 自动生成,`dict_name` 唯一 - `POST /pb/api/dictionary/update` - 按 `original_dict_name` / `dict_name` 更新字典 - `POST /pb/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 }]` ### 附件管理接口 新增 `attachment` 分类接口,统一要求平台管理用户访问: - `POST /pb/api/attachment/list` - 支持按 `attachments_id`、`attachments_filename` 模糊搜索 - 支持按 `attachments_status` 过滤 - 返回附件元数据以及 PocketBase 文件流链接 `attachments_url` - `POST /pb/api/attachment/detail` - 按 `attachments_id` 查询单个附件 - 返回文件流链接与下载链接 - `POST /pb/api/attachment/upload` - 使用 `multipart/form-data` - 文件字段固定为 `attachments_link` - 上传成功后自动生成 `attachments_id` - 自动写入 `attachments_owner = 当前用户 openid` - `POST /pb/api/attachment/delete` - 按 `attachments_id` 真删除附件 - 若该附件已被 `tbl_document.document_image` 或 `document_video` 中的任一附件列表引用,则拒绝删除 说明: - `tbl_attachments.attachments_link` 为 PocketBase `file` 字段,保存实际文件本体。 - 对外查询时会额外补充: - `attachments_url` - `attachments_download_url` ### 文档管理接口 新增 `document` 分类接口,统一要求平台管理用户访问: - `POST /pb/api/document/list` - 支持按 `document_id`、`document_title`、`document_subtitle`、`document_summary`、`document_keywords` 模糊搜索 - 支持按 `document_status`、`document_type` 过滤 - 返回时会自动联查 `tbl_attachments` - 额外补充: - `document_image_urls` - `document_video_urls` - `document_image_attachments` - `document_video_attachments` - `POST /pb/api/document/detail` - 按 `document_id` 查询单条文档 - 返回与附件表联动解析后的多文件流链接 - `POST /pb/api/document/create` - 新增文档 - `document_id` 可不传,由服务端自动生成 - `document_title`、`document_type` 为必填;其余字段均允许为空 - `document_image`、`document_video` 支持传入多个已存在的 `attachments_id` - 成功后会写入一条文档操作历史,类型为 `create` - `POST /pb/api/document/update` - 按 `document_id` 更新文档 - `document_title`、`document_type` 为必填;其余字段均允许为空 - 若传入附件字段,则会校验多个 `attachments_id` 是否都存在 - 成功后会写入一条文档操作历史,类型为 `update` - `POST /pb/api/document/delete` - 按 `document_id` 真删除文档 - 删除前会写入一条文档操作历史,类型为 `delete` 说明: - `document_image`、`document_video` 当前保存的是多个 `attachments_id`,底层以 `|` 分隔文本持久化,不是 PocketBase 文件字段。 - 文档查询时通过 `attachments_id -> tbl_attachments` 反查实际文件,并返回可直接访问的数据流链接数组。 - `document_owner` 语义为“上传者 openid”。 ### 文档操作历史接口 新增 `document-history` 分类接口,统一要求平台管理用户访问: - `POST /pb/api/document-history/list` - 不传 `document_id` 时返回全部文档历史 - 传入 `document_id` 时仅返回该文档历史 - 结果按创建时间倒序排列 说明: - 操作历史表为 `tbl_document_operation_history` - 当前由文档新增、修改、删除接口自动写入 - 主要字段为: - `doh_document_id` - `doh_operation_type` - `doh_user_id` - `doh_current_count` - `doh_remark` --- ## 七、页面与运维辅助能力新增 ### 1. PocketBase 页面 当前页面入口: - `/pb/manage` - `/pb/manage/login` - `/pb/manage/dictionary-manage` - `/pb/manage/document-manage` 页面能力: - 首页支持跳转到子页面 - 字典管理页支持: - Bearer Token 粘贴与本地保存 - `dict_name` 模糊搜索 - 指定字典查询 - 行内编辑基础字段 - 弹窗编辑枚举项 - 新增 / 删除字典 - 返回主页 - 文档管理页支持: - 先上传附件到 `tbl_attachments` - 再把返回的多个 `attachments_id` 写入 `tbl_document.document_image` / `document_video` - 图片和视频都支持多选上传 - 新增文档 - 编辑已有文档并回显多图片、多视频 - 从文档中移除附件并在保存后删除对应附件记录 - 查询文档列表 - 直接展示 PocketBase 文件流链接 - 删除文档 说明: - 原页面 `page-b.js` 已替换为 `document-manage.js` - 页面实际走的接口链路为: - `/pb/api/attachment/upload` - `/pb/api/document/create` - `/pb/api/document/update` - `/pb/api/attachment/delete` - `/pb/api/document/list` - `/pb/api/document/delete` ### 2. 健康检查版本探针 `POST /pb/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 /pb/api/system/health`:确认 `data.version` 2. `POST /pb/api/platform/login`:确认返回统一结构与顶层 `token` 3. `POST /pb/api/dictionary/list`:确认鉴权与字典接口可用 --- ## 十一、归档状态 - 已将本轮字典管理、PocketBase 页面、统一响应、平台登录兼容修复、健康检查版本探针、OpenAPI/Apifox 鉴权策略调整并入本变更记录。 - 本记录可作为当前 PocketBase hooks 阶段性归档基线继续维护。