feat: 规范化PocketBase数据库文档与原生API访问

- 将数据库文档拆分为按collection命名的标准文件,统一格式
- 补充tbl_company、tbl_system_dict等表的原生访问规则
- 新增users_tag、document_create等字段
- 优化用户资料更新接口,支持非必填字段
- 添加公司原生API测试脚本
- 归档本次变更至OpenSpec
This commit is contained in:
2026-03-29 16:21:34 +08:00
parent 51a90260e4
commit e9fe1165e3
46 changed files with 3790 additions and 1108 deletions

View File

@@ -133,6 +133,7 @@
"users_type": "注册用户", "users_type": "注册用户",
"users_name": "张三", "users_name": "张三",
"users_phone": "13800138000", "users_phone": "13800138000",
"users_tag": "核心客户",
"users_phone_masked": "138****8000", "users_phone_masked": "138****8000",
"users_picture": "ATT-1743123456789-abc123", "users_picture": "ATT-1743123456789-abc123",
"users_picture_url": "https://bai-api.blv-oa.com/pb/api/files/pbc_xxx/recordId/avatar.png", "users_picture_url": "https://bai-api.blv-oa.com/pb/api/files/pbc_xxx/recordId/avatar.png",
@@ -163,6 +164,8 @@
{ {
"users_name": "张三", "users_name": "张三",
"users_phone_code": "2b7d9f2e3c4a5b6d7e8f", "users_phone_code": "2b7d9f2e3c4a5b6d7e8f",
"users_phone": "13800138000",
"users_tag": "核心客户",
"users_picture": "ATT-1743123456789-abc123", "users_picture": "ATT-1743123456789-abc123",
"users_id_pic_a": "ATT-1743123456789-id-a", "users_id_pic_a": "ATT-1743123456789-id-a",
"users_id_pic_b": "ATT-1743123456789-id-b", "users_id_pic_b": "ATT-1743123456789-id-b",
@@ -174,9 +177,11 @@
| 参数名 | 类型 | 必填 | 说明 | | 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---| |---|---|---|---|
| `users_name` | string | | 用户姓名 | | `users_name` | string | | 用户姓名;非空时才更新 |
| `users_phone_code` | string | | 微信手机号获取凭证 code,后端将据此换取真实手机号 | | `users_phone_code` | string | | 微信手机号获取凭证 code;若提供,服务端优先据此换取真实手机号 |
| `users_picture` | string | | 用户头像附件的 `attachments_id` | | `users_phone` | string | | 直接写入的手机号;仅在未提供 `users_phone_code` 时生效 |
| `users_tag` | string | 否 | 用户标签;非空时才更新 |
| `users_picture` | string | 否 | 用户头像附件的 `attachments_id`;非空时才更新 |
| `users_id_pic_a` | string | 否 | 证件正面附件的 `attachments_id` | | `users_id_pic_a` | string | 否 | 证件正面附件的 `attachments_id` |
| `users_id_pic_b` | string | 否 | 证件反面附件的 `attachments_id` | | `users_id_pic_b` | string | 否 | 证件反面附件的 `attachments_id` |
| `users_title_picture` | string | 否 | 资质附件的 `attachments_id` | | `users_title_picture` | string | 否 | 资质附件的 `attachments_id` |
@@ -186,8 +191,10 @@
-`Authorization` 对应的 PocketBase auth record 读取当前用户 `openid` -`Authorization` 对应的 PocketBase auth record 读取当前用户 `openid`
- 校验 `Authorization` - 校验 `Authorization`
- 不再从 body 读取 `users_wx_code` - 不再从 body 读取 `users_wx_code`
- 使用 `users_phone_code` 调微信官方接口换取真实手机号 - 所有字段均允许不传或传空
- 真实手机号写入数据库字段 `users_phone` - 若传入 `users_phone_code`,优先调用微信接口换取真实手机号写入 `users_phone`
- 若未传 `users_phone_code` 但传入 `users_phone`,则直接写入数据库字段 `users_phone`
- 未传或传空的字段不会清空数据库中的已有值;只有非空字段才会更新
- `users_picture``users_id_pic_a``users_id_pic_b``users_title_picture` 均按 `attachments_id` 存储,服务端查询用户信息时会自动补充对应文件流链接 - `users_picture``users_id_pic_a``users_id_pic_b``users_title_picture` 均按 `attachments_id` 存储,服务端查询用户信息时会自动补充对应文件流链接
- 若用户首次从“三项资料均为空”变为“三项资料均完整”,则将 `users_type``游客` 升级为 `注册用户` - 若用户首次从“三项资料均为空”变为“三项资料均完整”,则将 `users_type``游客` 升级为 `注册用户`
- 返回更新后的完整用户信息 - 返回更新后的完整用户信息
@@ -205,6 +212,7 @@
"users_type": "注册用户", "users_type": "注册用户",
"users_name": "张三", "users_name": "张三",
"users_phone": "13800138000", "users_phone": "13800138000",
"users_tag": "核心客户",
"users_phone_masked": "138****8000", "users_phone_masked": "138****8000",
"users_picture": "ATT-1743123456789-abc123", "users_picture": "ATT-1743123456789-abc123",
"users_picture_url": "https://bai-api.blv-oa.com/pb/api/files/pbc_xxx/recordId/avatar.png", "users_picture_url": "https://bai-api.blv-oa.com/pb/api/files/pbc_xxx/recordId/avatar.png",

View File

@@ -0,0 +1,168 @@
# API 返回字段备注补充
这份文档用于补充 OpenAPI 返回示例里需要展示的“备注 | 类型”文案。
## 1. 用户返回结构 `UserInfo`
| 字段名 | 建议示例文案 |
| :--- | :--- |
| `pb_id` | `PocketBase记录主键 | string` |
| `users_convers_id` | `会话侧用户ID | string` |
| `users_id` | `用户业务ID | string` |
| `users_idtype` | `用户身份来源类型 | string` |
| `users_id_number` | `证件号 | string` |
| `users_type` | `用户类型 | string` |
| `users_name` | `用户姓名 | string` |
| `users_status` | `用户状态 | string` |
| `users_rank_level` | `等级 | number` |
| `users_auth_type` | `账户类型 | number` |
| `users_phone` | `手机号 | string` |
| `users_phone_masked` | `脱敏手机号 | string` |
| `users_level` | `用户等级 | string` |
| `users_tag` | `用户标签 | string` |
| `users_picture` | `用户头像附件ID | string` |
| `users_picture_url` | `用户头像文件流链接 | string` |
| `users_id_pic_a` | `证件正面附件ID | string` |
| `users_id_pic_a_url` | `证件正面文件流链接 | string` |
| `users_id_pic_b` | `证件反面附件ID | string` |
| `users_id_pic_b_url` | `证件反面文件流链接 | string` |
| `users_title_picture` | `资质附件ID | string` |
| `users_title_picture_url` | `资质文件流链接 | string` |
| `openid` | `全平台统一身份标识 | string` |
| `company_id` | `公司业务id | string` |
| `users_parent_id` | `上级用户业务id | string` |
| `users_promo_code` | `推广码 | string` |
| `usergroups_id` | `用户组业务id | string` |
| `created` | `创建时间 | string` |
| `updated` | `更新时间 | string` |
## 2. 公司返回结构 `CompanyInfo`
| 字段名 | 建议示例文案 |
| :--- | :--- |
| `pb_id` | `PocketBase记录主键 | string` |
| `company_id` | `公司业务id | string` |
| `company_name` | `公司名称 | string` |
| `company_type` | `公司类型 | string` |
| `company_entity` | `公司法人 | string` |
| `company_usci` | `统一社会信用代码 | string` |
| `company_nationality` | `国家名称 | string` |
| `company_nationality_code` | `国家编码 | string` |
| `company_province` | `省份名称 | string` |
| `company_province_code` | `省份编码 | string` |
| `company_city` | `城市名称 | string` |
| `company_city_code` | `城市编码 | string` |
| `company_district` | `区县名称 | string` |
| `company_district_code` | `区县编码 | string` |
| `company_postalcode` | `邮政编码 | string` |
| `company_add` | `公司地址 | string` |
| `company_status` | `公司状态 | string` |
| `company_level` | `公司等级 | string` |
| `company_owner_openid` | `公司所有者openid | string` |
| `company_remark` | `备注 | string` |
| `created` | `创建时间 | string` |
| `updated` | `更新时间 | string` |
## 3. 待你补充确认的字段
下面这些字段目前在已有 docs 中备注还不够稳定,如果你希望我继续把“所有接口返回示例”都改成这种风格,建议你先补充这几类字段的标准叫法:
- 系统类返回里各状态字段的最终业务叫法
你补完以后,我可以继续把剩余接口的返回示例全部统一成 `备注 | 类型` 风格。
## 3. 字典返回结构 `DictionaryRecord` / `DictionaryItem`
| 字段名 | 建议示例文案 |
| :--- | :--- |
| `pb_id` | `PocketBase记录主键 | string` |
| `system_dict_id` | `字典业务id | string` |
| `dict_name` | `词典名称 | string` |
| `dict_word_is_enabled` | `是否有效 | boolean` |
| `dict_word_parent_id` | `父级字典业务id | string` |
| `dict_word_remark` | `备注 | string` |
| `enum` | `枚举值 | string` |
| `description` | `枚举描述 | string` |
| `image` | `图片附件ID | string` |
| `imageUrl` | `图片文件流链接 | string` |
| `sortOrder` | `显示顺序 | integer` |
| `created` | `创建时间 | string` |
| `updated` | `更新时间 | string` |
## 4. 附件返回结构 `AttachmentRecord`
| 字段名 | 建议示例文案 |
| :--- | :--- |
| `pb_id` | `PocketBase记录主键 | string` |
| `attachments_id` | `附件业务id | string` |
| `attachments_link` | `PocketBase存储文件名 | string` |
| `attachments_url` | `附件文件流访问链接 | string` |
| `attachments_download_url` | `附件下载链接 | string` |
| `attachments_filename` | `原始文件名 | string` |
| `attachments_filetype` | `文件类型或MIME | string` |
| `attachments_size` | `附件大小 | number` |
| `attachments_owner` | `上传者业务id | string` |
| `attachments_md5` | `附件MD5码 | string` |
| `attachments_ocr` | `OCR识别结果 | string` |
| `attachments_status` | `附件状态 | string` |
| `attachments_remark` | `备注 | string` |
| `created` | `创建时间 | string` |
| `updated` | `更新时间 | string` |
## 5. 文档返回结构 `DocumentRecord`
| 字段名 | 建议示例文案 |
| :--- | :--- |
| `pb_id` | `PocketBase记录主键 | string` |
| `document_id` | `文档业务id | string` |
| `document_create` | `文档创建时间 | string` |
| `document_effect_date` | `文档生效日期 | string` |
| `document_expiry_date` | `文档到期日期 | string` |
| `document_type` | `文档类型 | string` |
| `document_title` | `文档标题 | string` |
| `document_subtitle` | `文档副标题 | string` |
| `document_summary` | `文档摘要 | string` |
| `document_content` | `正文内容 | string` |
| `document_image` | `图片附件ID串 | string` |
| `document_image_ids` | `图片附件ID列表 | array` |
| `document_image_urls` | `图片文件流链接列表 | array` |
| `document_image_url` | `第一张图片文件流链接 | string` |
| `document_video` | `视频附件ID串 | string` |
| `document_video_ids` | `视频附件ID列表 | array` |
| `document_video_urls` | `视频文件流链接列表 | array` |
| `document_video_url` | `第一个视频文件流链接 | string` |
| `document_file` | `文件附件ID串 | string` |
| `document_file_ids` | `文件附件ID列表 | array` |
| `document_file_urls` | `文件文件流链接列表 | array` |
| `document_file_url` | `第一个文件文件流链接 | string` |
| `document_owner` | `上传者openid | string` |
| `document_relation_model` | `关联机型标识 | string` |
| `document_keywords` | `关键词 | string` |
| `document_share_count` | `分享次数 | number` |
| `document_download_count` | `下载次数 | number` |
| `document_favorite_count` | `收藏次数 | number` |
| `document_status` | `文档状态 | string` |
| `document_embedding_status` | `文档嵌入状态 | string` |
| `document_embedding_error` | `文档错误原因 | string` |
| `document_embedding_lasttime` | `最后更新日期 | string` |
| `document_vector_version` | `向量版本号或模型名称 | string` |
| `document_product_categories` | `产品关联文档 | string` |
| `document_application_scenarios` | `筛选依据 | string` |
| `document_hotel_type` | `适用场景 | string` |
| `document_remark` | `备注 | string` |
| `created` | `创建时间 | string` |
| `updated` | `更新时间 | string` |
## 6. 文档历史返回结构 `DocumentHistoryRecord`
| 字段名 | 建议示例文案 |
| :--- | :--- |
| `pb_id` | `PocketBase记录主键 | string` |
| `doh_id` | `文档操作历史业务id | string` |
| `doh_document_id` | `关联文档业务id | string` |
| `doh_operation_type` | `操作类型 | string` |
| `doh_user_id` | `操作人业务id | string` |
| `doh_current_count` | `本次操作对应次数 | number` |
| `doh_remark` | `备注 | string` |
| `created` | `创建时间 | string` |
| `updated` | `更新时间 | string` |

View File

@@ -1,95 +0,0 @@
针对你的复杂权限需求,建议采用 **“RBAC基于角色的访问控制+ ABAC基于属性的访问控制+ 个性化覆盖Overrides”** 的混合模式。这种结构能支持从“大颗粒度角色”到“极细颗粒度字段/行”的动态权限矩阵。
以下是为你设计的实施方案及关键表结构规划:
### 一、 核心表方案设计
为了实现“字段级”和“行级”的动态控制,我们需要将 **资源Resource**、**权限定义Permission**、**角色Role** 与 **用户User** 彻底解耦。
#### 1. 用户基础表:`tbl_auth_users`
作为全局身份锚点,记录用户的静态信息和动态属性(部门、等级)。
| 字段名 | 类型 | 说明 |
| :--- | :--- | :--- |
| **users_convers_id** | String | 会话/对话侧用户标识,允许为空 |
| **openid** | String (Unique) | **全局身份锚点**,微信唯一标识 |
| **users_name** | String | 姓名/昵称 |
| **org_id** | Int | 所属组织/部门 ID影响行级权限的关键属性 |
| **users_rank_level** | Int | 职级/等级(影响动态权限矩阵的关键属性) |
| **users_status** | Int | 账户状态 (1: 正常, 0: 禁用) |
| **users_auth_type** | Int | 账户类型 (0: 微信小程序1: 管理平台2: 其他) |
---
#### 2. 资源定义表:`tbl_auth_resources`
定义系统中哪些表、哪些字段属于受控资源。
| 字段名 | 类型 | 说明 |
| :--- | :--- | :--- |
| **res_id** | Int (PK) | 资源 ID |
| **table_name** | String | 数据库表名 |
| **column_name** | String | 字段名(如果是表级权限,此项可为空或设为 '*' |
| **res_type** | Enum | 资源类型:`TABLE`(行/全表), `COLUMN`(字段级) |
---
#### 3. 角色与基础权限:`tbl_auth_roles` & `tbl_auth_role_perms`
实现通用的权限模板,方便批量管理。
* **`tbl_auth_roles`**: 角色表(如:财务经理、普通销售)。
* **`tbl_auth_role_perms`**: 角色权限关联表,定义该角色对某个资源的操作(读/写/无)。
---
#### 4. **核心:个性化权限覆盖表 `tbl_auth_user_overrides`**
这是满足你“某个用户对某个字段单独设置”需求的关键。
| 字段名 | 类型 | 说明 |
| :--- | :--- | :--- |
| **id** | BigInt (PK) | 自增 ID |
| **users_convers_id** | String | 用户会话标识(关联 `tbl_auth_users.users_convers_id` |
| **res_id** | Int | 资源 ID关联 `tbl_auth_resources` |
| **access_level** | Int | 权限值 (0: 无权, 1: 只读, 2: 读写) |
| **priority** | Int | 优先级(当角色权限与个人设置冲突时,以此为准) |
---
#### 5. **核心:行级过滤策略表 `tbl_auth_row_scopes`**
实现“某个用户只能看自己部门/某几个项目”的动态逻辑。
| 字段名 | 类型 | 说明 |
| :--- | :--- | :--- |
| **id** | Int (PK) | 策略 ID |
| **target_type** | Enum | 目标:`USER``ROLE` |
| **target_id** | BigInt | 对应的 UserID 或 RoleID |
| **table_name** | String | 作用的表名 |
| **filter_sql** | String | 过滤逻辑。例如:`dept_id = {user.org_id}``creator_id = {user.users_convers_id}` |
---
### 二、 权限实施方案逻辑
你的权限矩阵页面将由以下逻辑驱动:
#### 1. 权限计算路径 (Effective Permissions)
当用户访问某个数据时,系统按照以下顺序合并权限:
1. **取基础属性**:获取用户的 `org_id``users_rank_level`
2. **取角色权限**:获取该用户所属角色对应的资源权限列表。
3. **应用个性化覆盖**:查询 `tbl_auth_user_overrides`。如果该表中有记录,则**覆盖**(或叠加)角色权限。
4. **注入行级过滤**:如果是查询操作,解析 `tbl_auth_row_scopes` 中的 `filter_sql`,将 `{user.xxx}` 变量替换为当前用户的真实值。
#### 2. 动态更新机制
* **组织/等级变更**:当 `tbl_auth_users` 中的 `org_id``users_rank_level` 变化时,由于行级过滤表(`tbl_auth_row_scopes`)引用的是动态变量,**权限会自动生效**,无需重新授权。
* **缓存策略**:建议将计算后的“最终权限清单”缓存到 Redis。当用户在后台矩阵页面修改权限或者发生组织架构调整时通过 `wx_openid` **主动失效Purge** 该用户的 Redis 缓存。
#### 3. 字段级权限实现 (Field-Level)
在接口层(中间件),根据计算出的字段级权限清单(即用户对该 Table 下哪些 Column 有读权),动态过滤返回的 JSON 结构。
### 三、 总结:你需要几张表?
为了实现你描述的系统,最精简需要 **5 张表**
1. **`tbl_auth_users`**:用户主体(含 OpenID、部门、等级
2. **`tbl_auth_resources`**:资源清单(表名、字段名)。
3. **`tbl_auth_roles`**:角色定义。
4. **`tbl_auth_role_perms`** / **`tbl_auth_user_overrides`**:权限映射(解决字段级和个人特权)。
5. **`tbl_auth_row_scopes`**:行级过滤表达式(解决多维数据隔离)。

37
docs/pb_auth_model.md Normal file
View File

@@ -0,0 +1,37 @@
# pb_auth_model
> 来源:`script/pocketbase.newpb.js`、当前 hooks 权限实现
> 说明:本文件描述权限模型设计与表间职责,不替代单表结构文档
## 模型目标
当前项目采用 `RBAC + ABAC + 用户覆盖` 的混合权限模型:
- `RBAC` 负责角色级默认权限
- `ABAC` 负责组织、等级、目标表等属性条件
- `用户覆盖` 负责单人例外授权
## 核心表职责
| 文档 | 作用 |
| :--- | :--- |
| [pb_tbl_auth_users.md](/e:/Project_Class/BAI/Web_BAI_Manage_ApiServer/docs/pb_tbl_auth_users.md) | 用户身份源与 PocketBase auth 主表 |
| [pb_tbl_auth_resources.md](/e:/Project_Class/BAI/Web_BAI_Manage_ApiServer/docs/pb_tbl_auth_resources.md) | 资源目录 |
| [pb_tbl_auth_roles.md](/e:/Project_Class/BAI/Web_BAI_Manage_ApiServer/docs/pb_tbl_auth_roles.md) | 角色定义 |
| [pb_tbl_auth_role_perms.md](/e:/Project_Class/BAI/Web_BAI_Manage_ApiServer/docs/pb_tbl_auth_role_perms.md) | 角色权限映射 |
| [pb_tbl_auth_user_overrides.md](/e:/Project_Class/BAI/Web_BAI_Manage_ApiServer/docs/pb_tbl_auth_user_overrides.md) | 单用户权限覆盖 |
| [pb_tbl_auth_row_scopes.md](/e:/Project_Class/BAI/Web_BAI_Manage_ApiServer/docs/pb_tbl_auth_row_scopes.md) | 行级过滤范围 |
## 权限计算路径
1.`tbl_auth_users` 读取用户身份、组织、等级和角色。
2.`tbl_auth_role_perms` 读取角色默认权限。
3.`tbl_auth_user_overrides` 应用用户级覆盖。
4.`tbl_auth_row_scopes` 组合行级过滤条件。
5. 将可原生表达的权限同步到 PocketBase collection rules复杂业务权限仍由 hooks 判断。
## 当前实现约定
- 管理后台用户通过 `users_idtype = ManagePlatform` 与管理角色识别。
- hooks 页面 `/pb/manage/sdk-permission-manage` 负责维护角色与 collection CRUD 权限。
- 用户登录返回的是 PocketBase 原生可验证 token可直接给 PocketBase SDK 使用。

View File

@@ -1,106 +0,0 @@
# PocketBase 文档相关表结构
本方案新增 3 张 PocketBase `base collection`,统一采用业务 id 字段进行关联,不直接依赖 PocketBase 自动生成的 `id` 作为业务主键。
补充约定:
- `document_image``document_video``document_file` 只保存关联的 `attachments_id`,不直接存文件;当存在多个附件时,统一使用 `|` 分隔,例如:`ATT-001|ATT-002|ATT-003`
- `document_type` 使用多选字符串持久化,但格式特殊:`system_dict_id@dict_word_enum|system_dict_id@dict_word_enum`;前端显示时用枚举值描述,存库时保留该组合值。
- `document_keywords``document_product_categories``document_application_scenarios``document_hotel_type` 统一使用竖线分隔字符串,例如:`分类A|分类B|分类C`
- `document_status` 仅允许 `有效``过期` 两种值,并由系统根据生效日期与到期日期自动计算;当两者都为空时默认 `有效`
- `document_owner` 的业务含义为“上传者openid”。
- `attachments_link` 是 PocketBase `file` 字段,保存附件本体;访问链接由 PocketBase 根据记录和文件名生成。
- `tbl_attachments` 的文件查看/下载权限应保持公开;真正的业务访问控制交由引用 `attachments_id` 的业务表和业务接口决定。
- 文档字段中,面向用户填写的字段里只有 `document_title``document_type` 设为必填,其余字段均允许为空。
---
## 1. `tbl_attachments` 附件表
**类型:** Base Collection
| 字段名 | 类型 | 备注 |
| :--- | :--- | :--- |
| attachments_id | text | 附件业务 id唯一标识符 |
| attachments_link | file | 附件本体,单文件,不限制文件类型,数据库字段上限已放宽到约 4GB |
| attachments_filename | text | 原始文件名 |
| attachments_filetype | text | 文件类型/MIME |
| attachments_size | number | 附件大小 |
| attachments_owner | text | 上传者业务 id |
| attachments_md5 | text | 附件 MD5 码 |
| attachments_ocr | text | OCR 识别结果 |
| attachments_status | text | 附件状态 |
| attachments_remark | text | 备注 |
**索引规划:**
- `attachments_id` 唯一索引
- `attachments_owner` 普通索引
- `attachments_status` 普通索引
---
## 2. `tbl_document` 文档表
**类型:** Base Collection
| 字段名 | 类型 | 备注 |
| :--- | :--- | :--- |
| document_id | text | 文档业务 id唯一标识符 |
| document_effect_date | date | 文档生效日期 |
| document_expiry_date | date | 文档到期日期 |
| document_type | text | 文档类型,必填;多选时按 `system_dict_id@dict_word_enum|...` 保存 |
| document_title | text | 文档标题,必填 |
| document_subtitle | text | 文档副标题 |
| document_summary | text | 文档摘要 |
| document_content | text | 正文内容,保存 Markdown 原文 |
| document_image | text | 关联多个 `attachments_id`,使用 `|` 分隔 |
| document_video | text | 关联多个 `attachments_id`,使用 `|` 分隔 |
| document_file | text | 关联多个 `attachments_id`,使用 `|` 分隔 |
| document_owner | text | 上传者openid |
| document_relation_model | text | 关联机型/模型标识 |
| document_keywords | text | 关键词,多选后用 `|` 分隔保存 |
| document_share_count | number | 分享次数 |
| document_download_count | number | 下载次数 |
| document_favorite_count | number | 收藏次数 |
| document_status | text | 文档状态,仅允许 `有效``过期`,由系统依据生效日期和到期日期自动更新 |
| document_embedding_status | text | 文档嵌入状态 |
| document_embedding_error | text | 文档错误原因 |
| document_embedding_lasttime | date | 最后更新日期 |
| document_vector_version | text | 向量版本号或模型名称 |
| document_product_categories | text | 产品关联文档,多选后从 `文档-产品关联文档` 字典保存为 `|` 分隔字符串 |
| document_application_scenarios | text | 筛选依据,多选后从 `文档-筛选依据` 字典保存为 `|` 分隔字符串 |
| document_hotel_type | text | 适用场景,多选后从 `文档-适用场景` 字典保存为 `|` 分隔字符串 |
| document_remark | text | 备注 |
**索引规划:**
- `document_id` 唯一索引
- `document_owner` 普通索引
- `document_type` 普通索引
- `document_status` 普通索引
- `document_embedding_status` 普通索引
- `document_effect_date` 普通索引
- `document_expiry_date` 普通索引
---
## 3. `tbl_document_operation_history` 文档操作历史表
**类型:** Base Collection
| 字段名 | 类型 | 备注 |
| :--- | :--- | :--- |
| doh_id | text | 文档操作历史业务 id唯一标识符 |
| doh_document_id | text | 关联 `document_id` |
| doh_operation_type | text | 操作类型 |
| doh_user_id | text | 操作人业务 id |
| doh_current_count | number | 本次操作对应次数 |
| doh_remark | text | 备注 |
**索引规划:**
- `doh_id` 唯一索引
- `doh_document_id` 普通索引
- `doh_user_id` 普通索引
- `doh_operation_type` 普通索引

View File

@@ -0,0 +1,39 @@
# pb_tbl_attachments
> 来源:线上 PocketBase collection 回读、`script/pocketbase.documents.js`
> 类型:`base`
> 读写规则:公开可读;原生写权限与 hooks 管理权限仅管理员 / 管理角色允许
## 表用途
统一存储项目中的真实上传文件。业务表只保存 `attachments_id`,文件本体仅保存在本表 `attachments_link` 中。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `attachments_id` | `text` | 是 | 附件业务 ID |
| `attachments_link` | `file` | 否 | 文件本体,数据库限制已放宽到约 4GB |
| `attachments_filename` | `text` | 否 | 原始文件名 |
| `attachments_filetype` | `text` | 否 | 文件类型 / MIME |
| `attachments_size` | `number` | 否 | 文件大小 |
| `attachments_owner` | `text` | 否 | 上传者业务标识 |
| `attachments_md5` | `text` | 否 | 文件 MD5 |
| `attachments_ocr` | `text` | 否 | OCR 识别结果 |
| `attachments_status` | `text` | 否 | 附件状态 |
| `attachments_remark` | `text` | 否 | 备注 |
## 索引
| 索引名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `idx_tbl_attachments_attachments_id` | `UNIQUE INDEX` | 保证 `attachments_id` 唯一 |
| `idx_tbl_attachments_attachments_owner` | `INDEX` | 加速按上传者查询 |
| `idx_tbl_attachments_attachments_status` | `INDEX` | 加速按附件状态查询 |
## 补充约定
- 图片、视频、普通文件都统一走本表。
- 业务访问控制不放在本表,而由引用它的业务表与 hooks 接口控制。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -0,0 +1,28 @@
# pb_tbl_auth_resources
> 来源:线上 PocketBase collection 回读、`script/pocketbase.newpb.js`
> 类型:`base`
> 读写规则:仅管理员 / 管理角色允许
## 表用途
定义哪些表、哪些字段属于受控资源,是角色权限与用户覆盖权限的资源目录表。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `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` | 加速按表名查询 |
| `idx_tbl_auth_resources_res_type` | `INDEX` | 加速按资源类型查询 |
| `idx_tbl_auth_resources_unique_res` | `UNIQUE INDEX` | 保证 `table_name + column_name + res_type` 唯一 |

View File

@@ -0,0 +1,29 @@
# pb_tbl_auth_role_perms
> 来源:线上 PocketBase collection 回读、`script/pocketbase.newpb.js`
> 类型:`base`
> 读写规则:仅管理员 / 管理角色允许
## 表用途
用于存储角色与资源之间的权限映射,是按角色下发 PocketBase SDK CRUD 权限的基础表。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `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` | 加速按角色查询 |
| `idx_tbl_auth_role_perms_res_id` | `INDEX` | 加速按资源查询 |
| `idx_tbl_auth_role_perms_unique_map` | `UNIQUE INDEX` | 保证 `role_id + res_id` 唯一 |

28
docs/pb_tbl_auth_roles.md Normal file
View File

@@ -0,0 +1,28 @@
# pb_tbl_auth_roles
> 来源:线上 PocketBase collection 回读、`script/pocketbase.newpb.js`
> 类型:`base`
> 读写规则:仅管理员 / 管理角色允许
## 表用途
用于存储角色定义,供 SDK 直连权限管理页和用户授权流程使用。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `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` 唯一 |

View File

@@ -0,0 +1,29 @@
# pb_tbl_auth_row_scopes
> 来源:线上 PocketBase collection 回读、`script/pocketbase.newpb.js`
> 类型:`base`
> 读写规则:仅管理员 / 管理角色允许
## 表用途
用于定义按用户或按角色生效的行级过滤范围,是 ABAC / 行级权限的核心表。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `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` | 加速按目标类型查询 |
| `idx_tbl_auth_row_scopes_target_id` | `INDEX` | 加速按目标 ID 查询 |
| `idx_tbl_auth_row_scopes_table_name` | `INDEX` | 加速按作用表查询 |

View File

@@ -0,0 +1,29 @@
# pb_tbl_auth_user_overrides
> 来源:线上 PocketBase collection 回读、`script/pocketbase.newpb.js`
> 类型:`base`
> 读写规则:仅管理员 / 管理角色允许
## 表用途
用于给单个用户覆盖角色默认权限,实现细粒度例外授权。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `override_id` | `text` | 是 | 覆盖权限业务 ID |
| `users_convers_id` | `text` | 是 | 目标用户会话 ID |
| `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` | 加速按用户查询 |
| `idx_tbl_auth_user_overrides_res_id` | `INDEX` | 加速按资源查询 |
| `idx_tbl_auth_user_overrides_unique_map` | `UNIQUE INDEX` | 保证 `users_convers_id + res_id` 唯一 |

66
docs/pb_tbl_auth_users.md Normal file
View File

@@ -0,0 +1,66 @@
# pb_tbl_auth_users
> 来源:线上 PocketBase collection 回读、`script/pocketbase.newpb.js`
> 类型:`auth`
> 读写规则:仅管理员 / 管理角色允许
## 表用途
作为项目统一身份源,承载微信用户与管理平台用户,并负责向 PocketBase 原生鉴权体系签发可被 SDK 识别的 token。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `users_convers_id` | `text` | 否 | 会话侧用户 ID |
| `openid` | `text` | 是 | 全局身份锚点 |
| `org_id` | `number` | 否 | 组织 / 部门标识 |
| `users_rank_level` | `number` | 否 | 用户星级数值 |
| `users_status` | `text` | 否 | 用户状态 |
| `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` | 否 | 用户类型 |
| `company_id` | `text` | 否 | 所属公司业务 ID |
| `users_parent_id` | `text` | 否 | 上级用户业务 ID |
| `users_promo_code` | `text` | 否 | 推广码 |
| `users_picture` | `text` | 否 | 头像附件 ID |
| `usergroups_id` | `text` | 否 | 用户组 / 角色 ID |
| `password` | `password` | 是 | PocketBase 认证密码 |
| `tokenKey` | `text` | 是 | PocketBase token 签发关键字段 |
| `email` | `email` | 是 | PocketBase 认证邮箱 |
| `emailVisibility` | `bool` | 否 | 邮箱是否公开 |
| `verified` | `bool` | 否 | 邮箱是否验证 |
| `users_title_picture` | `text` | 否 | 资质附件 ID |
| `users_id_pic_a` | `text` | 否 | 证件照正面附件 ID |
| `users_id_pic_b` | `text` | 否 | 证件照反面附件 ID |
| `users_tag` | `text` | 否 | 用户标签 |
## 索引
| 索引名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `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` | 加速按组织查询 |
| `idx_tbl_auth_users_users_rank_level` | `INDEX` | 加速按等级查询 |
| `idx_tbl_auth_users_users_status` | `INDEX` | 加速按状态查询 |
| `idx_tbl_auth_users_users_auth_type` | `INDEX` | 加速按账户类型查询 |
| `idx_tbl_auth_users_users_phone` | `INDEX` | 加速按手机号查询 |
| `idx_tbl_auth_users_company_id` | `INDEX` | 加速按公司查询 |
| `idx_tbl_auth_users_usergroups_id` | `INDEX` | 加速按用户组查询 |
| `idx_tbl_auth_users_users_parent_id` | `INDEX` | 加速按上级查询 |
| `idx_tokenKey_pbc_421601843` | `UNIQUE INDEX` | PocketBase auth 系统索引 |
| `idx_email_pbc_421601843` | `UNIQUE INDEX` | PocketBase auth 系统索引,保证邮箱唯一 |
## 补充约定
- 本表为 `auth` collection除上述字段外还受 PocketBase 原生鉴权机制约束。
- 图片类字段统一只保存 `tbl_attachments.attachments_id`
- 登录接口返回的 token 来源于本表 auth record 的原生签发能力,可直接给 PocketBase SDK 使用。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

48
docs/pb_tbl_company.md Normal file
View File

@@ -0,0 +1,48 @@
# pb_tbl_company
> 来源:线上 PocketBase collection 回读、`script/pocketbase.js`
> 类型:`base`
> 读写规则:公开可创建、公开可列出;详情 / 更新 / 删除仅管理员或管理后台用户允许
## 表用途
用于存储公司主数据,并作为用户归属公司、微信端公司创建与原生 PocketBase 查询的基础表。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `company_id` | `text` | 是 | 公司业务 ID由数据库自动生成 |
| `company_name` | `text` | 否 | 公司名称 |
| `company_type` | `text` | 否 | 公司类型 |
| `company_entity` | `text` | 否 | 公司法人 |
| `company_usci` | `text` | 否 | 统一社会信用代码 |
| `company_nationality` | `text` | 否 | 国家名称 |
| `company_nationality_code` | `text` | 否 | 国家编码 |
| `company_province` | `text` | 否 | 省份名称 |
| `company_province_code` | `text` | 否 | 省份编码 |
| `company_city` | `text` | 否 | 城市名称 |
| `company_city_code` | `text` | 否 | 城市编码 |
| `company_district` | `text` | 否 | 区 / 县名称 |
| `company_district_code` | `text` | 否 | 区 / 县编码 |
| `company_postalcode` | `text` | 否 | 邮编 |
| `company_add` | `text` | 否 | 地址 |
| `company_status` | `text` | 否 | 公司状态 |
| `company_level` | `text` | 否 | 公司等级 |
| `company_owner_openid` | `text` | 否 | 公司所有者 openid |
| `company_remark` | `text` | 否 | 备注 |
## 索引
| 索引名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `idx_company_id` | `UNIQUE INDEX` | 保证 `company_id` 唯一 |
| `idx_company_usci` | `INDEX` | 加速按统一社会信用代码查询 |
| `idx_company_owner_openid` | `INDEX` | 加速按公司所有者查询 |
## 补充约定
- 微信端原生 PocketBase 接口支持公开创建公司记录。
- `company_id` 已切换为数据库自动生成,客户端不再需要提交。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

62
docs/pb_tbl_document.md Normal file
View File

@@ -0,0 +1,62 @@
# pb_tbl_document
> 来源:线上 PocketBase collection 回读、`script/pocketbase.documents.js`
> 类型:`base`
> 读写规则:公开可读;新增 / 修改 / 删除仅 `ManagePlatform` / 管理角色允许
## 表用途
用于存储文档主体信息,以及图片、视频、文件三类附件的关联关系。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `document_id` | `text` | 是 | 文档业务 ID |
| `document_effect_date` | `date` | 否 | 文档生效日期 |
| `document_expiry_date` | `date` | 否 | 文档到期日期 |
| `document_type` | `text` | 是 | 文档类型,多选时按 `system_dict_id@dict_word_enum|...` 保存 |
| `document_title` | `text` | 是 | 文档标题 |
| `document_subtitle` | `text` | 否 | 文档副标题 |
| `document_summary` | `text` | 否 | 文档摘要 |
| `document_content` | `text` | 否 | 正文内容,保存 Markdown |
| `document_image` | `text` | 否 | 图片附件 ID 集合,底层以 `|` 分隔 |
| `document_video` | `text` | 否 | 视频附件 ID 集合,底层以 `|` 分隔 |
| `document_owner` | `text` | 否 | 上传者 openid |
| `document_relation_model` | `text` | 否 | 关联机型 / 模型标识 |
| `document_keywords` | `text` | 否 | 关键词,多选后以 `|` 分隔 |
| `document_share_count` | `number` | 否 | 分享次数 |
| `document_download_count` | `number` | 否 | 下载次数 |
| `document_favorite_count` | `number` | 否 | 收藏次数 |
| `document_status` | `text` | 否 | 文档状态,仅 `有效` / `过期` |
| `document_embedding_status` | `text` | 否 | 文档嵌入状态 |
| `document_embedding_error` | `text` | 否 | 文档嵌入错误原因 |
| `document_embedding_lasttime` | `date` | 否 | 最后一次嵌入更新时间 |
| `document_vector_version` | `text` | 否 | 向量版本号 / 模型名称 |
| `document_product_categories` | `text` | 否 | 产品关联文档,多选后以 `|` 分隔 |
| `document_application_scenarios` | `text` | 否 | 筛选依据,多选后以 `|` 分隔 |
| `document_hotel_type` | `text` | 否 | 适用场景,多选后以 `|` 分隔 |
| `document_remark` | `text` | 否 | 备注 |
| `document_file` | `text` | 否 | 普通文件附件 ID 集合,底层以 `|` 分隔 |
| `document_create` | `autodate` | 否 | 文档创建时间,由数据库自动生成 |
## 索引
| 索引名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `idx_tbl_document_document_id` | `UNIQUE INDEX` | 保证 `document_id` 唯一 |
| `idx_tbl_document_document_create` | `INDEX` | 加速按创建时间倒序查询 |
| `idx_tbl_document_document_owner` | `INDEX` | 加速按上传者查询 |
| `idx_tbl_document_document_type` | `INDEX` | 加速按文档类型查询 |
| `idx_tbl_document_document_status` | `INDEX` | 加速按文档状态查询 |
| `idx_tbl_document_document_embedding_status` | `INDEX` | 加速按嵌入状态查询 |
| `idx_tbl_document_document_effect_date` | `INDEX` | 加速按生效日期查询 |
| `idx_tbl_document_document_expiry_date` | `INDEX` | 加速按到期日期查询 |
## 补充约定
- 三类附件字段都只保存 `attachments_id`,真实文件统一在 `tbl_attachments`
- `document_create` 已作为原生 PocketBase 列表排序字段,推荐使用 `sort=-document_create`
- 面向用户填写的字段里,仅 `document_title``document_type` 必填,其余允许为空。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -0,0 +1,35 @@
# pb_tbl_document_operation_history
> 来源:线上 PocketBase collection 回读、`script/pocketbase.documents.js`
> 类型:`base`
> 读写规则:仅管理员 / 管理角色允许
## 表用途
用于记录文档的新增、修改、删除等操作历史。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `doh_id` | `text` | 是 | 文档操作历史业务 ID |
| `doh_document_id` | `text` | 是 | 关联文档业务 ID |
| `doh_operation_type` | `text` | 否 | 操作类型 |
| `doh_user_id` | `text` | 否 | 操作人业务 ID |
| `doh_current_count` | `number` | 否 | 本次操作对应次数 |
| `doh_remark` | `text` | 否 | 备注 |
## 索引
| 索引名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `idx_tbl_document_operation_history_doh_id` | `UNIQUE INDEX` | 保证 `doh_id` 唯一 |
| `idx_tbl_document_operation_history_doh_document_id` | `INDEX` | 加速按文档查询历史 |
| `idx_tbl_document_operation_history_doh_user_id` | `INDEX` | 加速按操作人查询 |
| `idx_tbl_document_operation_history_doh_operation_type` | `INDEX` | 加速按操作类型查询 |
## 补充约定
- 本表主要用于管理端审计与追溯,不对匿名用户开放。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -0,0 +1,38 @@
# pb_tbl_system_dict
> 来源:线上 PocketBase collection 回读、`script/pocketbase.js`、`script/pocketbase.dictionary.js`
> 类型:`base`
> 读写规则:公开可读;仅 `ManagePlatform` / 管理角色可写
## 表用途
用于存储系统字典与枚举组。当前项目中,文档类型、关键词、产品关联文档、筛选依据、适用场景等前端多选项都来自本表。
## 字段清单
| 字段名 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `id` | `text` | 是 | PocketBase 记录主键 |
| `system_dict_id` | `text` | 是 | 字典业务 ID |
| `dict_name` | `text` | 是 | 字典名称,当前按全局唯一维护 |
| `dict_word_enum` | `text` | 否 | 枚举值集合,底层以聚合字符串保存 |
| `dict_word_description` | `text` | 否 | 枚举描述集合,底层以聚合字符串保存 |
| `dict_word_is_enabled` | `bool` | 否 | 字典是否启用 |
| `dict_word_sort_order` | `text` | 否 | 枚举排序集合,底层以聚合字符串保存 |
| `dict_word_parent_id` | `text` | 否 | 父级字典业务 ID |
| `dict_word_remark` | `text` | 否 | 备注 |
| `dict_word_image` | `text` | 否 | 枚举图片附件 ID 集合,和枚举值一一对应 |
## 索引
| 索引名 | 类型 | 说明 |
| :--- | :--- | :--- |
| `idx_system_dict_id` | `UNIQUE INDEX` | 保证 `system_dict_id` 唯一 |
| `idx_dict_name` | `UNIQUE INDEX` | 保证 `dict_name` 唯一 |
| `idx_dict_word_parent_id` | `INDEX` | 加速父子字典查询 |
## 补充约定
- 业务返回时hooks 会把聚合字段转换成 `items[]` 结构,每个元素包含 `enum``description``image``imageUrl``sortOrder`
- 字典项图片本体统一存放在 `tbl_attachments`,本表只保存 `attachments_id`
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -1,190 +0,0 @@
# 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` | `text` | 否 | 保存 `tbl_attachments.attachments_id`,用于关联证件照正面 |
| `users_id_pic_b` | `text` | 否 | 保存 `tbl_attachments.attachments_id`,用于关联证件照反面 |
| `users_title_picture` | `text` | 否 | 保存 `tbl_attachments.attachments_id`,用于关联资质照片 |
| `users_picture` | `text` | 否 | 用户头像,保存 `tbl_attachments.attachments_id` |
| `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. 若你需要,我可以继续帮你再生成一份“更像数据库设计说明书”的版本,增加字段含义、业务用途、关联关系三列。

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-29

View File

@@ -0,0 +1,56 @@
## Overview
本次变更是一次“文档基线收敛 + OpenSpec 补记”。运行时 schema 已经完成修改,因此设计重点不是实现新逻辑,而是把现有真实状态沉淀成可检索、可归档、可继续维护的标准资料。
## Documentation Strategy
### Per-collection database docs
数据库结构文档统一拆成按 collection 命名的单文件:
- `pb_tbl_system_dict.md`
- `pb_tbl_company.md`
- `pb_tbl_attachments.md`
- `pb_tbl_document.md`
- `pb_tbl_document_operation_history.md`
- `pb_tbl_auth_users.md`
- `pb_tbl_auth_resources.md`
- `pb_tbl_auth_roles.md`
- `pb_tbl_auth_role_perms.md`
- `pb_tbl_auth_user_overrides.md`
- `pb_tbl_auth_row_scopes.md`
混合型老文档删除,避免一张表同时出现在多份说明中。
### Uniform format
每份文档统一包含以下章节:
1. 来源
2. 类型
3. 读写规则
4. 表用途
5. 字段清单
6. 索引
7. 补充约定
字段信息以线上实际 collection 回读为主,脚本为辅;这能规避脚本历史遗留误差。
## OpenSpec Recording Strategy
### pocketbase-native-data-access
记录当前项目允许直接走 PocketBase 原生 API / SDK 的边界:
- `tbl_company` 公开创建与公开列表 / 条件查询
- `tbl_system_dict` 公开读
- `tbl_document` 公开读
### pocketbase-schema-docs
记录数据库结构文档必须采用按表命名和统一格式的规范,避免未来继续出现零散说明。
## Validation
- 文档字段以线上 PocketBase collection 回读结果为准
- OpenSpec 变更在归档前完成 `openspec validate`

View File

@@ -0,0 +1,29 @@
## Why
近期项目连续完成了 PocketBase schema 扩展、原生公开查询、附件统一存储、文档字段扩展和用户字段扩展,但 `docs/` 中与数据库结构相关的文档仍然混合在少量总表文件里,命名和格式也不统一。与此同时,这批 schema 与原生数据访问能力还没有完整计入 OpenSpec后续维护会出现“线上结构、脚本结构、文档结构、OpenSpec 记录”四套口径不一致的问题。
## What Changes
-`docs/` 中数据库结构相关文档拆分为按 collection 命名的标准文件,统一采用 `pb_tbl_xxx.md` 命名。
- 统一数据库文档格式,按“来源 / 类型 / 读写规则 / 表用途 / 字段清单 / 索引 / 补充约定”组织内容。
- 补充此前未进入 OpenSpec 的 PocketBase 原生数据访问与 schema 更新记录,包括:
- `tbl_company` 的公开创建 / 列表 / 条件查询约定
- `tbl_system_dict``tbl_document` 的公开读规则
- `company_owner_openid`、地区编码字段、`users_tag``document_file``document_create` 等新增字段
- 归档本次规范化工作,作为后续数据库文档与原生 API 文档的基线。
## Capabilities
### New Capabilities
- `pocketbase-native-data-access`: 记录原生 PocketBase 公开读写边界与标准查询方式。
- `pocketbase-schema-docs`: 规定数据库结构文档的命名与组织方式。
### Modified Capabilities
- None.
## Impact
- 受影响目录:`docs/``openspec/`
- 不涉及新的运行时代码发布,但会影响后续 schema 变更和接口文档维护方式

View File

@@ -0,0 +1,34 @@
## ADDED Requirements
### Requirement: PocketBase native company access SHALL expose a stable public create and query boundary
The system SHALL allow native PocketBase clients to create company records publicly and to query company records through standard PocketBase records APIs, while privileged mutations remain restricted to management users.
#### Scenario: Public client creates company record
- **WHEN** a client calls the native PocketBase create-record API for `tbl_company`
- **THEN** the request SHALL succeed without requiring a management token
#### Scenario: Public client queries company list
- **WHEN** a client calls the native PocketBase list-records API for `tbl_company`
- **THEN** the request SHALL return company records using standard PocketBase pagination parameters
#### Scenario: Management user updates company record
- **WHEN** a client attempts to update or delete `tbl_company`
- **THEN** PocketBase SHALL allow the operation only for management users or administrators
### Requirement: Public dictionary and document reads SHALL be supported through native PocketBase APIs
The system SHALL allow native PocketBase clients to read `tbl_system_dict` and `tbl_document` records without requiring application hooks tokens, while write operations remain restricted.
#### Scenario: Public client reads dictionary records
- **WHEN** a client calls the native PocketBase records API for `tbl_system_dict`
- **THEN** list and view operations SHALL be readable without authentication
#### Scenario: Public client reads document records
- **WHEN** a client calls the native PocketBase records API for `tbl_document`
- **THEN** list and view operations SHALL be readable without authentication

View File

@@ -0,0 +1,29 @@
## ADDED Requirements
### Requirement: Database structure docs SHALL be maintained per PocketBase collection
The project SHALL document PocketBase schema structures in per-collection markdown files under `docs/`, and each file SHALL use the collection name in the filename.
#### Scenario: Collection doc is named by collection
- **WHEN** a PocketBase collection has a dedicated schema document
- **THEN** the document filename SHALL follow the `pb_tbl_xxx.md` naming convention
#### Scenario: Mixed schema docs are replaced
- **WHEN** older documents combine multiple unrelated collection structures in one file
- **THEN** the project SHALL replace them with per-collection docs to avoid conflicting schema descriptions
### Requirement: Database structure docs SHALL use a uniform format
Each PocketBase schema document SHALL use the same section structure so readers can quickly compare tables and verify field and index changes.
#### Scenario: Reader opens a schema document
- **WHEN** a reader opens any database structure document under `docs/`
- **THEN** the document SHALL include source, collection type, access rule summary, purpose, field table, index table, and supplementary notes
#### Scenario: Schema docs reflect live collection state
- **WHEN** the project documents a collection structure
- **THEN** the field list and rule summary SHALL be aligned with the current live PocketBase collection state whenever practical

View File

@@ -0,0 +1,19 @@
## 1. 数据库文档整理
- [x] 1.1 盘点 `docs/` 中和数据库结构直接相关的文档。
- [x] 1.2 将混合型结构文档拆分为按 collection 命名的标准文件。
- [x] 1.3 统一所有数据库结构文档格式。
- [x] 1.4 删除被替代的旧文档文件。
## 2. 基于线上结构回读修正文档
- [x] 2.1 回读线上 `tbl_system_dict``tbl_company``tbl_attachments``tbl_document``tbl_document_operation_history`
- [x] 2.2 回读线上 `tbl_auth_*` 系列 collection。
- [x] 2.3 以线上结果修正字段、索引和权限描述。
## 3. OpenSpec 补记与归档
- [x] 3.1 为未计入 OpenSpec 的原生数据访问与 schema 文档规范补建 change。
- [x] 3.2 为 `pocketbase-native-data-access` 编写 delta spec。
- [x] 3.3 为 `pocketbase-schema-docs` 编写 delta spec。
- [x] 3.4 校验并归档本次 change。

View File

@@ -0,0 +1,38 @@
# pocketbase-native-data-access Specification
## Purpose
TBD - created by archiving change normalize-pocketbase-schema-docs. Update Purpose after archive.
## Requirements
### Requirement: PocketBase native company access SHALL expose a stable public create and query boundary
The system SHALL allow native PocketBase clients to create company records publicly and to query company records through standard PocketBase records APIs, while privileged mutations remain restricted to management users.
#### Scenario: Public client creates company record
- **WHEN** a client calls the native PocketBase create-record API for `tbl_company`
- **THEN** the request SHALL succeed without requiring a management token
#### Scenario: Public client queries company list
- **WHEN** a client calls the native PocketBase list-records API for `tbl_company`
- **THEN** the request SHALL return company records using standard PocketBase pagination parameters
#### Scenario: Management user updates company record
- **WHEN** a client attempts to update or delete `tbl_company`
- **THEN** PocketBase SHALL allow the operation only for management users or administrators
### Requirement: Public dictionary and document reads SHALL be supported through native PocketBase APIs
The system SHALL allow native PocketBase clients to read `tbl_system_dict` and `tbl_document` records without requiring application hooks tokens, while write operations remain restricted.
#### Scenario: Public client reads dictionary records
- **WHEN** a client calls the native PocketBase records API for `tbl_system_dict`
- **THEN** list and view operations SHALL be readable without authentication
#### Scenario: Public client reads document records
- **WHEN** a client calls the native PocketBase records API for `tbl_document`
- **THEN** list and view operations SHALL be readable without authentication

View File

@@ -0,0 +1,33 @@
# pocketbase-schema-docs Specification
## Purpose
TBD - created by archiving change normalize-pocketbase-schema-docs. Update Purpose after archive.
## Requirements
### Requirement: Database structure docs SHALL be maintained per PocketBase collection
The project SHALL document PocketBase schema structures in per-collection markdown files under `docs/`, and each file SHALL use the collection name in the filename.
#### Scenario: Collection doc is named by collection
- **WHEN** a PocketBase collection has a dedicated schema document
- **THEN** the document filename SHALL follow the `pb_tbl_xxx.md` naming convention
#### Scenario: Mixed schema docs are replaced
- **WHEN** older documents combine multiple unrelated collection structures in one file
- **THEN** the project SHALL replace them with per-collection docs to avoid conflicting schema descriptions
### Requirement: Database structure docs SHALL use a uniform format
Each PocketBase schema document SHALL use the same section structure so readers can quickly compare tables and verify field and index changes.
#### Scenario: Reader opens a schema document
- **WHEN** a reader opens any database structure document under `docs/`
- **THEN** the document SHALL include source, collection type, access rule summary, purpose, field table, index table, and supplementary notes
#### Scenario: Schema docs reflect live collection state
- **WHEN** the project documents a collection structure
- **THEN** the field list and rule summary SHALL be aligned with the current live PocketBase collection state whenever practical

View File

@@ -23,6 +23,7 @@ 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/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/login.js`)
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/platform/register.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/platform/register.js`)
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/company/records-create.js`)
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/list.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/list.js`)
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/detail.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/detail.js`)
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/create.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/create.js`)

View File

@@ -0,0 +1,17 @@
onRecordAfterCreateSuccess((e) => {
try {
const authRecord = e && e.auth ? e.auth : null
if (!authRecord || authRecord.collection().name !== 'tbl_auth_users') {
return
}
if (authRecord.getString('users_type') === '服务商') {
return
}
authRecord.set('users_type', '服务商')
$app.save(authRecord)
} catch (_error) {
// Keep company create flow unchanged even if user type sync fails.
}
}, 'tbl_company')

View File

@@ -6,7 +6,6 @@ routerAdd('POST', '/api/dictionary/detail', function (e) {
try { try {
guards.requireJson(e) guards.requireJson(e)
guards.requireManagePlatformUser(e)
const payload = guards.validateDictionaryDetailBody(e) const payload = guards.validateDictionaryDetailBody(e)
const data = dictionaryService.getDictionaryByName(payload.dict_name) const data = dictionaryService.getDictionaryByName(payload.dict_name)

View File

@@ -6,7 +6,6 @@ routerAdd('POST', '/api/dictionary/list', function (e) {
try { try {
guards.requireJson(e) guards.requireJson(e)
guards.requireManagePlatformUser(e)
const payload = guards.validateDictionaryListBody(e) const payload = guards.validateDictionaryListBody(e)
const data = dictionaryService.listDictionaries(payload.keyword) const data = dictionaryService.listDictionaries(payload.keyword)

View File

@@ -6,7 +6,5 @@ module.exports = {
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo', POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo',
WECHAT_APPID: 'wx3bd7a7b19679da7a', WECHAT_APPID: 'wx3bd7a7b19679da7a',
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1', WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
/* WECHAT_APPID: 'wx42e9add0f91af98b',
WECHAT_SECRET: '5620f00b40297efaf3d197d61ae184d6', */
BUILD_TIME: '', BUILD_TIME: '',
} }

View File

@@ -25,13 +25,12 @@ function validateLoginBody(e) {
function validateProfileBody(e) { function validateProfileBody(e) {
const payload = parseBody(e) const payload = parseBody(e)
if (!payload.users_name) throw createAppError(400, 'users_name 为必填项')
if (!payload.users_phone_code) throw createAppError(400, 'users_phone_code 为必填项')
if (!payload.users_picture) throw createAppError(400, 'users_picture 为必填项')
return { return {
users_name: payload.users_name, users_name: Object.prototype.hasOwnProperty.call(payload, 'users_name') ? String(payload.users_name || '').trim() : undefined,
users_phone_code: payload.users_phone_code, users_phone_code: Object.prototype.hasOwnProperty.call(payload, 'users_phone_code') ? String(payload.users_phone_code || '').trim() : undefined,
users_picture: payload.users_picture, users_phone: Object.prototype.hasOwnProperty.call(payload, 'users_phone') ? String(payload.users_phone || '').trim() : undefined,
users_tag: Object.prototype.hasOwnProperty.call(payload, 'users_tag') ? String(payload.users_tag || '').trim() : undefined,
users_picture: Object.prototype.hasOwnProperty.call(payload, 'users_picture') ? String(payload.users_picture || '').trim() : undefined,
users_id_pic_a: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_a') ? payload.users_id_pic_a : undefined, users_id_pic_a: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_a') ? payload.users_id_pic_a : undefined,
users_id_pic_b: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_b') ? payload.users_id_pic_b : undefined, users_id_pic_b: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_b') ? payload.users_id_pic_b : undefined,
users_title_picture: Object.prototype.hasOwnProperty.call(payload, 'users_title_picture') ? payload.users_title_picture : undefined, users_title_picture: Object.prototype.hasOwnProperty.call(payload, 'users_title_picture') ? payload.users_title_picture : undefined,
@@ -60,6 +59,7 @@ function validatePlatformRegisterBody(e) {
users_id_number: payload.users_id_number || '', users_id_number: payload.users_id_number || '',
users_level: payload.users_level || '', users_level: payload.users_level || '',
users_type: payload.users_type || '', users_type: payload.users_type || '',
users_tag: payload.users_tag || '',
company_id: payload.company_id || '', company_id: payload.company_id || '',
users_parent_id: payload.users_parent_id || '', users_parent_id: payload.users_parent_id || '',
users_promo_code: payload.users_promo_code || '', users_promo_code: payload.users_promo_code || '',

View File

@@ -206,6 +206,7 @@ function exportDocumentRecord(record) {
return { return {
pb_id: record.id, pb_id: record.id,
document_id: record.getString('document_id'), document_id: record.getString('document_id'),
document_create: String(record.get('document_create') || ''),
document_effect_date: String(record.get('document_effect_date') || ''), document_effect_date: String(record.get('document_effect_date') || ''),
document_expiry_date: String(record.get('document_expiry_date') || ''), document_expiry_date: String(record.get('document_expiry_date') || ''),
document_type: record.getString('document_type'), document_type: record.getString('document_type'),

View File

@@ -122,7 +122,30 @@ function getCompanyByCompanyId(companyId) {
function exportCompany(companyRecord) { function exportCompany(companyRecord) {
if (!companyRecord) return null if (!companyRecord) return null
return companyRecord.publicExport() return {
pb_id: companyRecord.id,
company_id: companyRecord.getString('company_id'),
company_name: companyRecord.getString('company_name'),
company_type: companyRecord.getString('company_type'),
company_entity: companyRecord.getString('company_entity'),
company_usci: companyRecord.getString('company_usci'),
company_nationality: companyRecord.getString('company_nationality'),
company_nationality_code: companyRecord.getString('company_nationality_code'),
company_province: companyRecord.getString('company_province'),
company_province_code: companyRecord.getString('company_province_code'),
company_city: companyRecord.getString('company_city'),
company_city_code: companyRecord.getString('company_city_code'),
company_district: companyRecord.getString('company_district'),
company_district_code: companyRecord.getString('company_district_code'),
company_postalcode: companyRecord.getString('company_postalcode'),
company_add: companyRecord.getString('company_add'),
company_status: companyRecord.getString('company_status'),
company_level: companyRecord.getString('company_level'),
company_owner_openid: companyRecord.getString('company_owner_openid'),
company_remark: companyRecord.getString('company_remark'),
created: String(companyRecord.created || ''),
updated: String(companyRecord.updated || ''),
}
} }
function resolveUserAttachment(attachmentId) { function resolveUserAttachment(attachmentId) {
@@ -167,15 +190,23 @@ function ensureAttachmentIdExists(attachmentId, fieldName) {
function applyUserAttachmentFields(record, payload) { function applyUserAttachmentFields(record, payload) {
if (!payload) return if (!payload) return
record.set('users_picture', ensureAttachmentIdExists(payload.users_picture || '', 'users_picture')) if (typeof payload.users_picture !== 'undefined' && payload.users_picture) {
record.set('users_picture', ensureAttachmentIdExists(payload.users_picture, 'users_picture'))
}
if (typeof payload.users_id_pic_a !== 'undefined') { if (typeof payload.users_id_pic_a !== 'undefined') {
record.set('users_id_pic_a', ensureAttachmentIdExists(payload.users_id_pic_a || '', 'users_id_pic_a')) if (payload.users_id_pic_a) {
record.set('users_id_pic_a', ensureAttachmentIdExists(payload.users_id_pic_a, 'users_id_pic_a'))
}
} }
if (typeof payload.users_id_pic_b !== 'undefined') { if (typeof payload.users_id_pic_b !== 'undefined') {
record.set('users_id_pic_b', ensureAttachmentIdExists(payload.users_id_pic_b || '', 'users_id_pic_b')) if (payload.users_id_pic_b) {
record.set('users_id_pic_b', ensureAttachmentIdExists(payload.users_id_pic_b, 'users_id_pic_b'))
}
} }
if (typeof payload.users_title_picture !== 'undefined') { if (typeof payload.users_title_picture !== 'undefined') {
record.set('users_title_picture', ensureAttachmentIdExists(payload.users_title_picture || '', 'users_title_picture')) if (payload.users_title_picture) {
record.set('users_title_picture', ensureAttachmentIdExists(payload.users_title_picture, 'users_title_picture'))
}
} }
} }
@@ -202,6 +233,7 @@ function enrichUser(userRecord) {
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_level: userRecord.getString('users_level'),
users_tag: userRecord.getString('users_tag'),
users_picture: userPicture.id, users_picture: userPicture.id,
users_picture_url: userPicture.url, users_picture_url: userPicture.url,
users_picture_attachment: userPicture.attachment, users_picture_attachment: userPicture.attachment,
@@ -366,6 +398,7 @@ function registerPlatformUser(payload) {
record.set('users_id_number', payload.users_id_number || '') record.set('users_id_number', payload.users_id_number || '')
record.set('users_level', payload.users_level || '') record.set('users_level', payload.users_level || '')
record.set('users_type', payload.users_type || REGISTERED_USER_TYPE) record.set('users_type', payload.users_type || REGISTERED_USER_TYPE)
record.set('users_tag', payload.users_tag || '')
record.set('company_id', payload.company_id || '') record.set('company_id', payload.company_id || '')
record.set('users_parent_id', payload.users_parent_id || '') record.set('users_parent_id', payload.users_parent_id || '')
record.set('users_promo_code', payload.users_promo_code || '') record.set('users_promo_code', payload.users_promo_code || '')
@@ -481,7 +514,12 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
throw createAppError(404, '未找到待编辑的用户') throw createAppError(404, '未找到待编辑的用户')
} }
const usersPhone = wechatService.getWxPhoneNumber(payload.users_phone_code) let usersPhone = ''
if (payload.users_phone_code) {
usersPhone = wechatService.getWxPhoneNumber(payload.users_phone_code)
} else if (payload.users_phone) {
usersPhone = String(payload.users_phone || '').trim()
}
if (usersPhone && usersPhone !== currentUser.getString('users_phone')) { if (usersPhone && usersPhone !== currentUser.getString('users_phone')) {
const samePhoneUsers = $app.findRecordsByFilter('tbl_auth_users', 'users_phone = {:phone}', '', 10, 0, { const samePhoneUsers = $app.findRecordsByFilter('tbl_auth_users', 'users_phone = {:phone}', '', 10, 0, {
@@ -494,15 +532,19 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
} }
} }
const shouldPromote = isAllProfileFieldsEmpty(currentUser) if (payload.users_name) {
&& !!payload.users_name
&& !!usersPhone
&& !!payload.users_picture
&& ((currentUser.getString('users_type') || GUEST_USER_TYPE) === GUEST_USER_TYPE)
currentUser.set('users_name', payload.users_name) currentUser.set('users_name', payload.users_name)
}
if (usersPhone) {
currentUser.set('users_phone', usersPhone) currentUser.set('users_phone', usersPhone)
}
if (typeof payload.users_tag !== 'undefined' && payload.users_tag) {
currentUser.set('users_tag', payload.users_tag)
}
applyUserAttachmentFields(currentUser, payload) applyUserAttachmentFields(currentUser, payload)
const shouldPromote = ((currentUser.getString('users_type') || GUEST_USER_TYPE) === GUEST_USER_TYPE)
&& isInfoComplete(currentUser)
if (shouldPromote) { if (shouldPromote) {
currentUser.set('users_type', REGISTERED_USER_TYPE) currentUser.set('users_type', REGISTERED_USER_TYPE)
} }

View File

@@ -65,12 +65,15 @@ routerAdd('GET', '/manage/document-manage', function (e) {
.dropzone-title { font-weight: 700; color: #1e3a8a; } .dropzone-title { font-weight: 700; color: #1e3a8a; }
.dropzone-text { color: #475569; font-size: 13px; margin-top: 6px; } .dropzone-text { color: #475569; font-size: 13px; margin-top: 6px; }
.dropzone input[type="file"] { margin-top: 12px; } .dropzone input[type="file"] { margin-top: 12px; }
.file-list { display: grid; gap: 10px; margin-top: 12px; } .file-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
.file-card { border: 1px solid #dbe3f0; border-radius: 14px; padding: 12px; background: #fff; } .file-card { min-width: 0; min-height: 122px; border: 1px solid #dbe3f0; border-radius: 14px; padding: 8px; background: #fff; display: flex; flex-direction: column; gap: 6px; }
.file-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .file-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.file-card-title { font-weight: 700; word-break: break-all; } .file-card-title { flex: 1; font-weight: 700; font-size: 12px; line-height: 1.3; word-break: break-all; }
.file-meta { color: #64748b; font-size: 12px; margin-top: 6px; word-break: break-all; } .file-card-icon { width: 24px; height: 24px; border-radius: 999px; border: 1px solid #dbe3f0; background: #fff; color: #475569; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; font-size: 14px; padding: 0; }
.thumb { width: 72px; height: 72px; border-radius: 12px; object-fit: cover; border: 1px solid #dbe3f0; background: #fff; cursor: zoom-in; } .file-card-icon:hover { border-color: #94a3b8; background: #f8fafc; }
.file-meta { display: none; }
.thumb { width: 60px; height: 60px; border-radius: 10px; object-fit: cover; border: 1px solid #dbe3f0; background: #fff; cursor: zoom-in; }
.file-preview { width: 60px; height: 60px; border-radius: 10px; border: 1px solid #dbe3f0; background: #f8fafc; color: #334155; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; }
.thumb-strip { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .thumb-strip { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.image-viewer { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.82); z-index: 9999; } .image-viewer { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.82); z-index: 9999; }
.image-viewer.show { display: flex; } .image-viewer.show { display: flex; }
@@ -85,6 +88,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
@media (max-width: 960px) { @media (max-width: 960px) {
.grid, .file-group { grid-template-columns: 1fr; } .grid, .file-group { grid-template-columns: 1fr; }
.option-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } .option-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.file-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
table, thead, tbody, th, td, tr { display: block; } table, thead, tbody, th, td, tr { display: block; }
thead { display: none; } thead { display: none; }
tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; } tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
@@ -526,15 +530,46 @@ routerAdd('GET', '/manage/document-manage', function (e) {
return sourceId ? (state.dictionariesById[sourceId] || null) : null return sourceId ? (state.dictionariesById[sourceId] || null) : null
} }
function normalizeDocumentTypeEnumValue(value) {
const raw = String(value || '').trim()
if (!raw) {
return ''
}
const separatorIndex = raw.indexOf('@')
return separatorIndex === -1 ? raw : raw.slice(separatorIndex + 1)
}
function buildDocumentTypeStorageValue() { function buildDocumentTypeStorageValue() {
const sourceDict = getDocumentTypeSourceDictionary() const sourceDict = getDocumentTypeSourceDictionary()
if (!sourceDict) { if (!sourceDict) {
return '' return ''
} }
return joinPipeValue(state.selections.documentTypeValues.map(function (enumValue) { const sourceId = String(sourceDict.system_dict_id || '').trim()
return sourceDict.system_dict_id + '@' + enumValue if (!sourceId) {
})) return ''
}
const seen = {}
const parts = []
state.selections.documentTypeValues.forEach(function (value) {
const enumValue = normalizeDocumentTypeEnumValue(value)
if (!enumValue) {
return
}
const token = sourceId + '@' + enumValue
if (seen[token]) {
return
}
seen[token] = true
parts.push(token)
})
return joinPipeValue(parts)
} }
function updateSelection(fieldName, value, checked) { function updateSelection(fieldName, value, checked) {
@@ -660,7 +695,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
if (state.mode === 'edit') { if (state.mode === 'edit') {
formTitleEl.textContent = '编辑文档' formTitleEl.textContent = '编辑文档'
editorModeEl.textContent = '当前模式:编辑 ' + state.editingId editorModeEl.textContent = '当前模式:编辑 ' + (state.editingSource && state.editingSource.document_title ? state.editingSource.document_title : state.editingId)
document.getElementById('submitBtn').textContent = '保存文档修改' document.getElementById('submitBtn').textContent = '保存文档修改'
} else if (state.mode === 'create') { } else if (state.mode === 'create') {
formTitleEl.textContent = '新增文档' formTitleEl.textContent = '新增文档'
@@ -826,26 +861,17 @@ routerAdd('GET', '/manage/document-manage', function (e) {
const title = pending const title = pending
? (item.file && item.file.name ? item.file.name : ('待上传文件' + (index + 1))) ? (item.file && item.file.name ? item.file.name : ('待上传文件' + (index + 1)))
: (item.attachments_filename || item.attachments_id || ('附件' + (index + 1))) : (item.attachments_filename || item.attachments_id || ('附件' + (index + 1)))
const meta = pending const previewUrl = getAttachmentPreviewUrl(item, pending)
? ('大小:' + String(item.file && item.file.size ? item.file.size : 0) + ' bytes')
: ('ID' + escapeHtml(item.attachments_id || '') + (item.attachments_filetype ? ' / ' + escapeHtml(item.attachments_filetype) : ''))
const linkHtml = pending || !item.attachments_url
? ''
: '<a href="' + escapeHtml(item.attachments_url) + '" target="_blank" rel="noreferrer">打开文件</a>'
const previewHtml = category === 'image' const previewHtml = category === 'image'
? renderImageThumb(getAttachmentPreviewUrl(item, pending), title) ? renderImageThumb(previewUrl, title)
: '' : (previewUrl
const actionLabel = pending ? '移除待上传' : '从文档移除' ? ('<a class="file-preview" href="' + escapeHtml(previewUrl) + '" target="_blank" rel="noreferrer">' + (category === 'video' ? '🎬' : '📄') + '</a>')
: ('<div class="file-preview">' + (category === 'video' ? '🎬' : '📄') + '</div>'))
const handler = pending ? '__removePendingAttachment' : '__removeCurrentAttachment' const handler = pending ? '__removePendingAttachment' : '__removeCurrentAttachment'
return '<div class="file-card">' return '<div class="file-card">'
+ '<div class="file-card-head"><div class="file-card-title">' + escapeHtml(title) + '</div></div>' + '<div class="file-card-head"><div class="file-card-title">' + escapeHtml(title) + '</div><button class="file-card-icon" type="button" title="移除" onclick="window.' + handler + '(\\'' + category + '\\',' + index + ')">×</button></div>'
+ previewHtml + previewHtml
+ '<div class="file-meta">' + meta + '</div>'
+ '<div class="file-actions">'
+ linkHtml
+ '<button class="btn btn-light" type="button" onclick="window.' + handler + '(\\'' + category + '\\',' + index + ')">' + actionLabel + '</button>'
+ '</div>'
+ '</div>' + '</div>'
}).join('') }).join('')
} }
@@ -860,27 +886,13 @@ routerAdd('GET', '/manage/document-manage', function (e) {
} }
function renderLinks(item) { function renderLinks(item) {
const links = []
const imageThumbs = []
const imageUrls = Array.isArray(item.document_image_urls) ? item.document_image_urls : (item.document_image_url ? [item.document_image_url] : []) const imageUrls = Array.isArray(item.document_image_urls) ? item.document_image_urls : (item.document_image_url ? [item.document_image_url] : [])
const videoUrls = Array.isArray(item.document_video_urls) ? item.document_video_urls : (item.document_video_url ? [item.document_video_url] : [])
const fileUrls = Array.isArray(item.document_file_urls) ? item.document_file_urls : (item.document_file_url ? [item.document_file_url] : [])
for (let i = 0; i < imageUrls.length; i += 1) { if (!imageUrls.length) {
links.push('<a href="' + escapeHtml(imageUrls[i]) + '" target="_blank" rel="noreferrer">图片流' + (i + 1) + '</a>')
imageThumbs.push(renderImageThumb(imageUrls[i], '图片流' + (i + 1)))
}
for (let i = 0; i < videoUrls.length; i += 1) {
links.push('<a href="' + escapeHtml(videoUrls[i]) + '" target="_blank" rel="noreferrer">视频流' + (i + 1) + '</a>')
}
for (let i = 0; i < fileUrls.length; i += 1) {
links.push('<a href="' + escapeHtml(fileUrls[i]) + '" target="_blank" rel="noreferrer">文件流' + (i + 1) + '</a>')
}
if (!links.length) {
return '<span class="muted">无</span>' return '<span class="muted">无</span>'
} }
return '<div class="doc-links">' + links.join('') + '</div>'
+ (imageThumbs.length ? '<div class="thumb-strip">' + imageThumbs.join('') + '</div>' : '') return '<div class="thumb-strip">' + renderImageThumb(imageUrls[0], '文档图片预览') + '</div>'
} }
function renderTable() { function renderTable() {
@@ -1132,7 +1144,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
fillFormFromItem(target) fillFormFromItem(target)
updateEditorMode() updateEditorMode()
renderAttachmentEditors() renderAttachmentEditors()
setStatus('已进入编辑模式:' + target.document_id, 'success') setStatus('已进入编辑模式:' + (target.document_title || target.document_id), 'success')
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
} }

View File

@@ -1,472 +0,0 @@
# 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` 改为非必填。
- 其余业务字段保持非必填。
### 4. 用户图片字段统一改为附件 ID 语义
- `users_picture`
- `users_id_pic_a`
- `users_id_pic_b`
- `users_title_picture`
以上字段已统一改为保存 `tbl_attachments.attachments_id`,不再直接保存 PocketBase `file` 字段或外部图片 URL。
查询用户信息时hooks 会自动联查 `tbl_attachments` 并补充:
- `users_picture_url`
- `users_id_pic_a_url`
- `users_id_pic_b_url`
- `users_title_picture_url`
说明:
- `tbl_attachments` 仍由附件表保存实际文件本体;
- 业务表仅负责保存附件 ID
- hooks 中原有 `ManagePlatform` 访问限制保持不变。
---
## 三、查询与排序修复
### 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_image``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_image``dict_word_sort_order` 已统一改为 JSON 字符串持久化,其中 `dict_word_sort_order` 已改为 `text` 类型。
- 字典项图片需先调用 `/pb/api/attachment/upload` 上传,再把返回的 `attachments_id` 写入 `dict_word_image` 对应位置。
- 查询时统一聚合为:`items: [{ enum, description, image, imageUrl, imageAttachment, 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``document_file` 中的任一附件列表引用,则拒绝删除
说明:
- `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_file_urls`
- `document_image_attachments`
- `document_video_attachments`
- `document_file_attachments`
- `POST /pb/api/document/detail`
-`document_id` 查询单条文档
- 返回与附件表联动解析后的多文件流链接
- `POST /pb/api/document/create`
- 新增文档
- `document_id` 可不传,由服务端自动生成
- `document_title``document_type` 为必填;其余字段均允许为空
- `document_image``document_video``document_file` 支持传入多个已存在的 `attachments_id`
- `document_type` 前端从单个字典来源中多选枚举值,最终按 `system_dict_id@dict_word_enum|...` 保存
- `document_keywords``document_product_categories``document_application_scenarios``document_hotel_type` 统一从固定字典多选并按 `|` 保存
- 其中 `document_product_categories` 改为从 `文档-产品关联文档` 读取,`document_application_scenarios` 改为从 `文档-筛选依据` 读取,`document_hotel_type` 改为从 `文档-适用场景` 读取
- `document_status` 仅保留 `有效` / `过期` 两种状态,并由生效日期与到期日期自动计算
- 成功后会写入一条文档操作历史,类型为 `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``document_file` 当前保存的是多个 `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` 模糊搜索
- 指定字典查询
- 行内编辑基础字段
- 弹窗编辑枚举项
- 为每个枚举项单独上传图片,并保存对应 `attachments_id`
- 回显字典项图片缩略图与文件流链接
- 新增 / 删除字典
- 返回主页
- 文档管理页支持:
- 先上传附件到 `tbl_attachments`
- 再把返回的多个 `attachments_id` 写入 `tbl_document.document_image` / `document_video` / `document_file`
- 图片、视频、文件都支持多选上传
- 新增文档
- 编辑已有文档并回显多图片、多视频
- 从文档中移除附件并在保存后删除对应附件记录
- 查询文档列表
- 直接展示 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 阶段性归档基线继续维护。

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,8 @@ info:
本文档面向小程序端直接使用 PocketBase JS SDK / REST API 访问 `tbl_company`。 本文档面向小程序端直接使用 PocketBase JS SDK / REST API 访问 `tbl_company`。
本文档统一以 PocketBase 原生记录主键 `id` 作为唯一识别键。 本文档统一以 PocketBase 原生记录主键 `id` 作为唯一识别键。
`company_id` 保留为普通业务字段,可用于展示、筛选和业务关联,但不再作为 CRUD 的唯一键。 `company_id` 保留为普通业务字段,可用于展示、筛选和业务关联,但不再作为 CRUD 的唯一键。
当前线上 `tbl_company` 还包含 `company_owner_openid` 字段,用于保存公司所有者 openid并带普通索引。
同时新增了国家、省、市、区的名称与编码字段,便于前端直接按行政区划存取。
license: license:
name: Proprietary name: Proprietary
identifier: LicenseRef-Proprietary identifier: LicenseRef-Proprietary
@@ -29,7 +31,8 @@ paths:
支持三种常见模式: 支持三种常见模式:
1. 全表查询:不传 `filter` 1. 全表查询:不传 `filter`
2. 精确查询:`filter=id="q1w2e3r4t5y6u7i"` 2. 精确查询:`filter=id="q1w2e3r4t5y6u7i"`
3. 模糊查询:`filter=(company_name~"华住" || company_usci~"9131" || company_entity~"张三")` 3. 模糊查询:`filter=(company_name~"华住" || company_usci~"9131" || company_entity~"张三")`
4. 按 `company_id` 查询单条:`filter=company_id="WX-COMPANY-10001"&perPage=1&page=1`。
parameters: parameters:
- $ref: '#/components/parameters/Page' - $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage' - $ref: '#/components/parameters/PerPage'
@@ -64,12 +67,18 @@ paths:
company_entity: 张三 company_entity: 张三
company_usci: '91310000123456789A' company_usci: '91310000123456789A'
company_nationality: 中国 company_nationality: 中国
company_nationality_code: CN
company_province: 上海 company_province: 上海
company_province_code: '310000'
company_city: 上海 company_city: 上海
company_city_code: '310100'
company_district: 浦东新区
company_district_code: '310115'
company_postalcode: '200000' company_postalcode: '200000'
company_add: 上海市浦东新区XX路1号 company_add: 上海市浦东新区XX路1号
company_status: 有效 company_status: 有效
company_level: A company_level: A
company_owner_openid: wx-openid-owner-001
company_remark: '' company_remark: ''
exact: exact:
summary: 按 id 精确查询 summary: 按 id 精确查询
@@ -90,12 +99,18 @@ paths:
company_entity: 张三 company_entity: 张三
company_usci: '91310000123456789A' company_usci: '91310000123456789A'
company_nationality: 中国 company_nationality: 中国
company_nationality_code: CN
company_province: 上海 company_province: 上海
company_province_code: '310000'
company_city: 上海 company_city: 上海
company_city_code: '310100'
company_district: 浦东新区
company_district_code: '310115'
company_postalcode: '200000' company_postalcode: '200000'
company_add: 上海市浦东新区XX路1号 company_add: 上海市浦东新区XX路1号
company_status: 有效 company_status: 有效
company_level: A company_level: A
company_owner_openid: wx-openid-owner-001
company_remark: '' company_remark: ''
'400': '400':
description: 过滤表达式或查询参数不合法 description: 过滤表达式或查询参数不合法
@@ -115,7 +130,8 @@ paths:
summary: 新增公司 summary: 新增公司
description: >- description: >-
创建一条 `tbl_company` 记录。当前文档以 `id` 作为记录唯一识别键, 创建一条 `tbl_company` 记录。当前文档以 `id` 作为记录唯一识别键,
新建成功后由 PocketBase 自动生成 `id`根据当前项目建表脚本,`company_id` 仍是必填业务字段,但不再作为 CRUD 唯一键。 新建成功后由 PocketBase 自动生成 `id``company_id` 也由数据库自动生成,
客户端创建时不需要传入,但仍可作为后续业务查询字段。
requestBody: requestBody:
required: true required: true
content: content:
@@ -125,18 +141,23 @@ paths:
examples: examples:
default: default:
value: value:
company_id: C10001
company_name: 宝镜科技 company_name: 宝镜科技
company_type: 渠道商 company_type: 渠道商
company_entity: 张三 company_entity: 张三
company_usci: '91310000123456789A' company_usci: '91310000123456789A'
company_nationality: 中国 company_nationality: 中国
company_nationality_code: CN
company_province: 上海 company_province: 上海
company_province_code: '310000'
company_city: 上海 company_city: 上海
company_city_code: '310100'
company_district: 浦东新区
company_district_code: '310115'
company_postalcode: '200000' company_postalcode: '200000'
company_add: 上海市浦东新区XX路1号 company_add: 上海市浦东新区XX路1号
company_status: 有效 company_status: 有效
company_level: A company_level: A
company_owner_openid: wx-openid-owner-001
company_remark: 首次创建 company_remark: 首次创建
responses: responses:
'200': '200':
@@ -198,6 +219,8 @@ paths:
summary: 按 PocketBase 记录 id 更新公司 summary: 按 PocketBase 记录 id 更新公司
description: >- description: >-
这是 PocketBase 原生更新接口,路径参数统一使用记录主键 `id`。 这是 PocketBase 原生更新接口,路径参数统一使用记录主键 `id`。
如果业务侧只有 `company_id`,标准流程是先调用 list 接口
`filter=company_id="..."&perPage=1&page=1` 查出对应记录,再用返回的 `id` 调用本接口。
parameters: parameters:
- $ref: '#/components/parameters/RecordId' - $ref: '#/components/parameters/RecordId'
- $ref: '#/components/parameters/Fields' - $ref: '#/components/parameters/Fields'
@@ -213,6 +236,7 @@ paths:
company_name: 宝镜科技(更新) company_name: 宝镜科技(更新)
company_status: 有效 company_status: 有效
company_level: S company_level: S
company_owner_openid: wx-openid-owner-002
company_remark: 已更新基础资料 company_remark: 已更新基础资料
responses: responses:
'200': '200':
@@ -349,12 +373,27 @@ components:
company_nationality: company_nationality:
type: string type: string
description: 国家 description: 国家
company_nationality_code:
type: string
description: 国家编码
company_province: company_province:
type: string type: string
description: 省份 description: 省份
company_province_code:
type: string
description: 省份编码
company_city: company_city:
type: string type: string
description: 城市 description: 城市
company_city_code:
type: string
description: 城市编码
company_district:
type: string
description: 区/县
company_district_code:
type: string
description: 区/县编码
company_postalcode: company_postalcode:
type: string type: string
description: 邮编 description: 邮编
@@ -367,44 +406,132 @@ components:
company_level: company_level:
type: string type: string
description: 公司等级 description: 公司等级
company_owner_openid:
type: string
description: 公司所有者 openid
company_remark: company_remark:
type: string type: string
description: 备注 description: 备注
CompanyCreateRequest: CompanyCreateRequest:
allOf: type: object
- $ref: '#/components/schemas/CompanyBase' description: 创建时不需要传 `company_id`,由数据库自动生成。
- type: object properties:
required: [company_id] company_name:
description: "公司名称"
type: string
company_type:
description: "公司类型"
type: string
company_entity:
description: "公司法人"
type: string
company_usci:
description: "统一社会信用代码"
type: string
company_nationality:
description: "国家名称"
type: string
company_nationality_code:
description: "国家编码"
type: string
company_province:
description: "省份名称"
type: string
company_province_code:
description: "省份编码"
type: string
company_city:
description: "城市名称"
type: string
company_city_code:
description: "城市编码"
type: string
company_district:
description: "区 / 县名称"
type: string
company_district_code:
description: "区 / 县编码"
type: string
company_postalcode:
description: "邮编"
type: string
company_add:
description: "地址"
type: string
company_status:
description: "公司状态"
type: string
company_level:
description: "公司等级"
type: string
company_owner_openid:
description: "公司所有者 openid"
type: string
company_remark:
description: "备注"
type: string
additionalProperties: false
CompanyUpdateRequest: CompanyUpdateRequest:
type: object type: object
description: >- description: >-
更新时可只传需要修改的字段;记录定位统一依赖路径参数 `id`。 更新时可只传需要修改的字段;记录定位统一依赖路径参数 `id`。
properties: properties:
company_id: company_id:
description: "所属公司业务 ID"
type: string type: string
company_name: company_name:
description: "公司名称"
type: string type: string
company_type: company_type:
description: "公司类型"
type: string type: string
company_entity: company_entity:
description: "公司法人"
type: string type: string
company_usci: company_usci:
description: "统一社会信用代码"
type: string type: string
company_nationality: company_nationality:
description: "国家名称"
type: string
company_nationality_code:
description: "国家编码"
type: string type: string
company_province: company_province:
description: "省份名称"
type: string
company_province_code:
description: "省份编码"
type: string type: string
company_city: company_city:
description: "城市名称"
type: string
company_city_code:
description: "城市编码"
type: string
company_district:
description: "区 / 县名称"
type: string
company_district_code:
description: "区 / 县编码"
type: string type: string
company_postalcode: company_postalcode:
description: "邮编"
type: string type: string
company_add: company_add:
description: "地址"
type: string type: string
company_status: company_status:
description: "公司状态"
type: string type: string
company_level: company_level:
description: "公司等级"
type: string
company_owner_openid:
description: "公司所有者 openid"
type: string type: string
company_remark: company_remark:
description: "备注"
type: string type: string
CompanyRecord: CompanyRecord:
allOf: allOf:
@@ -418,8 +545,10 @@ components:
collectionName: collectionName:
type: string type: string
created: created:
description: "记录创建时间"
type: string type: string
updated: updated:
description: "记录更新时间"
type: string type: string
- $ref: '#/components/schemas/CompanyBase' - $ref: '#/components/schemas/CompanyBase'
CompanyListResponse: CompanyListResponse:
@@ -445,5 +574,6 @@ components:
message: message:
type: string type: string
data: data:
description: "业务响应数据"
type: object type: object
additionalProperties: true additionalProperties: true

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
### 1. tbl_system_dict (系统词典) ### 1. tbl_system_dict (系统词典)
**类型:** Base Collection **类型:** Base Collection
**读写权限:** 所有人可读;仅 `ManagePlatform`/管理员可写
| 字段名 | 类型 | 备注 | | 字段名 | 类型 | 备注 |
| :--- | :--- | :--- | | :--- | :--- | :--- |
@@ -30,23 +31,30 @@
| 字段名 | 类型 | 备注 | | 字段名 | 类型 | 备注 |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| company_id | text | 自定义公司id | | company_id | text | 公司业务id由数据库自动生成默认形如 `WX-COMPANY-时间串` |
| company_name | text | 公司名称 | | company_name | text | 公司名称 |
| company_type | text | 公司类型 | | company_type | text | 公司类型 |
| company_entity | text | 公司法人 | | company_entity | text | 公司法人 |
| company_usci | text | 统一社会信用代码 | | company_usci | text | 统一社会信用代码 |
| company_nationality | text | 国家 | | company_nationality | text | 国家 |
| company_nationality_code | text | 国家编码 |
| company_province | text | 省份 | | company_province | text | 省份 |
| company_province_code | text | 省份编码 |
| company_city | text | 城市 | | company_city | text | 城市 |
| company_city_code | text | 城市编码 |
| company_district | text | 区/县 |
| company_district_code | text | 区/县编码 |
| company_postalcode | text | 邮编 | | company_postalcode | text | 邮编 |
| company_add | text | 地址 | | company_add | text | 地址 |
| company_status | text | 公司状态 | | company_status | text | 公司状态 |
| company_level | text | 公司等级 | | company_level | text | 公司等级 |
| company_owner_openid | text | 公司所有者 openid |
| company_remark | text | 备注 | | company_remark | text | 备注 |
**索引规划 (Indexes):** **索引规划 (Indexes):**
* `CREATE UNIQUE INDEX` 针对 `company_id` (确保业务主键唯一) * `CREATE UNIQUE INDEX` 针对 `company_id` (确保业务主键唯一)
* `CREATE INDEX` 针对 `company_usci` (加速信用代码检索) * `CREATE INDEX` 针对 `company_usci` (加速信用代码检索)
* `CREATE INDEX` 针对 `company_owner_openid` (加速按公司所有者 openid 查询)
--- ---
@@ -78,6 +86,7 @@
| users_wx_openid | text | 微信号 | | users_wx_openid | text | 微信号 |
| users_level | text | 用户等级 | | users_level | text | 用户等级 |
| users_type | text | 用户类型 | | users_type | text | 用户类型 |
| users_tag | text | 用户标签 |
| users_status | text | 用户状态 | | users_status | text | 用户状态 |
| company_id | text | 公司id (存储 tbl_company.company_id) | | company_id | text | 公司id (存储 tbl_company.company_id) |
| users_parent_id | text | 用户父级id (存储 tbl_users.users_id) | | users_parent_id | text | 用户父级id (存储 tbl_users.users_id) |

View File

@@ -8,7 +8,8 @@
"init:newpb": "node pocketbase.newpb.js", "init:newpb": "node pocketbase.newpb.js",
"init:documents": "node pocketbase.documents.js", "init:documents": "node pocketbase.documents.js",
"init:dictionary": "node pocketbase.dictionary.js", "init:dictionary": "node pocketbase.dictionary.js",
"migrate:file-fields": "node pocketbase.file-fields-to-attachments.js" "migrate:file-fields": "node pocketbase.file-fields-to-attachments.js",
"test:company-native-api": "node test-tbl-company-native-api.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@@ -23,6 +23,11 @@ const pb = new PocketBase(PB_URL);
const collectionData = { const collectionData = {
name: 'tbl_system_dict', name: 'tbl_system_dict',
type: 'base', type: 'base',
listRule: '',
viewRule: '',
createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
fields: [ fields: [
{ name: 'system_dict_id', type: 'text', required: true }, { name: 'system_dict_id', type: 'text', required: true },
{ name: 'dict_name', type: 'text', required: true }, { name: 'dict_name', type: 'text', required: true },
@@ -68,6 +73,11 @@ function buildCollectionPayload(target, existingCollection) {
return { return {
name: target.name, name: target.name,
type: target.type, type: target.type,
listRule: target.listRule,
viewRule: target.viewRule,
createRule: target.createRule,
updateRule: target.updateRule,
deleteRule: target.deleteRule,
fields: target.fields.map((field) => normalizeFieldPayload(field, null)), fields: target.fields.map((field) => normalizeFieldPayload(field, null)),
indexes: target.indexes, indexes: target.indexes,
}; };
@@ -91,6 +101,11 @@ function buildCollectionPayload(target, existingCollection) {
return { return {
name: target.name, name: target.name,
type: target.type, type: target.type,
listRule: target.listRule,
viewRule: target.viewRule,
createRule: target.createRule,
updateRule: target.updateRule,
deleteRule: target.deleteRule,
fields: fields, fields: fields,
indexes: target.indexes, indexes: target.indexes,
}; };

View File

@@ -47,8 +47,11 @@ const collections = [
{ {
name: 'tbl_document', name: 'tbl_document',
type: 'base', type: 'base',
listRule: '',
viewRule: '',
fields: [ fields: [
{ name: 'document_id', type: 'text', required: true }, { name: 'document_id', type: 'text', required: true },
{ name: 'document_create', type: 'autodate', onCreate: true, onUpdate: false },
{ name: 'document_effect_date', type: 'date' }, { name: 'document_effect_date', type: 'date' },
{ name: 'document_expiry_date', type: 'date' }, { name: 'document_expiry_date', type: 'date' },
{ name: 'document_type', type: 'text', required: true }, { name: 'document_type', type: 'text', required: true },
@@ -77,6 +80,7 @@ const collections = [
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)', 'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)',
'CREATE INDEX idx_tbl_document_document_create ON tbl_document (document_create)',
'CREATE INDEX idx_tbl_document_document_owner ON tbl_document (document_owner)', 'CREATE INDEX idx_tbl_document_document_owner ON tbl_document (document_owner)',
'CREATE INDEX idx_tbl_document_document_type ON tbl_document (document_type)', 'CREATE INDEX idx_tbl_document_document_type ON tbl_document (document_type)',
'CREATE INDEX idx_tbl_document_document_status ON tbl_document (document_status)', 'CREATE INDEX idx_tbl_document_document_status ON tbl_document (document_status)',
@@ -130,6 +134,11 @@ function normalizeFieldPayload(field, existingField) {
payload.mimeTypes = Array.isArray(field.mimeTypes) && field.mimeTypes.length ? field.mimeTypes : null; payload.mimeTypes = Array.isArray(field.mimeTypes) && field.mimeTypes.length ? field.mimeTypes : null;
} }
if (field.type === 'autodate') {
payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true;
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
}
return payload; return payload;
} }

View File

@@ -13,6 +13,11 @@ const collections = [
{ {
name: 'tbl_system_dict', name: 'tbl_system_dict',
type: 'base', type: 'base',
listRule: '',
viewRule: '',
createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
fields: [ fields: [
{ name: 'system_dict_id', type: 'text', required: true }, { name: 'system_dict_id', type: 'text', required: true },
{ name: 'dict_name', type: 'text', required: true }, { name: 'dict_name', type: 'text', required: true },
@@ -33,23 +38,30 @@ const collections = [
name: 'tbl_company', name: 'tbl_company',
type: 'base', type: 'base',
fields: [ fields: [
{ name: 'company_id', type: 'text', required: true }, { name: 'company_id', type: 'text', required: true, autogeneratePattern: 'WX-COMPANY-[0-9]{13}' },
{ name: 'company_name', type: 'text' }, { name: 'company_name', type: 'text' },
{ name: 'company_type', type: 'text' }, { name: 'company_type', type: 'text' },
{ name: 'company_entity', type: 'text' }, { name: 'company_entity', type: 'text' },
{ name: 'company_usci', type: 'text' }, { name: 'company_usci', type: 'text' },
{ name: 'company_nationality', type: 'text' }, { name: 'company_nationality', type: 'text' },
{ name: 'company_nationality_code', type: 'text' },
{ name: 'company_province', type: 'text' }, { name: 'company_province', type: 'text' },
{ name: 'company_province_code', type: 'text' },
{ name: 'company_city', type: 'text' }, { name: 'company_city', type: 'text' },
{ name: 'company_city_code', type: 'text' },
{ name: 'company_district', type: 'text' },
{ name: 'company_district_code', type: 'text' },
{ name: 'company_postalcode', type: 'text' }, { name: 'company_postalcode', type: 'text' },
{ name: 'company_add', type: 'text' }, { name: 'company_add', type: 'text' },
{ name: 'company_status', type: 'text' }, { name: 'company_status', type: 'text' },
{ name: 'company_level', type: 'text' }, { name: 'company_level', type: 'text' },
{ name: 'company_owner_openid', type: 'text' },
{ name: 'company_remark', type: 'text' } { name: 'company_remark', type: 'text' }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_company_id ON tbl_company (company_id)', 'CREATE UNIQUE INDEX idx_company_id ON tbl_company (company_id)',
'CREATE INDEX idx_company_usci ON tbl_company (company_usci)' 'CREATE INDEX idx_company_usci ON tbl_company (company_usci)',
'CREATE INDEX idx_company_owner_openid ON tbl_company (company_owner_openid)'
] ]
}, },
{ {
@@ -122,6 +134,11 @@ async function createOrUpdateCollection(collectionData) {
const payload = { const payload = {
name: collectionData.name, name: collectionData.name,
type: collectionData.type, type: collectionData.type,
listRule: collectionData.listRule,
viewRule: collectionData.viewRule,
createRule: collectionData.createRule,
updateRule: collectionData.updateRule,
deleteRule: collectionData.deleteRule,
fields: collectionData.fields, fields: collectionData.fields,
indexes: collectionData.indexes indexes: collectionData.indexes
}; };

View File

@@ -52,6 +52,7 @@ const collections = [
{ name: 'users_phone', type: 'text' }, { name: 'users_phone', type: 'text' },
{ name: 'users_level', type: 'text' }, { name: 'users_level', type: 'text' },
{ name: 'users_type', type: 'text' }, { name: 'users_type', type: 'text' },
{ name: 'users_tag', type: 'text' },
{ name: 'users_status', type: 'text' }, { name: 'users_status', type: 'text' },
{ name: 'company_id', type: 'text' }, { name: 'company_id', type: 'text' },
{ name: 'users_parent_id', type: 'text' }, { name: 'users_parent_id', type: 'text' },

View File

@@ -0,0 +1,125 @@
import fs from 'node:fs/promises'
const runtimePath = new URL('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js', import.meta.url)
const runtimeText = await fs.readFile(runtimePath, 'utf8')
function readRuntimeValue(name) {
const match = runtimeText.match(new RegExp(`${name}:\\s*'([^']*)'`))
if (!match) {
throw new Error(`未在 runtime.js 中找到 ${name}`)
}
return match[1]
}
const baseUrl = readRuntimeValue('APP_BASE_URL')
const adminToken = readRuntimeValue('POCKETBASE_AUTH_TOKEN')
function assert(condition, message) {
if (!condition) {
throw new Error(message)
}
}
async function requestJson(path, options = {}) {
const res = await fetch(`${baseUrl}${path}`, options)
const text = await res.text()
let json = null
try {
json = text ? JSON.parse(text) : null
} catch {
throw new Error(`接口 ${path} 返回了非 JSON 响应:${text}`)
}
return { res, json }
}
async function main() {
const knownCompanyId = 'WX-COMPANY-10001'
const initialOwnerOpenid = 'wx-owner-create'
const updatedOwnerOpenid = 'wx-owner-updated'
const createResult = await requestJson('/pb/api/collections/tbl_company/records', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
company_name: '原生接口测试公司',
company_nationality: '中国',
company_nationality_code: 'CN',
company_province: '上海',
company_province_code: '310000',
company_city: '上海',
company_city_code: '310100',
company_district: '浦东新区',
company_district_code: '310115',
company_status: '有效',
company_owner_openid: initialOwnerOpenid,
}),
})
assert(createResult.res.ok, `公开创建失败:${JSON.stringify(createResult.json)}`)
assert(typeof createResult.json?.company_id === 'string' && createResult.json.company_id.startsWith('WX-COMPANY-'), '公开创建未自动生成 company_id')
assert(createResult.json?.company_owner_openid === initialOwnerOpenid, '公开创建返回的 company_owner_openid 不匹配')
assert(createResult.json?.company_city_code === '310100', '公开创建返回的 company_city_code 不匹配')
const byCompanyIdResult = await requestJson(
`/pb/api/collections/tbl_company/records?filter=${encodeURIComponent(`company_id="${knownCompanyId}"`)}&perPage=1&page=1`
)
assert(byCompanyIdResult.res.ok, `按 company_id 查询失败:${JSON.stringify(byCompanyIdResult.json)}`)
assert(byCompanyIdResult.json?.totalItems === 1, `按 company_id 查询结果数量不为 1${JSON.stringify(byCompanyIdResult.json)}`)
assert(byCompanyIdResult.json?.items?.[0]?.company_id === knownCompanyId, '按 company_id 查询返回了错误的记录')
const listResult = await requestJson('/pb/api/collections/tbl_company/records?perPage=5&page=1')
assert(listResult.res.ok, `公开列表查询失败:${JSON.stringify(listResult.json)}`)
assert((listResult.json?.totalItems || 0) >= 1, '公开列表查询未返回任何数据')
const createdCompanyId = createResult.json.company_id
const createdLookupResult = await requestJson(
`/pb/api/collections/tbl_company/records?filter=${encodeURIComponent(`company_id="${createdCompanyId}"`)}&perPage=1&page=1`
)
assert(createdLookupResult.res.ok, `按 company_id 查询测试记录失败:${JSON.stringify(createdLookupResult.json)}`)
assert(createdLookupResult.json?.totalItems === 1, `按 company_id 查询测试记录数量不为 1${JSON.stringify(createdLookupResult.json)}`)
const createdRecordId = createdLookupResult.json.items[0].id
const updateResult = await requestJson(`/pb/api/collections/tbl_company/records/${createdRecordId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${adminToken}`,
},
body: JSON.stringify({
company_name: '原生接口测试公司-已更新',
company_owner_openid: updatedOwnerOpenid,
company_district: '徐汇区',
company_district_code: '310104',
}),
})
assert(updateResult.res.ok, `按 company_id 定位后更新失败:${JSON.stringify(updateResult.json)}`)
assert(updateResult.json?.company_name === '原生接口测试公司-已更新', '更新后的 company_name 不正确')
assert(updateResult.json?.company_owner_openid === updatedOwnerOpenid, '更新后的 company_owner_openid 不正确')
assert(updateResult.json?.company_district_code === '310104', '更新后的 company_district_code 不正确')
const cleanupResult = await requestJson(`/pb/api/collections/tbl_company/records/${createResult.json.id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${adminToken}`,
},
})
assert(cleanupResult.res.ok, `测试清理失败:${JSON.stringify(cleanupResult.json)}`)
console.log(JSON.stringify({
createdCompanyId: createdCompanyId,
queriedCompanyId: byCompanyIdResult.json.items[0].company_id,
publicListTotalItems: listResult.json.totalItems,
updatedRecordId: createdRecordId,
updatedOwnerOpenid: updateResult.json.company_owner_openid,
updatedDistrictCode: updateResult.json.company_district_code,
cleanupDeletedId: createResult.json.id,
}, null, 2))
}
await main()