feat: 增强 PocketBase hooks 认证功能
- 更新微信登录、平台注册/登录、资料更新、token 刷新、认证落库等功能,统一使用 openid 作为全平台身份锚点。 - 新增平台用户注册和登录接口,支持手机号和密码认证。 - 实现系统级 token 刷新接口,支持通过微信 code 重新签发 token。 - 新增用户总数查询接口,返回 tbl_auth_users 表中的用户总数。 - 更新 OpenAPI 文档,反映新的接口和数据结构。 - 修改数据库结构,调整字段名称和索引。 - 新增页面示例,展示基本的 HTML 页面结构。
This commit is contained in:
@@ -12,4 +12,4 @@ APP_BASE_URL=https://bai-api.blv-oa.com
|
|||||||
|
|
||||||
# Database Configuration (Pocketbase)
|
# Database Configuration (Pocketbase)
|
||||||
POCKETBASE_API_URL=https://bai-api.blv-oa.com/pb/
|
POCKETBASE_API_URL=https://bai-api.blv-oa.com/pb/
|
||||||
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTc3NDMxNTc3OSwiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.PJqbHcHQ57WLYKQdycA-a96EwI5IFuKM1Mr1o-CNw_g
|
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo
|
||||||
|
|||||||
@@ -11,13 +11,13 @@
|
|||||||
|
|
||||||
| 字段名 | 类型 | 说明 |
|
| 字段名 | 类型 | 说明 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **user_id** | BigInt (PK) | 内部全局唯一 ID |
|
| **users_convers_id** | String | 会话/对话侧用户标识,允许为空 |
|
||||||
| **openid** | String (Unique) | **全局身份锚点**,微信唯一标识 |
|
| **openid** | String (Unique) | **全局身份锚点**,微信唯一标识 |
|
||||||
| **user_name** | String | 姓名/昵称 |
|
| **users_name** | String | 姓名/昵称 |
|
||||||
| **org_id** | Int | 所属组织/部门 ID(影响行级权限的关键属性) |
|
| **org_id** | Int | 所属组织/部门 ID(影响行级权限的关键属性) |
|
||||||
| **rank_level** | Int | 职级/等级(影响动态权限矩阵的关键属性) |
|
| **users_rank_level** | Int | 职级/等级(影响动态权限矩阵的关键属性) |
|
||||||
| **status** | Int | 账户状态 (1: 正常, 0: 禁用) |
|
| **users_status** | Int | 账户状态 (1: 正常, 0: 禁用) |
|
||||||
| **user_type** | Int | 账户类型 (0: 微信小程序,1: 管理平台,2: 其他) |
|
| **users_auth_type** | Int | 账户类型 (0: 微信小程序,1: 管理平台,2: 其他) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
| 字段名 | 类型 | 说明 |
|
| 字段名 | 类型 | 说明 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **id** | BigInt (PK) | 自增 ID |
|
| **id** | BigInt (PK) | 自增 ID |
|
||||||
| **user_id** | BigInt | 用户 ID(关联 `tbl_auth_users`) |
|
| **users_convers_id** | String | 用户会话标识(关联 `tbl_auth_users.users_convers_id`) |
|
||||||
| **res_id** | Int | 资源 ID(关联 `tbl_auth_resources`) |
|
| **res_id** | Int | 资源 ID(关联 `tbl_auth_resources`) |
|
||||||
| **access_level** | Int | 权限值 (0: 无权, 1: 只读, 2: 读写) |
|
| **access_level** | Int | 权限值 (0: 无权, 1: 只读, 2: 读写) |
|
||||||
| **priority** | Int | 优先级(当角色权限与个人设置冲突时,以此为准) |
|
| **priority** | Int | 优先级(当角色权限与个人设置冲突时,以此为准) |
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
| **target_type** | Enum | 目标:`USER` 或 `ROLE` |
|
| **target_type** | Enum | 目标:`USER` 或 `ROLE` |
|
||||||
| **target_id** | BigInt | 对应的 UserID 或 RoleID |
|
| **target_id** | BigInt | 对应的 UserID 或 RoleID |
|
||||||
| **table_name** | String | 作用的表名 |
|
| **table_name** | String | 作用的表名 |
|
||||||
| **filter_sql** | String | 过滤逻辑。例如:`dept_id = {user.org_id}` 或 `creator_id = {user.user_id}` |
|
| **filter_sql** | String | 过滤逻辑。例如:`dept_id = {user.org_id}` 或 `creator_id = {user.users_convers_id}` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -73,13 +73,13 @@
|
|||||||
|
|
||||||
#### 1. 权限计算路径 (Effective Permissions)
|
#### 1. 权限计算路径 (Effective Permissions)
|
||||||
当用户访问某个数据时,系统按照以下顺序合并权限:
|
当用户访问某个数据时,系统按照以下顺序合并权限:
|
||||||
1. **取基础属性**:获取用户的 `org_id` 和 `rank_level`。
|
1. **取基础属性**:获取用户的 `org_id` 和 `users_rank_level`。
|
||||||
2. **取角色权限**:获取该用户所属角色对应的资源权限列表。
|
2. **取角色权限**:获取该用户所属角色对应的资源权限列表。
|
||||||
3. **应用个性化覆盖**:查询 `tbl_auth_user_overrides`。如果该表中有记录,则**覆盖**(或叠加)角色权限。
|
3. **应用个性化覆盖**:查询 `tbl_auth_user_overrides`。如果该表中有记录,则**覆盖**(或叠加)角色权限。
|
||||||
4. **注入行级过滤**:如果是查询操作,解析 `tbl_auth_row_scopes` 中的 `filter_sql`,将 `{user.xxx}` 变量替换为当前用户的真实值。
|
4. **注入行级过滤**:如果是查询操作,解析 `tbl_auth_row_scopes` 中的 `filter_sql`,将 `{user.xxx}` 变量替换为当前用户的真实值。
|
||||||
|
|
||||||
#### 2. 动态更新机制
|
#### 2. 动态更新机制
|
||||||
* **组织/等级变更**:当 `tbl_auth_users` 中的 `org_id` 或 `rank_level` 变化时,由于行级过滤表(`tbl_auth_row_scopes`)引用的是动态变量,**权限会自动生效**,无需重新授权。
|
* **组织/等级变更**:当 `tbl_auth_users` 中的 `org_id` 或 `users_rank_level` 变化时,由于行级过滤表(`tbl_auth_row_scopes`)引用的是动态变量,**权限会自动生效**,无需重新授权。
|
||||||
* **缓存策略**:建议将计算后的“最终权限清单”缓存到 Redis。当用户在后台矩阵页面修改权限,或者发生组织架构调整时,通过 `wx_openid` **主动失效(Purge)** 该用户的 Redis 缓存。
|
* **缓存策略**:建议将计算后的“最终权限清单”缓存到 Redis。当用户在后台矩阵页面修改权限,或者发生组织架构调整时,通过 `wx_openid` **主动失效(Purge)** 该用户的 Redis 缓存。
|
||||||
|
|
||||||
#### 3. 字段级权限实现 (Field-Level)
|
#### 3. 字段级权限实现 (Field-Level)
|
||||||
|
|||||||
190
docs/tbl_auth_tables.md
Normal file
190
docs/tbl_auth_tables.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# tbl_auth 系列表结构说明
|
||||||
|
|
||||||
|
> 数据来源:`script/pocketbase.newpb.js`
|
||||||
|
>
|
||||||
|
> 说明:以下为当前脚本中定义的 `tbl_auth_` 系列 PocketBase 集合结构,不含 PocketBase `auth` 集合的系统内置字段,仅列出脚本中显式声明的自定义字段与索引。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. `tbl_auth_users`
|
||||||
|
|
||||||
|
- 集合类型:`auth`
|
||||||
|
- 说明:认证用户主表,业务身份锚点为 `openid`
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 备注 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `users_convers_id` | `text` | 否 | 会话/对话侧用户 ID |
|
||||||
|
| `openid` | `text` | 是 | 微信身份锚点,唯一索引 |
|
||||||
|
| `org_id` | `number` | 否 | 组织/部门 ID |
|
||||||
|
| `users_rank_level` | `number` | 否 | 等级 |
|
||||||
|
| `users_status` | `number` | 否 | 状态 |
|
||||||
|
| `users_auth_type` | `number` | 否 | 账户类型 |
|
||||||
|
| `users_id` | `text` | 否 | 自定义用户 ID |
|
||||||
|
| `users_name` | `text` | 否 | 用户姓名 |
|
||||||
|
| `users_idtype` | `text` | 否 | 证件类型 |
|
||||||
|
| `users_id_number` | `text` | 否 | 证件号 |
|
||||||
|
| `users_phone` | `text` | 否 | 手机号 |
|
||||||
|
| `users_level` | `text` | 否 | 用户等级 |
|
||||||
|
| `users_type` | `text` | 否 | 用户类型 |
|
||||||
|
| `users_status` | `text` | 否 | 用户状态 |
|
||||||
|
| `company_id` | `text` | 否 | 公司 ID |
|
||||||
|
| `users_parent_id` | `text` | 否 | 上级用户 ID |
|
||||||
|
| `users_promo_code` | `text` | 否 | 推广码 |
|
||||||
|
| `users_id_pic_a` | `file` | 否 | 证件照正面,`maxSelect: 1`,允许 `jpeg/png/webp` |
|
||||||
|
| `users_id_pic_b` | `file` | 否 | 证件照反面,`maxSelect: 1`,允许 `jpeg/png/webp` |
|
||||||
|
| `users_title_picture` | `file` | 否 | 资质照片,`maxSelect: 1`,允许 `jpeg/png/webp` |
|
||||||
|
| `users_picture` | `text` | 否 | 用户头像 |
|
||||||
|
| `usergroups_id` | `text` | 否 | 用户组 ID |
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_auth_users_users_convers_id` | UNIQUE INDEX | `users_convers_id` |
|
||||||
|
| `idx_tbl_auth_users_openid` | UNIQUE INDEX | `openid` |
|
||||||
|
| `idx_tbl_auth_users_org_id` | INDEX | `org_id` |
|
||||||
|
| `idx_tbl_auth_users_users_rank_level` | INDEX | `users_rank_level` |
|
||||||
|
| `idx_tbl_auth_users_users_status` | INDEX | `users_status` |
|
||||||
|
| `idx_tbl_auth_users_users_auth_type` | INDEX | `users_auth_type` |
|
||||||
|
| `idx_tbl_auth_users_users_phone` | INDEX | `users_phone` |
|
||||||
|
| `idx_tbl_auth_users_company_id` | INDEX | `company_id` |
|
||||||
|
| `idx_tbl_auth_users_usergroups_id` | INDEX | `usergroups_id` |
|
||||||
|
| `idx_tbl_auth_users_users_parent_id` | INDEX | `users_parent_id` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `tbl_auth_resources`
|
||||||
|
|
||||||
|
- 集合类型:`base`
|
||||||
|
- 说明:受控资源定义表
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 备注 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `res_id` | `text` | 是 | 资源 ID |
|
||||||
|
| `table_name` | `text` | 是 | 表名 |
|
||||||
|
| `column_name` | `text` | 否 | 字段名 |
|
||||||
|
| `res_type` | `text` | 是 | 资源类型 |
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_auth_resources_res_id` | UNIQUE INDEX | `res_id` |
|
||||||
|
| `idx_tbl_auth_resources_table_name` | INDEX | `table_name` |
|
||||||
|
| `idx_tbl_auth_resources_res_type` | INDEX | `res_type` |
|
||||||
|
| `idx_tbl_auth_resources_unique_res` | UNIQUE INDEX | `table_name, column_name, res_type` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `tbl_auth_roles`
|
||||||
|
|
||||||
|
- 集合类型:`base`
|
||||||
|
- 说明:角色定义表
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 备注 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `role_id` | `text` | 是 | 角色 ID |
|
||||||
|
| `role_name` | `text` | 是 | 角色名称 |
|
||||||
|
| `role_code` | `text` | 否 | 角色编码 |
|
||||||
|
| `role_status` | `number` | 否 | 角色状态 |
|
||||||
|
| `role_remark` | `text` | 否 | 备注 |
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_auth_roles_role_id` | UNIQUE INDEX | `role_id` |
|
||||||
|
| `idx_tbl_auth_roles_role_name` | UNIQUE INDEX | `role_name` |
|
||||||
|
| `idx_tbl_auth_roles_role_code` | UNIQUE INDEX | `role_code` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `tbl_auth_role_perms`
|
||||||
|
|
||||||
|
- 集合类型:`base`
|
||||||
|
- 说明:角色权限映射表
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 备注 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `role_perm_id` | `text` | 是 | 角色权限记录 ID |
|
||||||
|
| `role_id` | `text` | 是 | 角色 ID |
|
||||||
|
| `res_id` | `text` | 是 | 资源 ID |
|
||||||
|
| `access_level` | `number` | 是 | 权限级别 |
|
||||||
|
| `priority` | `number` | 否 | 优先级 |
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_auth_role_perms_role_perm_id` | UNIQUE INDEX | `role_perm_id` |
|
||||||
|
| `idx_tbl_auth_role_perms_role_id` | INDEX | `role_id` |
|
||||||
|
| `idx_tbl_auth_role_perms_res_id` | INDEX | `res_id` |
|
||||||
|
| `idx_tbl_auth_role_perms_unique_map` | UNIQUE INDEX | `role_id, res_id` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. `tbl_auth_user_overrides`
|
||||||
|
|
||||||
|
- 集合类型:`base`
|
||||||
|
- 说明:用户个性化权限覆盖表
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 备注 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `override_id` | `text` | 是 | 覆盖记录 ID |
|
||||||
|
| `users_convers_id` | `text` | 是 | 用户会话标识 |
|
||||||
|
| `res_id` | `text` | 是 | 资源 ID |
|
||||||
|
| `access_level` | `number` | 是 | 权限级别 |
|
||||||
|
| `priority` | `number` | 否 | 优先级 |
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_auth_user_overrides_override_id` | UNIQUE INDEX | `override_id` |
|
||||||
|
| `idx_tbl_auth_user_overrides_users_convers_id` | INDEX | `users_convers_id` |
|
||||||
|
| `idx_tbl_auth_user_overrides_res_id` | INDEX | `res_id` |
|
||||||
|
| `idx_tbl_auth_user_overrides_unique_map` | UNIQUE INDEX | `users_convers_id, res_id` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. `tbl_auth_row_scopes`
|
||||||
|
|
||||||
|
- 集合类型:`base`
|
||||||
|
- 说明:行级权限范围表
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 备注 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `scope_id` | `text` | 是 | 范围记录 ID |
|
||||||
|
| `target_type` | `text` | 是 | 目标类型 |
|
||||||
|
| `target_id` | `text` | 是 | 目标 ID |
|
||||||
|
| `table_name` | `text` | 是 | 作用表名 |
|
||||||
|
| `filter_sql` | `editor` | 是 | 行级过滤表达式 |
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_auth_row_scopes_scope_id` | UNIQUE INDEX | `scope_id` |
|
||||||
|
| `idx_tbl_auth_row_scopes_target_type` | INDEX | `target_type` |
|
||||||
|
| `idx_tbl_auth_row_scopes_target_id` | INDEX | `target_id` |
|
||||||
|
| `idx_tbl_auth_row_scopes_table_name` | INDEX | `table_name` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
1. `tbl_auth_users` 是 `auth` 集合,除上表字段外,还会受 PocketBase auth 系统字段与认证配置影响。
|
||||||
|
2. 当前文档仅以 `script/pocketbase.newpb.js` 为准,不代表线上数据库已经 100% 同步成功。
|
||||||
|
3. 若你需要,我可以继续帮你再生成一份“更像数据库设计说明书”的版本,增加字段含义、业务用途、关联关系三列。
|
||||||
@@ -31,9 +31,11 @@ pocket-base/
|
|||||||
|
|
||||||
- `POST /api/system/test-helloworld`
|
- `POST /api/system/test-helloworld`
|
||||||
- `POST /api/system/health`
|
- `POST /api/system/health`
|
||||||
|
- `POST /api/system/refresh-token`
|
||||||
|
- `POST /api/platform/register`
|
||||||
|
- `POST /api/platform/login`
|
||||||
- `POST /api/wechat/login`
|
- `POST /api/wechat/login`
|
||||||
- `POST /api/wechat/profile`
|
- `POST /api/wechat/profile`
|
||||||
- `POST /api/wechat/refresh-token`
|
|
||||||
|
|
||||||
> 当前自定义路由统一使用 `/api/...` 前缀。
|
> 当前自定义路由统一使用 `/api/...` 前缀。
|
||||||
|
|
||||||
@@ -43,6 +45,37 @@ pocket-base/
|
|||||||
- `Open-Authorization` 不是本项目接口定义的 Header,如调试工具里出现,通常是工具全局预设,应删除。
|
- `Open-Authorization` 不是本项目接口定义的 Header,如调试工具里出现,通常是工具全局预设,应删除。
|
||||||
- `users_wx_openid` Header 已移除,不再需要客户端额外传递。
|
- `users_wx_openid` Header 已移除,不再需要客户端额外传递。
|
||||||
- 当前用户身份以 PocketBase auth record 中的 `openid` 字段为准。
|
- 当前用户身份以 PocketBase auth record 中的 `openid` 字段为准。
|
||||||
|
- `openid` 现已定义为**全平台统一身份锚点**:
|
||||||
|
- 微信用户:`openid = 微信 openid`
|
||||||
|
- 平台用户:`openid = 服务端生成的 GUID`
|
||||||
|
- 当前登录/注册成功后返回的 `token` 为 PocketBase 原生 auth token,可直接用于 PocketBase SDK 与本项目 hooks 接口调用。
|
||||||
|
|
||||||
|
## 平台用户与微信用户说明
|
||||||
|
|
||||||
|
### 平台用户
|
||||||
|
|
||||||
|
- 注册接口:`POST /api/platform/register`
|
||||||
|
- 登录接口:`POST /api/platform/login`
|
||||||
|
- 平台用户注册时会自动生成 GUID 并写入 `tbl_auth_users.openid`
|
||||||
|
- 同时写入 `users_idtype = ManagePlatform`
|
||||||
|
- 平台登录对前端暴露为 `users_phone + password`
|
||||||
|
- 服务端内部仍使用 PocketBase 原生 password auth,以确保返回原生 `token`
|
||||||
|
|
||||||
|
### 通用认证能力
|
||||||
|
|
||||||
|
- 刷新 token 接口:`POST /api/system/refresh-token`
|
||||||
|
- 该接口属于系统级通用认证接口,不区分微信用户或平台用户
|
||||||
|
- body 中 `users_wx_code` 允许为空
|
||||||
|
- 服务端会优先验证 `Authorization`:若 token 仍有效,直接续签,不调用微信接口
|
||||||
|
- 仅当 token 失效时,且提供了 `users_wx_code`,才走微信 code 重签流程(逻辑独立于 `/api/wechat/login` 的完整返回)
|
||||||
|
- 若 token 失效且未提供 `users_wx_code`,返回:`token已过期,请上传users_wx_code`
|
||||||
|
- 返回体为精简结构,仅返回新 `token`
|
||||||
|
|
||||||
|
### 微信用户
|
||||||
|
|
||||||
|
- 登录/注册接口:`POST /api/wechat/login`
|
||||||
|
- 微信用户以微信 code 换取微信侧 openid,并写入统一 `tbl_auth_users.openid`
|
||||||
|
- 首次注册时写入 `users_idtype = WeChat`
|
||||||
|
|
||||||
## 部署方式
|
## 部署方式
|
||||||
|
|
||||||
@@ -126,10 +159,12 @@ PocketBase JSVM 不是 Node.js 运行时:
|
|||||||
本次变更重点包括:
|
本次变更重点包括:
|
||||||
|
|
||||||
- 微信登录链路错误显式返回
|
- 微信登录链路错误显式返回
|
||||||
|
- 平台用户注册/登录接口补充完成
|
||||||
|
- 刷新 token 接口调整到 system 分类
|
||||||
- `recordAuthResponse` 使用空 `authMethod`
|
- `recordAuthResponse` 使用空 `authMethod`
|
||||||
- active hooks 中移除 `-created` 排序
|
- active hooks 中移除 `-created` 排序
|
||||||
- `users_phone` 索引由唯一改为普通索引
|
- `users_phone` 索引由唯一改为普通索引
|
||||||
- `tbl_auth_users` 以 `openid` 为业务身份锚点
|
- `tbl_auth_users` 以全平台统一 `openid` 为业务身份锚点
|
||||||
- auth 集合兼容占位 `email`、随机密码与 `passwordConfirm`
|
- auth 集合兼容占位 `email`、随机密码与 `passwordConfirm`
|
||||||
|
|
||||||
## 与原项目关系
|
## 与原项目关系
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ routerUse(function (e) {
|
|||||||
|
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/hello.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/hello.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/health.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/health.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/users-count.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/refresh-token.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/platform/login.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/platform/register.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/login.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/login.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/profile.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/profile.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/refresh-token.js`)
|
|
||||||
|
|||||||
3
pocket-base/bai-web-main.pb.js
Normal file
3
pocket-base/bai-web-main.pb.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
require(`${__hooks}/bai_web_pb_hooks/pages/index.js`)
|
||||||
|
require(`${__hooks}/bai_web_pb_hooks/pages/page-a.js`)
|
||||||
|
require(`${__hooks}/bai_web_pb_hooks/pages/page-b.js`)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
routerAdd('POST', '/api/platform/login', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validatePlatformLoginBody(e)
|
||||||
|
const data = userService.authenticatePlatformUser(payload)
|
||||||
|
|
||||||
|
$apis.recordAuthResponse(e, data.authRecord, data.authMethod, data.meta)
|
||||||
|
return
|
||||||
|
} catch (err) {
|
||||||
|
const status =
|
||||||
|
(err && typeof err.statusCode === 'number' && err.statusCode)
|
||||||
|
|| (err && typeof err.status === 'number' && err.status)
|
||||||
|
|| 400
|
||||||
|
|
||||||
|
logger.error('平台登录失败', {
|
||||||
|
status: status,
|
||||||
|
message: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
|
||||||
|
return e.json(status, {
|
||||||
|
code: status,
|
||||||
|
msg: (err && err.message) || '平台登录失败',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
routerAdd('POST', '/api/platform/register', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validatePlatformRegisterBody(e)
|
||||||
|
const data = userService.registerPlatformUser(payload)
|
||||||
|
|
||||||
|
$apis.recordAuthResponse(e, data.authRecord, data.authMethod, data.meta)
|
||||||
|
return
|
||||||
|
} catch (err) {
|
||||||
|
const status =
|
||||||
|
(err && typeof err.statusCode === 'number' && err.statusCode)
|
||||||
|
|| (err && typeof err.status === 'number' && err.status)
|
||||||
|
|| 400
|
||||||
|
|
||||||
|
logger.error('平台注册失败', {
|
||||||
|
status: status,
|
||||||
|
message: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
|
||||||
|
return e.json(status, {
|
||||||
|
code: status,
|
||||||
|
msg: (err && err.message) || '平台注册失败',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
routerAdd('POST', '/api/system/refresh-token', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validateSystemRefreshBody(e)
|
||||||
|
|
||||||
|
if (e.auth && e.auth.collection().name === 'tbl_auth_users' && e.auth.getString('openid')) {
|
||||||
|
const openid = e.auth.getString('openid')
|
||||||
|
const data = userService.refreshAuthToken(openid)
|
||||||
|
const token = userService.issueAuthToken(data.authRecord)
|
||||||
|
|
||||||
|
return e.json(200, {
|
||||||
|
code: 200,
|
||||||
|
msg: '刷新成功',
|
||||||
|
data: {
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.users_wx_code) {
|
||||||
|
return e.json(401, {
|
||||||
|
code: 401,
|
||||||
|
msg: 'token已过期,请上传users_wx_code',
|
||||||
|
data: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData = userService.authenticateWechatUser(payload)
|
||||||
|
const token = userService.issueAuthToken(authData.authRecord)
|
||||||
|
|
||||||
|
return e.json(200, {
|
||||||
|
code: 200,
|
||||||
|
msg: '刷新成功',
|
||||||
|
data: {
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const status =
|
||||||
|
(err && typeof err.statusCode === 'number' && err.statusCode)
|
||||||
|
|| (err && typeof err.status === 'number' && err.status)
|
||||||
|
|| 400
|
||||||
|
|
||||||
|
logger.error('系统刷新令牌失败', {
|
||||||
|
status: status,
|
||||||
|
message: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
|
||||||
|
return e.json(status, {
|
||||||
|
code: status,
|
||||||
|
msg: (err && err.message) || '刷新失败',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
routerAdd('POST', '/api/system/users-count', function (e) {
|
||||||
|
const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
const pageSize = 500
|
||||||
|
let page = 1
|
||||||
|
let total = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const records = $app.findRecordsByFilter(
|
||||||
|
'tbl_auth_users',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
pageSize,
|
||||||
|
(page - 1) * pageSize
|
||||||
|
)
|
||||||
|
|
||||||
|
const count = Array.isArray(records) ? records.length : 0
|
||||||
|
total += count
|
||||||
|
|
||||||
|
if (count < pageSize) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(e, '查询成功', {
|
||||||
|
total_users: total,
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,11 +4,11 @@ routerAdd('POST', '/api/wechat/profile', function (e) {
|
|||||||
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
||||||
|
|
||||||
guards.requireJson(e)
|
guards.requireJson(e)
|
||||||
const authState = guards.requireWechatAuth(e)
|
const authState = guards.requireAuthUser(e)
|
||||||
guards.duplicateGuard(e)
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
const payload = guards.validateProfileBody(e)
|
const payload = guards.validateProfileBody(e)
|
||||||
const data = userService.updateWechatUserProfile(authState.usersWxOpenid, payload)
|
const data = userService.updateWechatUserProfile(authState.openid, payload)
|
||||||
|
|
||||||
return success(e, '信息更新成功', data)
|
return success(e, '信息更新成功', data)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
routerAdd('POST', '/api/wechat/refresh-token', function (e) {
|
|
||||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
|
||||||
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
|
||||||
|
|
||||||
const usersWxOpenid = guards.requireWechatOpenid(e)
|
|
||||||
const data = userService.refreshWechatToken(usersWxOpenid)
|
|
||||||
|
|
||||||
$apis.recordAuthResponse(e, data.authRecord, data.authMethod, data.meta)
|
|
||||||
return
|
|
||||||
})
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
APP_BASE_URL=https://bai-api.blv-oa.com
|
APP_BASE_URL=https://bai-api.blv-oa.com
|
||||||
POCKETBASE_API_URL=https://bai-api.blv-oa.com/pb/
|
POCKETBASE_API_URL=https://bai-api.blv-oa.com/pb/
|
||||||
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTc3NDEwMTc4NiwiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.67DCKhqRQYF3ClPU_9mBgON_9ZDEy-NzqTeS50rGGZo
|
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo
|
||||||
|
|
||||||
#正式服
|
#正式服
|
||||||
#WECHAT_APPID=wx3bd7a7b19679da7a
|
#WECHAT_APPID=wx3bd7a7b19679da7a
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module.exports = {
|
|||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
APP_BASE_URL: 'https://bai-api.blv-oa.com',
|
APP_BASE_URL: 'https://bai-api.blv-oa.com',
|
||||||
POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb/',
|
POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb/',
|
||||||
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTc3NDEwMTc4NiwiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.67DCKhqRQYF3ClPU_9mBgON_9ZDEy-NzqTeS50rGGZo',
|
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo',
|
||||||
WECHAT_APPID: 'wx3bd7a7b19679da7a',
|
WECHAT_APPID: 'wx3bd7a7b19679da7a',
|
||||||
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
|
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
|
||||||
/* WECHAT_APPID: 'wx42e9add0f91af98b',
|
/* WECHAT_APPID: 'wx42e9add0f91af98b',
|
||||||
|
|||||||
@@ -31,7 +31,50 @@ function validateProfileBody(e) {
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireWechatOpenid(e) {
|
function validatePlatformRegisterBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
|
||||||
|
if (!payload.users_name) throw createAppError(400, 'users_name 为必填项')
|
||||||
|
if (!payload.users_phone) throw createAppError(400, 'users_phone 为必填项')
|
||||||
|
if (!payload.password) throw createAppError(400, 'password 为必填项')
|
||||||
|
if (!payload.passwordConfirm) throw createAppError(400, 'passwordConfirm 为必填项')
|
||||||
|
if (!payload.users_picture) throw createAppError(400, 'users_picture 为必填项')
|
||||||
|
|
||||||
|
if (payload.password !== payload.passwordConfirm) {
|
||||||
|
throw createAppError(400, 'password 与 passwordConfirm 不一致')
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePlatformLoginBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
|
||||||
|
if (!payload.users_phone) throw createAppError(400, 'users_phone 为必填项')
|
||||||
|
if (!payload.password) throw createAppError(400, 'password 为必填项')
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateSystemRefreshBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
|
||||||
|
if (typeof payload.users_wx_code === 'undefined' || payload.users_wx_code === null) {
|
||||||
|
return {
|
||||||
|
users_wx_code: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload.users_wx_code !== 'string') {
|
||||||
|
throw createAppError(400, 'users_wx_code 类型错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
users_wx_code: payload.users_wx_code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAuthOpenid(e) {
|
||||||
if (!e.auth) {
|
if (!e.auth) {
|
||||||
throw createAppError(401, '认证令牌无效或已过期')
|
throw createAppError(401, '认证令牌无效或已过期')
|
||||||
}
|
}
|
||||||
@@ -48,7 +91,7 @@ function requireWechatOpenid(e) {
|
|||||||
return openid
|
return openid
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireWechatAuth(e) {
|
function requireAuthUser(e) {
|
||||||
const authHeader = e.request.header.get('Authorization') || ''
|
const authHeader = e.request.header.get('Authorization') || ''
|
||||||
const parts = authHeader.split(' ')
|
const parts = authHeader.split(' ')
|
||||||
|
|
||||||
@@ -70,7 +113,7 @@ function requireWechatAuth(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
usersWxOpenid: authOpenid,
|
openid: authOpenid,
|
||||||
authRecord: e.auth,
|
authRecord: e.auth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +135,10 @@ module.exports = {
|
|||||||
requireJson,
|
requireJson,
|
||||||
validateLoginBody,
|
validateLoginBody,
|
||||||
validateProfileBody,
|
validateProfileBody,
|
||||||
requireWechatOpenid,
|
validatePlatformRegisterBody,
|
||||||
requireWechatAuth,
|
validatePlatformLoginBody,
|
||||||
|
validateSystemRefreshBody,
|
||||||
|
requireAuthOpenid,
|
||||||
|
requireAuthUser,
|
||||||
duplicateGuard,
|
duplicateGuard,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const wechatService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/servic
|
|||||||
|
|
||||||
const GUEST_USER_TYPE = '游客'
|
const GUEST_USER_TYPE = '游客'
|
||||||
const REGISTERED_USER_TYPE = '注册用户'
|
const REGISTERED_USER_TYPE = '注册用户'
|
||||||
|
const WECHAT_ID_TYPE = 'WeChat'
|
||||||
|
const MANAGE_PLATFORM_ID_TYPE = 'ManagePlatform'
|
||||||
const mutationLocks = {}
|
const mutationLocks = {}
|
||||||
|
|
||||||
function buildUserId() {
|
function buildUserId() {
|
||||||
@@ -16,6 +18,10 @@ function buildUserId() {
|
|||||||
return 'U' + date + suffix
|
return 'U' + date + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildGuid() {
|
||||||
|
return $security.randomString(8) + '-' + $security.randomString(4) + '-' + $security.randomString(4) + '-' + $security.randomString(4) + '-' + $security.randomString(12)
|
||||||
|
}
|
||||||
|
|
||||||
function maskPhone(phone) {
|
function maskPhone(phone) {
|
||||||
const value = phone || ''
|
const value = phone || ''
|
||||||
if (!value || value.length < 7) return ''
|
if (!value || value.length < 7) return ''
|
||||||
@@ -51,6 +57,13 @@ function findUserByOpenid(usersWxOpenid) {
|
|||||||
return records.length ? records[0] : null
|
return records.length ? records[0] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findUserByPhone(usersPhone) {
|
||||||
|
const records = $app.findRecordsByFilter('tbl_auth_users', 'users_phone = {:phone}', '', 1, 0, {
|
||||||
|
phone: usersPhone,
|
||||||
|
})
|
||||||
|
return records.length ? records[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
function getCompanyByCompanyId(companyId) {
|
function getCompanyByCompanyId(companyId) {
|
||||||
if (!companyId) return null
|
if (!companyId) return null
|
||||||
const records = $app.findRecordsByFilter('tbl_company', 'company_id = {:companyId}', '', 1, 0, {
|
const records = $app.findRecordsByFilter('tbl_company', 'company_id = {:companyId}', '', 1, 0, {
|
||||||
@@ -71,14 +84,24 @@ function enrichUser(userRecord) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pb_id: userRecord.id,
|
pb_id: userRecord.id,
|
||||||
users_id: userRecord.getString('users_id') || userRecord.getString('user_id'),
|
users_convers_id: userRecord.getString('users_convers_id'),
|
||||||
|
users_id: userRecord.getString('users_id'),
|
||||||
|
users_idtype: userRecord.getString('users_idtype'),
|
||||||
|
users_id_number: userRecord.getString('users_id_number'),
|
||||||
users_type: userRecord.getString('users_type') || GUEST_USER_TYPE,
|
users_type: userRecord.getString('users_type') || GUEST_USER_TYPE,
|
||||||
users_name: userRecord.getString('users_name') || userRecord.getString('user_name'),
|
users_name: userRecord.getString('users_name'),
|
||||||
|
users_status: userRecord.get('users_status'),
|
||||||
|
users_rank_level: userRecord.get('users_rank_level'),
|
||||||
|
users_auth_type: userRecord.get('users_auth_type'),
|
||||||
users_phone: userRecord.getString('users_phone'),
|
users_phone: userRecord.getString('users_phone'),
|
||||||
users_phone_masked: maskPhone(userRecord.getString('users_phone')),
|
users_phone_masked: maskPhone(userRecord.getString('users_phone')),
|
||||||
|
users_level: userRecord.getString('users_level'),
|
||||||
users_picture: userRecord.getString('users_picture'),
|
users_picture: userRecord.getString('users_picture'),
|
||||||
openid: openid,
|
openid: openid,
|
||||||
company_id: companyId || '',
|
company_id: companyId || '',
|
||||||
|
users_parent_id: userRecord.getString('users_parent_id'),
|
||||||
|
users_promo_code: userRecord.getString('users_promo_code'),
|
||||||
|
usergroups_id: userRecord.getString('usergroups_id'),
|
||||||
company: exportCompany(companyRecord),
|
company: exportCompany(companyRecord),
|
||||||
created: String(userRecord.created || ''),
|
created: String(userRecord.created || ''),
|
||||||
updated: String(userRecord.updated || ''),
|
updated: String(userRecord.updated || ''),
|
||||||
@@ -114,11 +137,17 @@ function ensureAuthIdentity(record) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureUserId(record) {
|
||||||
|
if (!record.getString('users_id')) {
|
||||||
|
record.set('users_id', buildUserId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveAuthUserRecord(record) {
|
function saveAuthUserRecord(record) {
|
||||||
try {
|
try {
|
||||||
$app.save(record)
|
$app.save(record)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw createAppError(400, '保存微信用户失败', {
|
throw createAppError(400, '保存认证用户失败', {
|
||||||
originalMessage: (err && err.message) || '未知错误',
|
originalMessage: (err && err.message) || '未知错误',
|
||||||
originalData: (err && err.data) || {},
|
originalData: (err && err.data) || {},
|
||||||
})
|
})
|
||||||
@@ -154,11 +183,11 @@ function authenticateWechatUser(payload) {
|
|||||||
|
|
||||||
const collection = $app.findCollectionByNameOrId('tbl_auth_users')
|
const collection = $app.findCollectionByNameOrId('tbl_auth_users')
|
||||||
const record = new Record(collection)
|
const record = new Record(collection)
|
||||||
record.set('user_id', buildUserId())
|
|
||||||
record.set('users_id', record.getString('user_id'))
|
|
||||||
record.set('openid', openid)
|
record.set('openid', openid)
|
||||||
|
ensureUserId(record)
|
||||||
|
record.set('users_idtype', WECHAT_ID_TYPE)
|
||||||
record.set('users_type', GUEST_USER_TYPE)
|
record.set('users_type', GUEST_USER_TYPE)
|
||||||
record.set('user_auth_type', 0)
|
record.set('users_auth_type', 0)
|
||||||
ensureAuthIdentity(record)
|
ensureAuthIdentity(record)
|
||||||
saveAuthUserRecord(record)
|
saveAuthUserRecord(record)
|
||||||
|
|
||||||
@@ -184,6 +213,112 @@ function authenticateWechatUser(payload) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerPlatformUser(payload) {
|
||||||
|
return withUserLock('platform-register:' + payload.users_phone, function () {
|
||||||
|
const existingPhoneUsers = $app.findRecordsByFilter('tbl_auth_users', 'users_phone = {:phone}', '', 10, 0, {
|
||||||
|
phone: payload.users_phone,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < existingPhoneUsers.length; i += 1) {
|
||||||
|
if (existingPhoneUsers[i].getString('users_phone') === payload.users_phone) {
|
||||||
|
throw createAppError(400, '手机号已被注册')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformOpenid = buildGuid()
|
||||||
|
const existingOpenid = findUserByOpenid(platformOpenid)
|
||||||
|
if (existingOpenid) {
|
||||||
|
throw createAppError(500, '生成平台用户唯一标识失败,请稍后重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection = $app.findCollectionByNameOrId('tbl_auth_users')
|
||||||
|
const record = new Record(collection)
|
||||||
|
|
||||||
|
record.set('openid', platformOpenid)
|
||||||
|
ensureUserId(record)
|
||||||
|
record.set('users_idtype', MANAGE_PLATFORM_ID_TYPE)
|
||||||
|
record.set('users_name', payload.users_name)
|
||||||
|
record.set('users_phone', payload.users_phone)
|
||||||
|
record.set('users_picture', payload.users_picture)
|
||||||
|
record.set('users_id_number', payload.users_id_number || '')
|
||||||
|
record.set('users_level', payload.users_level || '')
|
||||||
|
record.set('users_type', payload.users_type || REGISTERED_USER_TYPE)
|
||||||
|
record.set('company_id', payload.company_id || '')
|
||||||
|
record.set('users_parent_id', payload.users_parent_id || '')
|
||||||
|
record.set('users_promo_code', payload.users_promo_code || '')
|
||||||
|
record.set('usergroups_id', payload.usergroups_id || '')
|
||||||
|
record.set('users_auth_type', 0)
|
||||||
|
record.set('email', platformOpenid + '@manage.local')
|
||||||
|
record.setPassword(payload.password)
|
||||||
|
record.set('passwordConfirm', payload.passwordConfirm)
|
||||||
|
|
||||||
|
saveAuthUserRecord(record)
|
||||||
|
|
||||||
|
const user = enrichUser(record)
|
||||||
|
|
||||||
|
logger.info('平台注册用户成功', {
|
||||||
|
users_id: user.users_id,
|
||||||
|
openid: user.openid,
|
||||||
|
users_idtype: user.users_idtype,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'register_success',
|
||||||
|
is_info_complete: isInfoComplete(record),
|
||||||
|
user: user,
|
||||||
|
authRecord: record,
|
||||||
|
authMethod: '',
|
||||||
|
meta: buildAuthMeta({
|
||||||
|
status: 'register_success',
|
||||||
|
is_info_complete: isInfoComplete(record),
|
||||||
|
user: user,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function authenticatePlatformUser(payload) {
|
||||||
|
return withUserLock('platform-login:' + payload.users_phone, function () {
|
||||||
|
const userRecord = findUserByPhone(payload.users_phone)
|
||||||
|
if (!userRecord) {
|
||||||
|
throw createAppError(404, '平台用户不存在')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userRecord.getString('users_idtype') !== MANAGE_PLATFORM_ID_TYPE) {
|
||||||
|
throw createAppError(400, '当前手机号对应的不是平台用户')
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = userRecord.getString('email')
|
||||||
|
if (!identity) {
|
||||||
|
throw createAppError(500, '平台用户缺少原生登录标识')
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData = $apis.recordAuthWithPassword('tbl_auth_users', identity, payload.password)
|
||||||
|
const authRecord = authData.record || userRecord
|
||||||
|
|
||||||
|
const user = enrichUser(authRecord)
|
||||||
|
|
||||||
|
logger.info('平台用户登录成功', {
|
||||||
|
users_id: user.users_id,
|
||||||
|
openid: user.openid,
|
||||||
|
users_idtype: user.users_idtype,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'login_success',
|
||||||
|
is_info_complete: isInfoComplete(authRecord),
|
||||||
|
user: user,
|
||||||
|
authRecord: authRecord,
|
||||||
|
authMethod: '',
|
||||||
|
meta: buildAuthMeta({
|
||||||
|
status: 'login_success',
|
||||||
|
is_info_complete: isInfoComplete(authRecord),
|
||||||
|
user: user,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function updateWechatUserProfile(usersWxOpenid, payload) {
|
function updateWechatUserProfile(usersWxOpenid, payload) {
|
||||||
return withUserLock('profile:' + usersWxOpenid, function () {
|
return withUserLock('profile:' + usersWxOpenid, function () {
|
||||||
const currentUser = findUserByOpenid(usersWxOpenid)
|
const currentUser = findUserByOpenid(usersWxOpenid)
|
||||||
@@ -211,7 +346,6 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
|
|||||||
&& ((currentUser.getString('users_type') || GUEST_USER_TYPE) === GUEST_USER_TYPE)
|
&& ((currentUser.getString('users_type') || GUEST_USER_TYPE) === GUEST_USER_TYPE)
|
||||||
|
|
||||||
currentUser.set('users_name', payload.users_name)
|
currentUser.set('users_name', payload.users_name)
|
||||||
currentUser.set('user_name', payload.users_name)
|
|
||||||
currentUser.set('users_phone', usersPhone)
|
currentUser.set('users_phone', usersPhone)
|
||||||
currentUser.set('users_picture', payload.users_picture)
|
currentUser.set('users_picture', payload.users_picture)
|
||||||
if (shouldPromote) {
|
if (shouldPromote) {
|
||||||
@@ -235,14 +369,15 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshWechatToken(usersWxOpenid) {
|
function refreshAuthToken(openid) {
|
||||||
const userRecord = findUserByOpenid(usersWxOpenid)
|
const userRecord = findUserByOpenid(openid)
|
||||||
if (!userRecord) {
|
if (!userRecord) {
|
||||||
throw createAppError(404, '未注册用户')
|
throw createAppError(404, '未注册用户')
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('微信用户刷新令牌成功', {
|
logger.info('认证用户刷新令牌成功', {
|
||||||
users_id: userRecord.getString('users_id') || userRecord.getString('user_id'),
|
users_id: userRecord.getString('users_id'),
|
||||||
|
users_convers_id: userRecord.getString('users_convers_id'),
|
||||||
openid: userRecord.getString('openid'),
|
openid: userRecord.getString('openid'),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -257,8 +392,28 @@ function refreshWechatToken(usersWxOpenid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function issueAuthToken(authRecord) {
|
||||||
|
if (!authRecord) {
|
||||||
|
throw createAppError(500, '签发令牌失败:认证用户不存在')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof authRecord.newAuthToken !== 'function') {
|
||||||
|
throw createAppError(500, '签发令牌失败:当前运行环境不支持 newAuthToken')
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authRecord.newAuthToken()
|
||||||
|
if (!token) {
|
||||||
|
throw createAppError(500, '签发令牌失败:生成 token 为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
authenticateWechatUser,
|
authenticateWechatUser,
|
||||||
|
authenticatePlatformUser,
|
||||||
updateWechatUserProfile,
|
updateWechatUserProfile,
|
||||||
refreshWechatToken,
|
refreshAuthToken,
|
||||||
|
issueAuthToken,
|
||||||
|
registerPlatformUser,
|
||||||
}
|
}
|
||||||
|
|||||||
52
pocket-base/bai_web_pb_hooks/pages/page-a.js
Normal file
52
pocket-base/bai_web_pb_hooks/pages/page-a.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
routerAdd('GET', '/web/page-a', function (e) {
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>页面一</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 28px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
h1 { margin-top: 0; }
|
||||||
|
p { line-height: 1.8; color: #4b5563; }
|
||||||
|
.actions { margin-top: 20px; }
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #2563eb;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<section class="card">
|
||||||
|
<h1>页面一</h1>
|
||||||
|
<p>这里是 page-a 页面。后续你可以把它扩展成介绍页、帮助页、数据展示页或简单表单页。</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a href="/web">返回首页</a>
|
||||||
|
<a href="/web/page-b">跳转到页面二</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
return e.html(200, html)
|
||||||
|
})
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 范围
|
## 范围
|
||||||
|
|
||||||
本次变更覆盖 `pocket-base/` 下 PocketBase hooks 项目的微信登录、资料更新、token 刷新、认证落库、错误可观测性、索引策略与运行规范。
|
本次变更覆盖 `pocket-base/` 下 PocketBase hooks 项目的微信登录、平台注册/登录、资料更新、token 刷新、认证落库、错误可观测性、索引策略与运行规范。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,7 +30,9 @@
|
|||||||
|
|
||||||
### 1. openid 作为唯一业务身份锚点
|
### 1. openid 作为唯一业务身份锚点
|
||||||
|
|
||||||
- `tbl_auth_users` 仅保留 `openid` 作为微信身份锚点。
|
- `tbl_auth_users` 统一保留 `openid` 作为全平台身份锚点。
|
||||||
|
- 微信用户:`openid = 微信 openid`
|
||||||
|
- 平台用户:`openid = 服务端生成的 GUID`
|
||||||
- 业务逻辑中不再使用 `users_wx_openid`。
|
- 业务逻辑中不再使用 `users_wx_openid`。
|
||||||
- 用户查询、token 刷新、资料更新均基于 auth record 的 `openid`。
|
- 用户查询、token 刷新、资料更新均基于 auth record 的 `openid`。
|
||||||
|
|
||||||
@@ -38,7 +40,9 @@
|
|||||||
|
|
||||||
由于 `tbl_auth_users` 当前为 PocketBase `auth` 集合,登录注册时为兼容 PocketBase 原生 auth 校验,新增以下兼容策略:
|
由于 `tbl_auth_users` 当前为 PocketBase `auth` 集合,登录注册时为兼容 PocketBase 原生 auth 校验,新增以下兼容策略:
|
||||||
|
|
||||||
- `email` 使用占位格式:`<openid>@wechat.local`
|
- `email` 使用占位格式:
|
||||||
|
- 微信用户:`<openid>@wechat.local`
|
||||||
|
- 平台用户:`<openid>@manage.local`
|
||||||
- 自动生成随机密码
|
- 自动生成随机密码
|
||||||
- 自动补齐 `passwordConfirm`
|
- 自动补齐 `passwordConfirm`
|
||||||
|
|
||||||
@@ -92,7 +96,7 @@
|
|||||||
|
|
||||||
新增 `saveAuthUserRecord(record)` 包装 `$app.save(record)`:
|
新增 `saveAuthUserRecord(record)` 包装 `$app.save(record)`:
|
||||||
|
|
||||||
- 失败时统一抛出 `保存微信用户失败`
|
- 失败时统一抛出 `保存认证用户失败`
|
||||||
- 附带 `originalMessage` 与 `originalData`
|
- 附带 `originalMessage` 与 `originalData`
|
||||||
|
|
||||||
目的:
|
目的:
|
||||||
@@ -130,17 +134,36 @@
|
|||||||
|
|
||||||
- `POST /api/system/test-helloworld`
|
- `POST /api/system/test-helloworld`
|
||||||
- `POST /api/system/health`
|
- `POST /api/system/health`
|
||||||
|
- `POST /api/system/refresh-token`
|
||||||
|
- `POST /api/platform/register`
|
||||||
|
- `POST /api/platform/login`
|
||||||
- `POST /api/wechat/login`
|
- `POST /api/wechat/login`
|
||||||
- `POST /api/wechat/profile`
|
- `POST /api/wechat/profile`
|
||||||
- `POST /api/wechat/refresh-token`
|
|
||||||
|
其中平台用户链路补充为:
|
||||||
|
|
||||||
|
### `POST /api/platform/register`
|
||||||
|
|
||||||
|
- body 必填:`users_name`、`users_phone`、`password`、`passwordConfirm`、`users_picture`
|
||||||
|
- 自动生成 GUID 并写入统一身份字段 `openid`
|
||||||
|
- 写入 `users_idtype = ManagePlatform`
|
||||||
|
- 成功时返回 PocketBase 原生 token + auth record + meta
|
||||||
|
|
||||||
|
### `POST /api/platform/login`
|
||||||
|
|
||||||
|
- body 必填:`users_phone`、`password`
|
||||||
|
- 仅允许 `users_idtype = ManagePlatform`
|
||||||
|
- 前端使用手机号+密码提交
|
||||||
|
- 服务端内部仍通过 PocketBase 原生 password auth 返回原生 token
|
||||||
|
|
||||||
其中:
|
其中:
|
||||||
|
|
||||||
### `POST /api/wechat/login`
|
### `POST /api/wechat/login`
|
||||||
|
|
||||||
- body 必填:`users_wx_code`
|
- body 必填:`users_wx_code`
|
||||||
- 自动以微信 code 换取 `openid`
|
- 自动以微信 code 换取微信侧 `openid` 并写入统一身份字段
|
||||||
- 若不存在 auth 用户则尝试创建 `tbl_auth_users` 记录
|
- 若不存在 auth 用户则尝试创建 `tbl_auth_users` 记录
|
||||||
|
- 写入 `users_idtype = WeChat`
|
||||||
- 成功时返回 PocketBase 原生 token + auth record + meta
|
- 成功时返回 PocketBase 原生 token + auth record + meta
|
||||||
|
|
||||||
### `POST /api/wechat/profile`
|
### `POST /api/wechat/profile`
|
||||||
@@ -149,10 +172,15 @@
|
|||||||
- 基于当前 auth record 的 `openid` 定位用户
|
- 基于当前 auth record 的 `openid` 定位用户
|
||||||
- 服务端用 `users_phone_code` 换取手机号后保存
|
- 服务端用 `users_phone_code` 换取手机号后保存
|
||||||
|
|
||||||
### `POST /api/wechat/refresh-token`
|
### `POST /api/system/refresh-token`
|
||||||
|
|
||||||
- 需 `Authorization`
|
- body 可选:`users_wx_code`(允许为空)
|
||||||
- 直接基于当前 auth record 返回新的 PocketBase 原生 token
|
- `Authorization` 可选:
|
||||||
|
- 若 token 仍有效:基于当前 auth record 续签
|
||||||
|
- 若 token 已过期:回退到微信 code 重签流程
|
||||||
|
- 若 token 已过期且未提供 `users_wx_code`,返回:`token已过期,请上传users_wx_code`
|
||||||
|
- 返回精简结构,仅返回新 token(不返回完整登录用户信息)
|
||||||
|
- 属于系统级通用认证能力,不限定为微信专属接口
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: BAI PocketBase Hooks API
|
title: BAI PocketBase Hooks API
|
||||||
description: 基于 PocketBase `bai_api_pb_hooks` 的对外接口文档,可直接导入 Postman。
|
description: |
|
||||||
|
基于 PocketBase `bai_api_pb_hooks` 的对外接口文档,可直接导入 Postman。
|
||||||
|
当前 `tbl_auth_users.openid` 已被定义为全平台统一身份锚点:
|
||||||
|
- 微信用户:`openid = 微信 openid`
|
||||||
|
- 平台用户:`openid = 服务端生成的 GUID`
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
servers:
|
servers:
|
||||||
- url: https://bai-api.blv-oa.com/pb
|
- url: https://bai-api.blv-oa.com/pb
|
||||||
@@ -12,7 +16,9 @@ tags:
|
|||||||
- name: 系统
|
- name: 系统
|
||||||
description: 基础检查接口
|
description: 基础检查接口
|
||||||
- name: 微信认证
|
- name: 微信认证
|
||||||
description: 基于微信 openid 与 PocketBase 原生 token 的认证接口
|
description: 面向微信用户的认证接口;认证成功后仍统一使用全平台 `openid` 与 PocketBase 原生 token。
|
||||||
|
- name: 平台认证
|
||||||
|
description: 面向平台用户的认证接口;平台用户会生成 GUID 并写入统一 `openid` 字段。
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
@@ -42,6 +48,13 @@ components:
|
|||||||
timestamp:
|
timestamp:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
UsersCountData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
total_users:
|
||||||
|
type: integer
|
||||||
|
description: tbl_auth_users 表中的用户总数
|
||||||
|
example: 128
|
||||||
HelloWorldData:
|
HelloWorldData:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -64,11 +77,28 @@ components:
|
|||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
UserInfo:
|
UserInfo:
|
||||||
type: object
|
type: object
|
||||||
|
description: |
|
||||||
|
统一用户视图。
|
||||||
|
其中 `openid` 为全平台统一身份标识:微信用户使用微信 openid,平台用户使用服务端生成 GUID。
|
||||||
properties:
|
properties:
|
||||||
pb_id:
|
pb_id:
|
||||||
type: string
|
type: string
|
||||||
|
users_convers_id:
|
||||||
|
type: string
|
||||||
users_id:
|
users_id:
|
||||||
type: string
|
type: string
|
||||||
|
users_idtype:
|
||||||
|
type: string
|
||||||
|
description: 用户身份来源类型
|
||||||
|
enum: [WeChat, ManagePlatform]
|
||||||
|
users_id_number:
|
||||||
|
type: string
|
||||||
|
users_status:
|
||||||
|
type: number
|
||||||
|
users_rank_level:
|
||||||
|
type: number
|
||||||
|
users_auth_type:
|
||||||
|
type: number
|
||||||
users_type:
|
users_type:
|
||||||
type: string
|
type: string
|
||||||
enum: [游客, 注册用户]
|
enum: [游客, 注册用户]
|
||||||
@@ -78,12 +108,21 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
users_phone_masked:
|
users_phone_masked:
|
||||||
type: string
|
type: string
|
||||||
|
users_level:
|
||||||
|
type: string
|
||||||
users_picture:
|
users_picture:
|
||||||
type: string
|
type: string
|
||||||
openid:
|
openid:
|
||||||
type: string
|
type: string
|
||||||
|
description: 全平台统一身份标识;微信用户为微信 openid,平台用户为服务端生成的 GUID
|
||||||
company_id:
|
company_id:
|
||||||
type: string
|
type: string
|
||||||
|
users_parent_id:
|
||||||
|
type: string
|
||||||
|
users_promo_code:
|
||||||
|
type: string
|
||||||
|
usergroups_id:
|
||||||
|
type: string
|
||||||
company:
|
company:
|
||||||
$ref: '#/components/schemas/CompanyInfo'
|
$ref: '#/components/schemas/CompanyInfo'
|
||||||
created:
|
created:
|
||||||
@@ -92,6 +131,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
PocketBaseAuthResponse:
|
PocketBaseAuthResponse:
|
||||||
type: object
|
type: object
|
||||||
|
description: |
|
||||||
|
PocketBase 原生认证响应。
|
||||||
|
客户端可直接使用返回的 `token` 与 PocketBase SDK 或当前 hooks 接口交互。
|
||||||
properties:
|
properties:
|
||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
@@ -121,6 +163,7 @@ components:
|
|||||||
WechatLoginRequest:
|
WechatLoginRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [users_wx_code]
|
required: [users_wx_code]
|
||||||
|
description: 微信小程序登录/注册请求体。
|
||||||
properties:
|
properties:
|
||||||
users_wx_code:
|
users_wx_code:
|
||||||
type: string
|
type: string
|
||||||
@@ -129,6 +172,7 @@ components:
|
|||||||
WechatProfileRequest:
|
WechatProfileRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [users_name, users_phone_code, users_picture]
|
required: [users_name, users_phone_code, users_picture]
|
||||||
|
description: 微信用户资料完善请求体。
|
||||||
properties:
|
properties:
|
||||||
users_name:
|
users_name:
|
||||||
type: string
|
type: string
|
||||||
@@ -147,6 +191,70 @@ components:
|
|||||||
enum: [update_success]
|
enum: [update_success]
|
||||||
user:
|
user:
|
||||||
$ref: '#/components/schemas/UserInfo'
|
$ref: '#/components/schemas/UserInfo'
|
||||||
|
PlatformRegisterRequest:
|
||||||
|
type: object
|
||||||
|
required: [users_name, users_phone, password, passwordConfirm, users_picture]
|
||||||
|
description: 平台用户注册请求体;注册成功后将生成 GUID 并写入统一 `openid` 字段。
|
||||||
|
properties:
|
||||||
|
users_name:
|
||||||
|
type: string
|
||||||
|
example: 张三
|
||||||
|
users_phone:
|
||||||
|
type: string
|
||||||
|
example: 13800138000
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
example: 12345678
|
||||||
|
passwordConfirm:
|
||||||
|
type: string
|
||||||
|
example: 12345678
|
||||||
|
users_picture:
|
||||||
|
type: string
|
||||||
|
example: https://example.com/avatar.png
|
||||||
|
users_id_number:
|
||||||
|
type: string
|
||||||
|
users_level:
|
||||||
|
type: string
|
||||||
|
users_type:
|
||||||
|
type: string
|
||||||
|
company_id:
|
||||||
|
type: string
|
||||||
|
users_parent_id:
|
||||||
|
type: string
|
||||||
|
users_promo_code:
|
||||||
|
type: string
|
||||||
|
usergroups_id:
|
||||||
|
type: string
|
||||||
|
PlatformLoginRequest:
|
||||||
|
type: object
|
||||||
|
required: [users_phone, password]
|
||||||
|
description: 平台用户登录请求体;前端使用手机号+密码提交,服务端内部转换为 PocketBase 原生 password auth。
|
||||||
|
properties:
|
||||||
|
users_phone:
|
||||||
|
type: string
|
||||||
|
example: 13800138000
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
example: 12345678
|
||||||
|
SystemRefreshTokenRequest:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
系统刷新 token 请求体。
|
||||||
|
`users_wx_code` 允许为空。
|
||||||
|
当 `Authorization` 对应 token 有效时,可不传或传空;
|
||||||
|
当 token 失效时,需提供 `users_wx_code` 走微信 code 重新签发流程。
|
||||||
|
properties:
|
||||||
|
users_wx_code:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: 微信小程序登录临时凭证 code
|
||||||
|
example: 0a1b2c3d4e5f6g
|
||||||
|
RefreshTokenData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: 新签发的 PocketBase 原生 auth token
|
||||||
paths:
|
paths:
|
||||||
/api/system/test-helloworld:
|
/api/system/test-helloworld:
|
||||||
post:
|
post:
|
||||||
@@ -180,13 +288,76 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/HealthData'
|
$ref: '#/components/schemas/HealthData'
|
||||||
|
/api/system/users-count:
|
||||||
|
post:
|
||||||
|
tags: [系统]
|
||||||
|
summary: 查询用户总数
|
||||||
|
description: 统计 `tbl_auth_users` 集合中的记录总数,并返回一个数值。
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/UsersCountData'
|
||||||
|
/api/system/refresh-token:
|
||||||
|
post:
|
||||||
|
tags: [系统]
|
||||||
|
summary: 刷新系统认证 token
|
||||||
|
description: |
|
||||||
|
当前实现支持两种刷新路径:
|
||||||
|
1) 若 `Authorization` 对应 token 仍有效:直接按当前 auth record 续签(不调用微信接口)。
|
||||||
|
2) 若 token 已过期:仅在 body 提供 `users_wx_code` 时才走微信 code 重新签发。
|
||||||
|
返回体仅包含新的 `token`,不返回完整登录用户信息。
|
||||||
|
parameters:
|
||||||
|
- in: header
|
||||||
|
name: Authorization
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: 可选。建议传入旧 token(`Bearer <token>`)以优先走有效 token 续签路径。
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SystemRefreshTokenRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 刷新成功(返回精简 token)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/RefreshTokenData'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或微信侧身份换取失败
|
||||||
|
'401':
|
||||||
|
description: token 无效/已过期,且未提供 users_wx_code
|
||||||
|
'404':
|
||||||
|
description: 用户不存在
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
/api/wechat/login:
|
/api/wechat/login:
|
||||||
post:
|
post:
|
||||||
tags: [微信认证]
|
tags: [微信认证]
|
||||||
summary: 微信登录/注册合一
|
summary: 微信登录/注册合一
|
||||||
description: |
|
description: |
|
||||||
使用微信 code 换取 openid。
|
使用微信 code 换取微信侧 openid,并写入统一身份字段 `tbl_auth_users.openid`。
|
||||||
若 `tbl_auth_users` 中不存在对应用户则自动创建 auth record,随后返回 PocketBase 原生 auth token。
|
若 `tbl_auth_users` 中不存在对应用户则自动创建 auth record,随后返回 PocketBase 原生 auth token。
|
||||||
|
首次注册创建时会写入 `users_idtype = WeChat`。
|
||||||
|
返回的 `token` 可直接用于 PocketBase SDK 与当前 hooks 接口调用。
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -201,7 +372,73 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PocketBaseAuthResponse'
|
$ref: '#/components/schemas/PocketBaseAuthResponse'
|
||||||
'400':
|
'400':
|
||||||
description: 参数错误
|
description: 参数错误或微信侧身份换取失败
|
||||||
|
'401':
|
||||||
|
description: PocketBase 原生认证失败
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
'500':
|
||||||
|
description: 保存 auth 用户失败或服务端内部错误
|
||||||
|
/api/platform/register:
|
||||||
|
post:
|
||||||
|
tags: [平台认证]
|
||||||
|
summary: 平台用户注册
|
||||||
|
description: |
|
||||||
|
创建平台用户 auth record。
|
||||||
|
服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。
|
||||||
|
前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。
|
||||||
|
注册成功后直接返回 PocketBase 原生 auth token。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PlatformRegisterRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 注册成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PocketBaseAuthResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或手机号已存在
|
||||||
|
'500':
|
||||||
|
description: GUID 生成失败、auth identity 缺失或保存用户失败
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
/api/platform/login:
|
||||||
|
post:
|
||||||
|
tags: [平台认证]
|
||||||
|
summary: 平台用户登录
|
||||||
|
description: |
|
||||||
|
前端使用平台注册时保存的 `users_phone + password` 登录。
|
||||||
|
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
||||||
|
服务端会先按手机号定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
||||||
|
登录成功后直接返回 PocketBase 原生 auth token。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PlatformLoginRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 登录成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PocketBaseAuthResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、密码错误或用户类型不匹配
|
||||||
|
'404':
|
||||||
|
description: 平台用户不存在
|
||||||
|
'500':
|
||||||
|
description: 平台用户缺少原生登录 identity 或服务端内部错误
|
||||||
'415':
|
'415':
|
||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
'429':
|
'429':
|
||||||
@@ -210,6 +447,9 @@ paths:
|
|||||||
post:
|
post:
|
||||||
tags: [微信认证]
|
tags: [微信认证]
|
||||||
summary: 更新微信用户资料
|
summary: 更新微信用户资料
|
||||||
|
description: |
|
||||||
|
基于当前 `Authorization` 对应 auth record 中的统一 `openid` 定位当前微信用户。
|
||||||
|
当前接口仍用于微信资料完善场景。
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -231,23 +471,6 @@ paths:
|
|||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/WechatProfileResponseData'
|
$ref: '#/components/schemas/WechatProfileResponseData'
|
||||||
'401':
|
'401':
|
||||||
description: token 无效或当前 auth record 缺少 openid
|
description: token 无效或当前 auth record 缺少统一身份字段 openid
|
||||||
/api/wechat/refresh-token:
|
'400':
|
||||||
post:
|
description: 参数错误、手机号已被注册或资料更新失败
|
||||||
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: 用户不存在
|
|
||||||
|
|||||||
@@ -38,13 +38,12 @@ const collections = [
|
|||||||
name: 'tbl_auth_users',
|
name: 'tbl_auth_users',
|
||||||
type: 'auth',
|
type: 'auth',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'user_id', type: 'text' },
|
{ name: 'users_convers_id', type: 'text' },
|
||||||
{ name: 'openid', type: 'text', required: true },
|
{ name: 'openid', type: 'text', required: true },
|
||||||
{ name: 'user_name', type: 'text' },
|
|
||||||
{ name: 'org_id', type: 'number' },
|
{ name: 'org_id', type: 'number' },
|
||||||
{ name: 'rank_level', type: 'number' },
|
{ name: 'users_rank_level', type: 'number' },
|
||||||
{ name: 'status', type: 'number' },
|
{ name: 'users_status', type: 'number' },
|
||||||
{ name: 'user_auth_type', type: 'number' },
|
{ name: 'users_auth_type', type: 'number' },
|
||||||
|
|
||||||
{ name: 'users_id', type: 'text' },
|
{ name: 'users_id', type: 'text' },
|
||||||
{ name: 'users_name', type: 'text' },
|
{ name: 'users_name', type: 'text' },
|
||||||
@@ -64,14 +63,13 @@ const collections = [
|
|||||||
{ name: 'usergroups_id', type: 'text' }
|
{ name: 'usergroups_id', type: 'text' }
|
||||||
],
|
],
|
||||||
indexes: [
|
indexes: [
|
||||||
'CREATE UNIQUE INDEX idx_tbl_auth_users_user_id ON tbl_auth_users (user_id)',
|
'CREATE UNIQUE INDEX idx_tbl_auth_users_users_convers_id ON tbl_auth_users (users_convers_id)',
|
||||||
'CREATE UNIQUE INDEX idx_tbl_auth_users_openid ON tbl_auth_users (openid)',
|
'CREATE UNIQUE INDEX idx_tbl_auth_users_openid ON tbl_auth_users (openid)',
|
||||||
'CREATE INDEX idx_tbl_auth_users_org_id ON tbl_auth_users (org_id)',
|
'CREATE INDEX idx_tbl_auth_users_org_id ON tbl_auth_users (org_id)',
|
||||||
'CREATE INDEX idx_tbl_auth_users_rank_level ON tbl_auth_users (rank_level)',
|
'CREATE INDEX idx_tbl_auth_users_users_rank_level ON tbl_auth_users (users_rank_level)',
|
||||||
'CREATE INDEX idx_tbl_auth_users_status ON tbl_auth_users (status)',
|
'CREATE INDEX idx_tbl_auth_users_users_status ON tbl_auth_users (users_status)',
|
||||||
'CREATE INDEX idx_tbl_auth_users_user_auth_type ON tbl_auth_users (user_auth_type)',
|
'CREATE INDEX idx_tbl_auth_users_users_auth_type ON tbl_auth_users (users_auth_type)',
|
||||||
|
|
||||||
'CREATE UNIQUE INDEX idx_tbl_auth_users_users_id ON tbl_auth_users (users_id)',
|
|
||||||
// Allow unbound users with empty phone while still speeding up phone lookups.
|
// Allow unbound users with empty phone while still speeding up phone lookups.
|
||||||
'CREATE INDEX idx_tbl_auth_users_users_phone ON tbl_auth_users (users_phone)',
|
'CREATE INDEX idx_tbl_auth_users_users_phone ON tbl_auth_users (users_phone)',
|
||||||
'CREATE INDEX idx_tbl_auth_users_company_id ON tbl_auth_users (company_id)',
|
'CREATE INDEX idx_tbl_auth_users_company_id ON tbl_auth_users (company_id)',
|
||||||
@@ -133,16 +131,16 @@ const collections = [
|
|||||||
type: 'base',
|
type: 'base',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'override_id', type: 'text', required: true },
|
{ name: 'override_id', type: 'text', required: true },
|
||||||
{ name: 'user_id', type: 'text', required: true },
|
{ name: 'users_convers_id', type: 'text', required: true },
|
||||||
{ name: 'res_id', type: 'text', required: true },
|
{ name: 'res_id', type: 'text', required: true },
|
||||||
{ name: 'access_level', type: 'number', required: true },
|
{ name: 'access_level', type: 'number', required: true },
|
||||||
{ name: 'priority', type: 'number' }
|
{ name: 'priority', type: 'number' }
|
||||||
],
|
],
|
||||||
indexes: [
|
indexes: [
|
||||||
'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_override_id ON tbl_auth_user_overrides (override_id)',
|
'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_override_id ON tbl_auth_user_overrides (override_id)',
|
||||||
'CREATE INDEX idx_tbl_auth_user_overrides_user_id ON tbl_auth_user_overrides (user_id)',
|
'CREATE INDEX idx_tbl_auth_user_overrides_users_convers_id ON tbl_auth_user_overrides (users_convers_id)',
|
||||||
'CREATE INDEX idx_tbl_auth_user_overrides_res_id ON tbl_auth_user_overrides (res_id)',
|
'CREATE INDEX idx_tbl_auth_user_overrides_res_id ON tbl_auth_user_overrides (res_id)',
|
||||||
'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_unique_map ON tbl_auth_user_overrides (user_id, res_id)'
|
'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_unique_map ON tbl_auth_user_overrides (users_convers_id, res_id)'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user