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

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

View File

@@ -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) |

View File

@@ -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": "",

View File

@@ -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,
};

View File

@@ -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;
}

View File

@@ -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
};

View File

@@ -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' },

View File

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