feat: 规范化PocketBase数据库文档与原生API访问
- 将数据库文档拆分为按collection命名的标准文件,统一格式 - 补充tbl_company、tbl_system_dict等表的原生访问规则 - 新增users_tag、document_create等字段 - 优化用户资料更新接口,支持非必填字段 - 添加公司原生API测试脚本 - 归档本次变更至OpenSpec
This commit is contained in:
18
docs/api.md
18
docs/api.md
@@ -133,6 +133,7 @@
|
||||
"users_type": "注册用户",
|
||||
"users_name": "张三",
|
||||
"users_phone": "13800138000",
|
||||
"users_tag": "核心客户",
|
||||
"users_phone_masked": "138****8000",
|
||||
"users_picture": "ATT-1743123456789-abc123",
|
||||
"users_picture_url": "https://bai-api.blv-oa.com/pb/api/files/pbc_xxx/recordId/avatar.png",
|
||||
@@ -163,6 +164,8 @@
|
||||
{
|
||||
"users_name": "张三",
|
||||
"users_phone_code": "2b7d9f2e3c4a5b6d7e8f",
|
||||
"users_phone": "13800138000",
|
||||
"users_tag": "核心客户",
|
||||
"users_picture": "ATT-1743123456789-abc123",
|
||||
"users_id_pic_a": "ATT-1743123456789-id-a",
|
||||
"users_id_pic_b": "ATT-1743123456789-id-b",
|
||||
@@ -174,9 +177,11 @@
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `users_name` | string | 是 | 用户姓名 |
|
||||
| `users_phone_code` | string | 是 | 微信手机号获取凭证 code,后端将据此换取真实手机号 |
|
||||
| `users_picture` | string | 是 | 用户头像附件的 `attachments_id` |
|
||||
| `users_name` | string | 否 | 用户姓名;非空时才更新 |
|
||||
| `users_phone_code` | string | 否 | 微信手机号获取凭证 code;若提供,服务端优先据此换取真实手机号 |
|
||||
| `users_phone` | string | 否 | 直接写入的手机号;仅在未提供 `users_phone_code` 时生效 |
|
||||
| `users_tag` | string | 否 | 用户标签;非空时才更新 |
|
||||
| `users_picture` | string | 否 | 用户头像附件的 `attachments_id`;非空时才更新 |
|
||||
| `users_id_pic_a` | string | 否 | 证件正面附件的 `attachments_id` |
|
||||
| `users_id_pic_b` | string | 否 | 证件反面附件的 `attachments_id` |
|
||||
| `users_title_picture` | string | 否 | 资质附件的 `attachments_id` |
|
||||
@@ -186,8 +191,10 @@
|
||||
- 从 `Authorization` 对应的 PocketBase auth record 读取当前用户 `openid`
|
||||
- 校验 `Authorization`
|
||||
- 不再从 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_type` 从 `游客` 升级为 `注册用户`
|
||||
- 返回更新后的完整用户信息
|
||||
@@ -205,6 +212,7 @@
|
||||
"users_type": "注册用户",
|
||||
"users_name": "张三",
|
||||
"users_phone": "13800138000",
|
||||
"users_tag": "核心客户",
|
||||
"users_phone_masked": "138****8000",
|
||||
"users_picture": "ATT-1743123456789-abc123",
|
||||
"users_picture_url": "https://bai-api.blv-oa.com/pb/api/files/pbc_xxx/recordId/avatar.png",
|
||||
|
||||
168
docs/api_response_field_notes.md
Normal file
168
docs/api_response_field_notes.md
Normal 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` |
|
||||
@@ -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
37
docs/pb_auth_model.md
Normal 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 使用。
|
||||
@@ -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` 普通索引
|
||||
39
docs/pb_tbl_attachments.md
Normal file
39
docs/pb_tbl_attachments.md
Normal 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 字段清单里单独声明。
|
||||
28
docs/pb_tbl_auth_resources.md
Normal file
28
docs/pb_tbl_auth_resources.md
Normal 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` 唯一 |
|
||||
29
docs/pb_tbl_auth_role_perms.md
Normal file
29
docs/pb_tbl_auth_role_perms.md
Normal 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
28
docs/pb_tbl_auth_roles.md
Normal 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` 唯一 |
|
||||
29
docs/pb_tbl_auth_row_scopes.md
Normal file
29
docs/pb_tbl_auth_row_scopes.md
Normal 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` | 加速按作用表查询 |
|
||||
29
docs/pb_tbl_auth_user_overrides.md
Normal file
29
docs/pb_tbl_auth_user_overrides.md
Normal 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
66
docs/pb_tbl_auth_users.md
Normal 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
48
docs/pb_tbl_company.md
Normal 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
62
docs/pb_tbl_document.md
Normal 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 字段清单里单独声明。
|
||||
35
docs/pb_tbl_document_operation_history.md
Normal file
35
docs/pb_tbl_document_operation_history.md
Normal 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 字段清单里单独声明。
|
||||
38
docs/pb_tbl_system_dict.md
Normal file
38
docs/pb_tbl_system_dict.md
Normal 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 字段清单里单独声明。
|
||||
@@ -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. 若你需要,我可以继续帮你再生成一份“更像数据库设计说明书”的版本,增加字段含义、业务用途、关联关系三列。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-29
|
||||
@@ -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`
|
||||
@@ -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 变更和接口文档维护方式
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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。
|
||||
38
openspec/specs/pocketbase-native-data-access/spec.md
Normal file
38
openspec/specs/pocketbase-native-data-access/spec.md
Normal 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
|
||||
|
||||
33
openspec/specs/pocketbase-schema-docs/spec.md
Normal file
33
openspec/specs/pocketbase-schema-docs/spec.md
Normal 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
|
||||
|
||||
@@ -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/platform/login.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/detail.js`)
|
||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/create.js`)
|
||||
|
||||
@@ -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')
|
||||
@@ -6,7 +6,6 @@ routerAdd('POST', '/api/dictionary/detail', function (e) {
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
guards.requireManagePlatformUser(e)
|
||||
|
||||
const payload = guards.validateDictionaryDetailBody(e)
|
||||
const data = dictionaryService.getDictionaryByName(payload.dict_name)
|
||||
|
||||
@@ -6,7 +6,6 @@ routerAdd('POST', '/api/dictionary/list', function (e) {
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
guards.requireManagePlatformUser(e)
|
||||
|
||||
const payload = guards.validateDictionaryListBody(e)
|
||||
const data = dictionaryService.listDictionaries(payload.keyword)
|
||||
|
||||
@@ -6,7 +6,5 @@ module.exports = {
|
||||
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo',
|
||||
WECHAT_APPID: 'wx3bd7a7b19679da7a',
|
||||
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
|
||||
/* WECHAT_APPID: 'wx42e9add0f91af98b',
|
||||
WECHAT_SECRET: '5620f00b40297efaf3d197d61ae184d6', */
|
||||
BUILD_TIME: '',
|
||||
}
|
||||
@@ -25,13 +25,12 @@ function validateLoginBody(e) {
|
||||
|
||||
function validateProfileBody(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 {
|
||||
users_name: payload.users_name,
|
||||
users_phone_code: payload.users_phone_code,
|
||||
users_picture: payload.users_picture,
|
||||
users_name: Object.prototype.hasOwnProperty.call(payload, 'users_name') ? String(payload.users_name || '').trim() : undefined,
|
||||
users_phone_code: Object.prototype.hasOwnProperty.call(payload, 'users_phone_code') ? String(payload.users_phone_code || '').trim() : undefined,
|
||||
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_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,
|
||||
@@ -60,6 +59,7 @@ function validatePlatformRegisterBody(e) {
|
||||
users_id_number: payload.users_id_number || '',
|
||||
users_level: payload.users_level || '',
|
||||
users_type: payload.users_type || '',
|
||||
users_tag: payload.users_tag || '',
|
||||
company_id: payload.company_id || '',
|
||||
users_parent_id: payload.users_parent_id || '',
|
||||
users_promo_code: payload.users_promo_code || '',
|
||||
|
||||
@@ -206,6 +206,7 @@ function exportDocumentRecord(record) {
|
||||
return {
|
||||
pb_id: record.id,
|
||||
document_id: record.getString('document_id'),
|
||||
document_create: String(record.get('document_create') || ''),
|
||||
document_effect_date: String(record.get('document_effect_date') || ''),
|
||||
document_expiry_date: String(record.get('document_expiry_date') || ''),
|
||||
document_type: record.getString('document_type'),
|
||||
|
||||
@@ -122,7 +122,30 @@ function getCompanyByCompanyId(companyId) {
|
||||
|
||||
function exportCompany(companyRecord) {
|
||||
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) {
|
||||
@@ -167,15 +190,23 @@ function ensureAttachmentIdExists(attachmentId, fieldName) {
|
||||
function applyUserAttachmentFields(record, payload) {
|
||||
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') {
|
||||
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') {
|
||||
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') {
|
||||
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_masked: maskPhone(userRecord.getString('users_phone')),
|
||||
users_level: userRecord.getString('users_level'),
|
||||
users_tag: userRecord.getString('users_tag'),
|
||||
users_picture: userPicture.id,
|
||||
users_picture_url: userPicture.url,
|
||||
users_picture_attachment: userPicture.attachment,
|
||||
@@ -366,6 +398,7 @@ function registerPlatformUser(payload) {
|
||||
record.set('users_id_number', payload.users_id_number || '')
|
||||
record.set('users_level', payload.users_level || '')
|
||||
record.set('users_type', payload.users_type || REGISTERED_USER_TYPE)
|
||||
record.set('users_tag', payload.users_tag || '')
|
||||
record.set('company_id', payload.company_id || '')
|
||||
record.set('users_parent_id', payload.users_parent_id || '')
|
||||
record.set('users_promo_code', payload.users_promo_code || '')
|
||||
@@ -481,7 +514,12 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
|
||||
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')) {
|
||||
const samePhoneUsers = $app.findRecordsByFilter('tbl_auth_users', 'users_phone = {:phone}', '', 10, 0, {
|
||||
@@ -494,15 +532,19 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
const shouldPromote = isAllProfileFieldsEmpty(currentUser)
|
||||
&& !!payload.users_name
|
||||
&& !!usersPhone
|
||||
&& !!payload.users_picture
|
||||
&& ((currentUser.getString('users_type') || GUEST_USER_TYPE) === GUEST_USER_TYPE)
|
||||
|
||||
if (payload.users_name) {
|
||||
currentUser.set('users_name', payload.users_name)
|
||||
}
|
||||
if (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)
|
||||
|
||||
const shouldPromote = ((currentUser.getString('users_type') || GUEST_USER_TYPE) === GUEST_USER_TYPE)
|
||||
&& isInfoComplete(currentUser)
|
||||
if (shouldPromote) {
|
||||
currentUser.set('users_type', REGISTERED_USER_TYPE)
|
||||
}
|
||||
|
||||
@@ -65,12 +65,15 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
.dropzone-title { font-weight: 700; color: #1e3a8a; }
|
||||
.dropzone-text { color: #475569; font-size: 13px; margin-top: 6px; }
|
||||
.dropzone input[type="file"] { margin-top: 12px; }
|
||||
.file-list { display: grid; gap: 10px; margin-top: 12px; }
|
||||
.file-card { border: 1px solid #dbe3f0; border-radius: 14px; padding: 12px; background: #fff; }
|
||||
.file-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.file-card-title { font-weight: 700; word-break: break-all; }
|
||||
.file-meta { color: #64748b; font-size: 12px; margin-top: 6px; word-break: break-all; }
|
||||
.thumb { width: 72px; height: 72px; border-radius: 12px; object-fit: cover; border: 1px solid #dbe3f0; background: #fff; cursor: zoom-in; }
|
||||
.file-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
|
||||
.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: flex-start; justify-content: space-between; gap: 8px; }
|
||||
.file-card-title { flex: 1; font-weight: 700; font-size: 12px; line-height: 1.3; 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; }
|
||||
.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; }
|
||||
.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; }
|
||||
@@ -85,6 +88,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
@media (max-width: 960px) {
|
||||
.grid, .file-group { grid-template-columns: 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; }
|
||||
thead { display: none; }
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
const sourceDict = getDocumentTypeSourceDictionary()
|
||||
if (!sourceDict) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return joinPipeValue(state.selections.documentTypeValues.map(function (enumValue) {
|
||||
return sourceDict.system_dict_id + '@' + enumValue
|
||||
}))
|
||||
const sourceId = String(sourceDict.system_dict_id || '').trim()
|
||||
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) {
|
||||
@@ -660,7 +695,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
|
||||
if (state.mode === 'edit') {
|
||||
formTitleEl.textContent = '编辑文档'
|
||||
editorModeEl.textContent = '当前模式:编辑 ' + state.editingId
|
||||
editorModeEl.textContent = '当前模式:编辑 ' + (state.editingSource && state.editingSource.document_title ? state.editingSource.document_title : state.editingId)
|
||||
document.getElementById('submitBtn').textContent = '保存文档修改'
|
||||
} else if (state.mode === 'create') {
|
||||
formTitleEl.textContent = '新增文档'
|
||||
@@ -826,26 +861,17 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
const title = pending
|
||||
? (item.file && item.file.name ? item.file.name : ('待上传文件' + (index + 1)))
|
||||
: (item.attachments_filename || item.attachments_id || ('附件' + (index + 1)))
|
||||
const meta = 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 previewUrl = getAttachmentPreviewUrl(item, pending)
|
||||
const previewHtml = category === 'image'
|
||||
? renderImageThumb(getAttachmentPreviewUrl(item, pending), title)
|
||||
: ''
|
||||
const actionLabel = pending ? '移除待上传' : '从文档移除'
|
||||
? renderImageThumb(previewUrl, title)
|
||||
: (previewUrl
|
||||
? ('<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'
|
||||
|
||||
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
|
||||
+ '<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>'
|
||||
}).join('')
|
||||
}
|
||||
@@ -860,27 +886,13 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
}
|
||||
|
||||
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 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) {
|
||||
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) {
|
||||
if (!imageUrls.length) {
|
||||
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() {
|
||||
@@ -1132,7 +1144,7 @@ routerAdd('GET', '/manage/document-manage', function (e) {
|
||||
fillFormFromItem(target)
|
||||
updateEditorMode()
|
||||
renderAttachmentEditors()
|
||||
setStatus('已进入编辑模式:' + target.document_id, 'success')
|
||||
setStatus('已进入编辑模式:' + (target.document_title || target.document_id), 'success')
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -7,6 +7,8 @@ info:
|
||||
本文档面向小程序端直接使用 PocketBase JS SDK / REST API 访问 `tbl_company`。
|
||||
本文档统一以 PocketBase 原生记录主键 `id` 作为唯一识别键。
|
||||
`company_id` 保留为普通业务字段,可用于展示、筛选和业务关联,但不再作为 CRUD 的唯一键。
|
||||
当前线上 `tbl_company` 还包含 `company_owner_openid` 字段,用于保存公司所有者 openid,并带普通索引。
|
||||
同时新增了国家、省、市、区的名称与编码字段,便于前端直接按行政区划存取。
|
||||
license:
|
||||
name: Proprietary
|
||||
identifier: LicenseRef-Proprietary
|
||||
@@ -29,7 +31,8 @@ paths:
|
||||
支持三种常见模式:
|
||||
1. 全表查询:不传 `filter`;
|
||||
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:
|
||||
- $ref: '#/components/parameters/Page'
|
||||
- $ref: '#/components/parameters/PerPage'
|
||||
@@ -64,12 +67,18 @@ paths:
|
||||
company_entity: 张三
|
||||
company_usci: '91310000123456789A'
|
||||
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_postalcode: '200000'
|
||||
company_add: 上海市浦东新区XX路1号
|
||||
company_status: 有效
|
||||
company_level: A
|
||||
company_owner_openid: wx-openid-owner-001
|
||||
company_remark: ''
|
||||
exact:
|
||||
summary: 按 id 精确查询
|
||||
@@ -90,12 +99,18 @@ paths:
|
||||
company_entity: 张三
|
||||
company_usci: '91310000123456789A'
|
||||
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_postalcode: '200000'
|
||||
company_add: 上海市浦东新区XX路1号
|
||||
company_status: 有效
|
||||
company_level: A
|
||||
company_owner_openid: wx-openid-owner-001
|
||||
company_remark: ''
|
||||
'400':
|
||||
description: 过滤表达式或查询参数不合法
|
||||
@@ -115,7 +130,8 @@ paths:
|
||||
summary: 新增公司
|
||||
description: >-
|
||||
创建一条 `tbl_company` 记录。当前文档以 `id` 作为记录唯一识别键,
|
||||
新建成功后由 PocketBase 自动生成 `id`;根据当前项目建表脚本,`company_id` 仍是必填业务字段,但不再作为 CRUD 唯一键。
|
||||
新建成功后由 PocketBase 自动生成 `id`;`company_id` 也由数据库自动生成,
|
||||
客户端创建时不需要传入,但仍可作为后续业务查询字段。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -125,18 +141,23 @@ paths:
|
||||
examples:
|
||||
default:
|
||||
value:
|
||||
company_id: C10001
|
||||
company_name: 宝镜科技
|
||||
company_type: 渠道商
|
||||
company_entity: 张三
|
||||
company_usci: '91310000123456789A'
|
||||
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_postalcode: '200000'
|
||||
company_add: 上海市浦东新区XX路1号
|
||||
company_status: 有效
|
||||
company_level: A
|
||||
company_owner_openid: wx-openid-owner-001
|
||||
company_remark: 首次创建
|
||||
responses:
|
||||
'200':
|
||||
@@ -198,6 +219,8 @@ paths:
|
||||
summary: 按 PocketBase 记录 id 更新公司
|
||||
description: >-
|
||||
这是 PocketBase 原生更新接口,路径参数统一使用记录主键 `id`。
|
||||
如果业务侧只有 `company_id`,标准流程是先调用 list 接口
|
||||
`filter=company_id="..."&perPage=1&page=1` 查出对应记录,再用返回的 `id` 调用本接口。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/RecordId'
|
||||
- $ref: '#/components/parameters/Fields'
|
||||
@@ -213,6 +236,7 @@ paths:
|
||||
company_name: 宝镜科技(更新)
|
||||
company_status: 有效
|
||||
company_level: S
|
||||
company_owner_openid: wx-openid-owner-002
|
||||
company_remark: 已更新基础资料
|
||||
responses:
|
||||
'200':
|
||||
@@ -349,12 +373,27 @@ components:
|
||||
company_nationality:
|
||||
type: string
|
||||
description: 国家
|
||||
company_nationality_code:
|
||||
type: string
|
||||
description: 国家编码
|
||||
company_province:
|
||||
type: string
|
||||
description: 省份
|
||||
company_province_code:
|
||||
type: string
|
||||
description: 省份编码
|
||||
company_city:
|
||||
type: string
|
||||
description: 城市
|
||||
company_city_code:
|
||||
type: string
|
||||
description: 城市编码
|
||||
company_district:
|
||||
type: string
|
||||
description: 区/县
|
||||
company_district_code:
|
||||
type: string
|
||||
description: 区/县编码
|
||||
company_postalcode:
|
||||
type: string
|
||||
description: 邮编
|
||||
@@ -367,44 +406,132 @@ components:
|
||||
company_level:
|
||||
type: string
|
||||
description: 公司等级
|
||||
company_owner_openid:
|
||||
type: string
|
||||
description: 公司所有者 openid
|
||||
company_remark:
|
||||
type: string
|
||||
description: 备注
|
||||
CompanyCreateRequest:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompanyBase'
|
||||
- type: object
|
||||
required: [company_id]
|
||||
type: object
|
||||
description: 创建时不需要传 `company_id`,由数据库自动生成。
|
||||
properties:
|
||||
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:
|
||||
type: object
|
||||
description: >-
|
||||
更新时可只传需要修改的字段;记录定位统一依赖路径参数 `id`。
|
||||
properties:
|
||||
company_id:
|
||||
description: "所属公司业务 ID"
|
||||
type: string
|
||||
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
|
||||
CompanyRecord:
|
||||
allOf:
|
||||
@@ -418,8 +545,10 @@ components:
|
||||
collectionName:
|
||||
type: string
|
||||
created:
|
||||
description: "记录创建时间"
|
||||
type: string
|
||||
updated:
|
||||
description: "记录更新时间"
|
||||
type: string
|
||||
- $ref: '#/components/schemas/CompanyBase'
|
||||
CompanyListResponse:
|
||||
@@ -445,5 +574,6 @@ components:
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
description: "业务响应数据"
|
||||
type: object
|
||||
additionalProperties: true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
||||
|
||||
### 1. tbl_system_dict (系统词典)
|
||||
**类型:** Base Collection
|
||||
**读写权限:** 所有人可读;仅 `ManagePlatform`/管理员可写
|
||||
|
||||
| 字段名 | 类型 | 备注 |
|
||||
| :--- | :--- | :--- |
|
||||
@@ -30,23 +31,30 @@
|
||||
|
||||
| 字段名 | 类型 | 备注 |
|
||||
| :--- | :--- | :--- |
|
||||
| company_id | text | 自定义公司id |
|
||||
| company_id | text | 公司业务id,由数据库自动生成,默认形如 `WX-COMPANY-时间串` |
|
||||
| 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 | 备注 |
|
||||
|
||||
**索引规划 (Indexes):**
|
||||
* `CREATE UNIQUE INDEX` 针对 `company_id` (确保业务主键唯一)
|
||||
* `CREATE INDEX` 针对 `company_usci` (加速信用代码检索)
|
||||
* `CREATE INDEX` 针对 `company_owner_openid` (加速按公司所有者 openid 查询)
|
||||
|
||||
---
|
||||
|
||||
@@ -78,6 +86,7 @@
|
||||
| users_wx_openid | text | 微信号 |
|
||||
| users_level | text | 用户等级 |
|
||||
| users_type | text | 用户类型 |
|
||||
| users_tag | text | 用户标签 |
|
||||
| users_status | text | 用户状态 |
|
||||
| company_id | text | 公司id (存储 tbl_company.company_id) |
|
||||
| users_parent_id | text | 用户父级id (存储 tbl_users.users_id) |
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"init:newpb": "node pocketbase.newpb.js",
|
||||
"init:documents": "node pocketbase.documents.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": [],
|
||||
"author": "",
|
||||
|
||||
@@ -23,6 +23,11 @@ const pb = new PocketBase(PB_URL);
|
||||
const collectionData = {
|
||||
name: 'tbl_system_dict',
|
||||
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: [
|
||||
{ name: 'system_dict_id', type: 'text', required: true },
|
||||
{ name: 'dict_name', type: 'text', required: true },
|
||||
@@ -68,6 +73,11 @@ function buildCollectionPayload(target, existingCollection) {
|
||||
return {
|
||||
name: target.name,
|
||||
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)),
|
||||
indexes: target.indexes,
|
||||
};
|
||||
@@ -91,6 +101,11 @@ function buildCollectionPayload(target, existingCollection) {
|
||||
return {
|
||||
name: target.name,
|
||||
type: target.type,
|
||||
listRule: target.listRule,
|
||||
viewRule: target.viewRule,
|
||||
createRule: target.createRule,
|
||||
updateRule: target.updateRule,
|
||||
deleteRule: target.deleteRule,
|
||||
fields: fields,
|
||||
indexes: target.indexes,
|
||||
};
|
||||
|
||||
@@ -47,8 +47,11 @@ const collections = [
|
||||
{
|
||||
name: 'tbl_document',
|
||||
type: 'base',
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
fields: [
|
||||
{ name: 'document_id', type: 'text', required: true },
|
||||
{ name: 'document_create', type: 'autodate', onCreate: true, onUpdate: false },
|
||||
{ name: 'document_effect_date', type: 'date' },
|
||||
{ name: 'document_expiry_date', type: 'date' },
|
||||
{ name: 'document_type', type: 'text', required: true },
|
||||
@@ -77,6 +80,7 @@ const collections = [
|
||||
],
|
||||
indexes: [
|
||||
'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_type ON tbl_document (document_type)',
|
||||
'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;
|
||||
}
|
||||
|
||||
if (field.type === 'autodate') {
|
||||
payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true;
|
||||
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ const collections = [
|
||||
{
|
||||
name: 'tbl_system_dict',
|
||||
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: [
|
||||
{ name: 'system_dict_id', type: 'text', required: true },
|
||||
{ name: 'dict_name', type: 'text', required: true },
|
||||
@@ -33,23 +38,30 @@ const collections = [
|
||||
name: 'tbl_company',
|
||||
type: 'base',
|
||||
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_type', type: 'text' },
|
||||
{ name: 'company_entity', type: 'text' },
|
||||
{ name: 'company_usci', type: 'text' },
|
||||
{ name: 'company_nationality', type: 'text' },
|
||||
{ name: 'company_nationality_code', type: 'text' },
|
||||
{ name: 'company_province', type: 'text' },
|
||||
{ name: 'company_province_code', 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_add', type: 'text' },
|
||||
{ name: 'company_status', type: 'text' },
|
||||
{ name: 'company_level', type: 'text' },
|
||||
{ name: 'company_owner_openid', type: 'text' },
|
||||
{ name: 'company_remark', type: 'text' }
|
||||
],
|
||||
indexes: [
|
||||
'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 = {
|
||||
name: collectionData.name,
|
||||
type: collectionData.type,
|
||||
listRule: collectionData.listRule,
|
||||
viewRule: collectionData.viewRule,
|
||||
createRule: collectionData.createRule,
|
||||
updateRule: collectionData.updateRule,
|
||||
deleteRule: collectionData.deleteRule,
|
||||
fields: collectionData.fields,
|
||||
indexes: collectionData.indexes
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ const collections = [
|
||||
{ name: 'users_phone', type: 'text' },
|
||||
{ name: 'users_level', type: 'text' },
|
||||
{ name: 'users_type', type: 'text' },
|
||||
{ name: 'users_tag', type: 'text' },
|
||||
{ name: 'users_status', type: 'text' },
|
||||
{ name: 'company_id', type: 'text' },
|
||||
{ name: 'users_parent_id', type: 'text' },
|
||||
|
||||
125
script/test-tbl-company-native-api.js
Normal file
125
script/test-tbl-company-native-api.js
Normal 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()
|
||||
Reference in New Issue
Block a user