feat: 完善微信认证功能,新增用户资料更新与token刷新接口

- 新增 userService.js,包含用户认证、资料更新、token 刷新等功能
- 新增 wechatService.js,处理微信API交互,获取openid和手机号
- 新增 appError.js,封装应用错误处理
- 新增 logger.js,提供日志记录功能
- 新增 response.js,统一成功响应格式
- 新增 sanitize.js,提供输入数据清洗功能
- 更新 OpenAPI 文档,描述新增接口及请求响应格式
- 更新 PocketBase 数据库结构,调整用户表字段及索引策略
- 增强错误处理机制,确保错误信息可观测性
- 更新变更记录文档,详细记录本次变更内容
This commit is contained in:
2026-03-24 10:36:19 +08:00
parent d5dc47fc07
commit 02d5686c7b
76 changed files with 1722 additions and 2641 deletions

View File

@@ -0,0 +1,175 @@
# OpenSpec 变更记录PocketBase Hooks 认证链路加固
## 日期
- 2026-03-23
## 范围
本次变更覆盖 `pocket-base/` 下 PocketBase hooks 项目的微信登录、资料更新、token 刷新、认证落库、错误可观测性、索引策略与运行规范。
---
## 一、认证模型调整
### 1. 认证体系
- 保持 PocketBase 原生 auth token 作为唯一正式认证令牌。
- 登录与刷新响应统一通过 `$apis.recordAuthResponse(...)` 返回 PocketBase 原生认证结果。
- `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` 作为微信身份锚点。
- 业务逻辑中不再使用 `users_wx_openid`
- 用户查询、token 刷新、资料更新均基于 auth record 的 `openid`
### 2. auth 集合兼容字段
由于 `tbl_auth_users` 当前为 PocketBase `auth` 集合,登录注册时为兼容 PocketBase 原生 auth 校验,新增以下兼容策略:
- `email` 使用占位格式:`<openid>@wechat.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/wechat/login`
- `POST /api/wechat/profile`
- `POST /api/wechat/refresh-token`
其中:
### `POST /api/wechat/login`
- body 必填:`users_wx_code`
- 自动以微信 code 换取 `openid`
- 若不存在 auth 用户则尝试创建 `tbl_auth_users` 记录
- 成功时返回 PocketBase 原生 token + auth record + meta
### `POST /api/wechat/profile`
-`Authorization`
- 基于当前 auth record 的 `openid` 定位用户
- 服务端用 `users_phone_code` 换取手机号后保存
### `POST /api/wechat/refresh-token`
-`Authorization`
- 直接基于当前 auth record 返回新的 PocketBase 原生 token
---
## 七、当前已知边界
1. `tbl_auth_users` 为 PocketBase `auth` 集合,因此仍受 PocketBase auth 内置规则影响。
2. 自定义字段“除 openid 外均可空”已在脚本层按目标放宽,但 auth 集合结构更新仍可能触发 PocketBase 服务端限制。
3. 若线上仍返回 PocketBase 默认 400需要确保最新 hooks 已部署并重启生效。
---
## 八、归档建议
部署时至少同步以下文件:
- `pocket-base/bai-api-main.pb.js`
- `pocket-base/bai_api_pb_hooks/`
- `script/pocketbase.newpb.js`
并在 PocketBase 环境中执行 schema 同步后重启服务,再进行接口验证。

View File

@@ -0,0 +1,253 @@
openapi: 3.1.0
info:
title: BAI PocketBase Hooks API
description: 基于 PocketBase `bai_api_pb_hooks` 的对外接口文档,可直接导入 Postman。
version: 1.0.0
servers:
- url: https://bai-api.blv-oa.com/pb
description: 生产环境
- url: http://localhost:8090
description: PocketBase 本地环境
tags:
- name: 系统
description: 基础检查接口
- name: 微信认证
description: 基于微信 openid 与 PocketBase 原生 token 的认证接口
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: PocketBaseAuthToken
schemas:
ApiResponse:
type: object
required: [code, msg, data]
properties:
code:
type: integer
example: 200
msg:
type: string
example: 操作成功
data:
type: object
additionalProperties: true
HealthData:
type: object
properties:
status:
type: string
example: healthy
timestamp:
type: string
format: date-time
HelloWorldData:
type: object
properties:
message:
type: string
example: Hello, World!
timestamp:
type: string
format: date-time
status:
type: string
example: success
build_time:
type: string
nullable: true
format: date-time
CompanyInfo:
type: object
nullable: true
additionalProperties: true
UserInfo:
type: object
properties:
pb_id:
type: string
users_id:
type: string
users_type:
type: string
enum: [游客, 注册用户]
users_name:
type: string
users_phone:
type: string
users_phone_masked:
type: string
users_picture:
type: string
openid:
type: string
company_id:
type: string
company:
$ref: '#/components/schemas/CompanyInfo'
created:
type: string
updated:
type: string
PocketBaseAuthResponse:
type: object
properties:
token:
type: string
description: PocketBase 原生 auth token
record:
type: object
description: PocketBase auth record 原始对象
meta:
type: object
properties:
code:
type: integer
example: 200
msg:
type: string
example: 登录成功
data:
type: object
properties:
status:
type: string
enum: [register_success, login_success]
is_info_complete:
type: boolean
user:
$ref: '#/components/schemas/UserInfo'
WechatLoginRequest:
type: object
required: [users_wx_code]
properties:
users_wx_code:
type: string
description: 微信小程序登录临时凭证 code
example: 0a1b2c3d4e5f6g
WechatProfileRequest:
type: object
required: [users_name, users_phone_code, users_picture]
properties:
users_name:
type: string
example: 张三
users_phone_code:
type: string
example: 2b7d9f2e3c4a5b6d7e8f
users_picture:
type: string
example: https://example.com/avatar.png
WechatProfileResponseData:
type: object
properties:
status:
type: string
enum: [update_success]
user:
$ref: '#/components/schemas/UserInfo'
paths:
/api/system/test-helloworld:
post:
tags: [系统]
summary: HelloWorld 测试接口
responses:
'200':
description: 成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/HelloWorldData'
/api/system/health:
post:
tags: [系统]
summary: 健康检查
responses:
'200':
description: 成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/HealthData'
/api/wechat/login:
post:
tags: [微信认证]
summary: 微信登录/注册合一
description: |
使用微信 code 换取 openid。
若 `tbl_auth_users` 中不存在对应用户则自动创建 auth record随后返回 PocketBase 原生 auth token。
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WechatLoginRequest'
responses:
'200':
description: 登录或注册成功
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseAuthResponse'
'400':
description: 参数错误
'415':
description: 请求体必须为 application/json
'429':
description: 重复请求过于频繁
/api/wechat/profile:
post:
tags: [微信认证]
summary: 更新微信用户资料
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WechatProfileRequest'
responses:
'200':
description: 更新成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/WechatProfileResponseData'
'401':
description: token 无效或当前 auth record 缺少 openid
/api/wechat/refresh-token:
post:
tags: [微信认证]
summary: 刷新 PocketBase 原生 token
description: |
当前实现完全基于 PocketBase 原生鉴权,直接从当前 `Authorization` 对应的 auth record 读取 openid 并重新返回原生 auth token。
security:
- bearerAuth: []
responses:
'200':
description: 刷新成功
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseAuthResponse'
'401':
description: token 无效或当前 auth record 缺少 openid
'404':
description: 用户不存在