# 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 <token>`
- 非标准 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` 使用占位格式：
	- 微信用户：`<openid>@wechat.local`
	- 平台用户：`<openid>@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/attachment/list`
- `POST /api/attachment/detail`
- `POST /api/attachment/upload`
- `POST /api/attachment/delete`
- `POST /api/document/list`
- `POST /api/document/detail`
- `POST /api/document/create`
- `POST /api/document/update`
- `POST /api/document/delete`
- `POST /api/document-history/list`

其中平台用户链路补充为：

### `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 }]`

### 附件管理接口

新增 `attachment` 分类接口，统一要求平台管理用户访问：

- `POST /api/attachment/list`
	- 支持按 `attachments_id`、`attachments_filename` 模糊搜索
	- 支持按 `attachments_status` 过滤
	- 返回附件元数据以及 PocketBase 文件流链接 `attachments_url`
- `POST /api/attachment/detail`
	- 按 `attachments_id` 查询单个附件
	- 返回文件流链接与下载链接
- `POST /api/attachment/upload`
	- 使用 `multipart/form-data`
	- 文件字段固定为 `attachments_link`
	- 上传成功后自动生成 `attachments_id`
	- 自动写入 `attachments_owner = 当前用户 openid`
- `POST /api/attachment/delete`
	- 按 `attachments_id` 真删除附件
	- 若该附件已被 `tbl_document.document_image` 或 `document_video` 引用，则拒绝删除

说明：

- `tbl_attachments.attachments_link` 为 PocketBase `file` 字段，保存实际文件本体。
- 对外查询时会额外补充：
	- `attachments_url`
	- `attachments_download_url`

### 文档管理接口

新增 `document` 分类接口，统一要求平台管理用户访问：

- `POST /api/document/list`
	- 支持按 `document_id`、`document_title`、`document_subtitle`、`document_summary`、`document_keywords` 模糊搜索
	- 支持按 `document_status`、`document_type` 过滤
	- 返回时会自动联查 `tbl_attachments`
	- 额外补充：
		- `document_image_url`
		- `document_video_url`
		- `document_image_attachment`
		- `document_video_attachment`
- `POST /api/document/detail`
	- 按 `document_id` 查询单条文档
	- 返回与附件表联动解析后的文件流链接
- `POST /api/document/create`
	- 新增文档
	- `document_id` 可不传，由服务端自动生成
	- `document_image`、`document_video` 必须传入已存在的 `attachments_id`
	- 成功后会写入一条文档操作历史，类型为 `create`
- `POST /api/document/update`
	- 按 `document_id` 更新文档
	- 若传入附件字段，则会校验对应 `attachments_id` 是否存在
	- 成功后会写入一条文档操作历史，类型为 `update`
- `POST /api/document/delete`
	- 按 `document_id` 真删除文档
	- 删除前会写入一条文档操作历史，类型为 `delete`

说明：

- `document_image`、`document_video` 当前保存的是 `attachments_id`，不是 PocketBase 文件字段。
- 文档查询时通过 `attachments_id -> tbl_attachments` 反查实际文件，并返回可直接访问的数据流链接。
- `document_owner` 语义为“上传者 openid”。

### 文档操作历史接口

新增 `document-history` 分类接口，统一要求平台管理用户访问：

- `POST /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`
- 页面实际走的接口链路为：
	- `/api/attachment/upload`
	- `/api/document/create`
	- `/api/document/list`
	- `/api/document/delete`

### 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 阶段性归档基线继续维护。
