feat: 添加 PocketBase MiniApp 公司 API 文档和文件字段迁移脚本
- 新增 openapi-miniapp-company.yaml 文件,定义 tbl_company 的基础 CRUD 接口文档,包括查询、创建、更新和删除公司记录的详细描述和示例。 - 新增 pocketbase.file-fields-to-attachments.js 脚本,用于迁移 PocketBase 中的文件字段到文本字段,并处理 tbl_attachments 集合的公开规则。
This commit is contained in:
@@ -82,10 +82,10 @@
|
||||
| company_id | text | 公司id (存储 tbl_company.company_id) |
|
||||
| users_parent_id | text | 用户父级id (存储 tbl_users.users_id) |
|
||||
| users_promo_code | text | 用户推广码 |
|
||||
| users_id_pic_a | file | 用户证件照片(正) |
|
||||
| users_id_pic_b | file | 用户证件照片(反) |
|
||||
| users_title_picture | file | 用户资质照片 |
|
||||
| users_picture | file | 用户头像 |
|
||||
| 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 (存储 tbl_user_groups.usergroups_id) |
|
||||
|
||||
**索引规划 (Indexes):**
|
||||
@@ -93,5 +93,3 @@
|
||||
* `CREATE UNIQUE INDEX` 针对 `users_phone` (确保手机号唯一,加速登录查询)
|
||||
* `CREATE UNIQUE INDEX` 针对 `users_wx_openid` (确保微信开放ID唯一)
|
||||
* `CREATE INDEX` 针对 `company_id`, `usergroups_id`, `users_parent_id` (加速这三个高频业务外键的匹配查询)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"init:newpb": "node pocketbase.newpb.js",
|
||||
"init:documents": "node pocketbase.documents.js",
|
||||
"init:dictionary": "node pocketbase.dictionary.js"
|
||||
"init:dictionary": "node pocketbase.dictionary.js",
|
||||
"migrate:file-fields": "node pocketbase.file-fields-to-attachments.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
@@ -24,6 +24,8 @@ const collections = [
|
||||
{
|
||||
name: 'tbl_attachments',
|
||||
type: 'base',
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
fields: [
|
||||
{ name: 'attachments_id', type: 'text', required: true },
|
||||
{ name: 'attachments_link', type: 'file', maxSelect: 0, maxSize: 4294967296, mimeTypes: [] },
|
||||
@@ -56,6 +58,7 @@ const collections = [
|
||||
{ name: 'document_content', type: 'text' },
|
||||
{ name: 'document_image', type: 'text' },
|
||||
{ name: 'document_video', type: 'text' },
|
||||
{ name: 'document_file', type: 'text' },
|
||||
{ name: 'document_owner', type: 'text' },
|
||||
{ name: 'document_relation_model', type: 'text' },
|
||||
{ name: 'document_keywords', type: 'text' },
|
||||
@@ -135,6 +138,11 @@ function buildCollectionPayload(collectionData, existingCollection) {
|
||||
return {
|
||||
name: collectionData.name,
|
||||
type: collectionData.type,
|
||||
listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : null,
|
||||
viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : null,
|
||||
createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : null,
|
||||
updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : null,
|
||||
deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : null,
|
||||
fields: collectionData.fields.map((field) => normalizeFieldPayload(field, null)),
|
||||
indexes: collectionData.indexes,
|
||||
};
|
||||
@@ -158,6 +166,11 @@ function buildCollectionPayload(collectionData, existingCollection) {
|
||||
return {
|
||||
name: collectionData.name,
|
||||
type: collectionData.type,
|
||||
listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : existingCollection.listRule,
|
||||
viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : existingCollection.viewRule,
|
||||
createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : existingCollection.createRule,
|
||||
updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : existingCollection.updateRule,
|
||||
deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : existingCollection.deleteRule,
|
||||
fields: fields,
|
||||
indexes: collectionData.indexes,
|
||||
};
|
||||
|
||||
142
script/pocketbase.file-fields-to-attachments.js
Normal file
142
script/pocketbase.file-fields-to-attachments.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { createRequire } from 'module';
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
let runtimeConfig = {};
|
||||
try {
|
||||
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
|
||||
} catch (_error) {
|
||||
runtimeConfig = {};
|
||||
}
|
||||
|
||||
const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, '');
|
||||
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
|
||||
|
||||
if (!AUTH_TOKEN) {
|
||||
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行文件字段迁移。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pb = new PocketBase(PB_URL);
|
||||
|
||||
function buildReplacementTextField(field) {
|
||||
return {
|
||||
name: field.name,
|
||||
type: 'text',
|
||||
required: !!field.required,
|
||||
hidden: !!field.hidden,
|
||||
presentable: typeof field.presentable === 'boolean' ? field.presentable : true,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCollectionPayload(remote, fields) {
|
||||
const payload = {
|
||||
name: remote.name,
|
||||
type: remote.type,
|
||||
fields: fields,
|
||||
indexes: remote.indexes || [],
|
||||
listRule: remote.listRule,
|
||||
viewRule: remote.viewRule,
|
||||
createRule: remote.createRule,
|
||||
updateRule: remote.updateRule,
|
||||
deleteRule: remote.deleteRule,
|
||||
};
|
||||
|
||||
if (remote.name === 'tbl_attachments') {
|
||||
payload.listRule = '';
|
||||
payload.viewRule = '';
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function migrateCollections() {
|
||||
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
|
||||
pb.authStore.save(AUTH_TOKEN, null);
|
||||
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
|
||||
|
||||
const collections = await pb.collections.getFullList({ sort: 'name' });
|
||||
const changed = [];
|
||||
|
||||
for (const remote of collections) {
|
||||
const fileFields = (remote.fields || []).filter((field) => field.type === 'file');
|
||||
const shouldOpenAttachments = remote.name === 'tbl_attachments' && (remote.listRule !== '' || remote.viewRule !== '');
|
||||
|
||||
if (!fileFields.length && !shouldOpenAttachments) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🔄 正在处理集合: ${remote.name}`);
|
||||
if (remote.name === 'tbl_attachments') {
|
||||
await pb.collections.update(remote.id, buildCollectionPayload(remote, remote.fields || []));
|
||||
} else if (fileFields.length) {
|
||||
const remainingFields = (remote.fields || []).filter((field) => field.type !== 'file');
|
||||
await pb.collections.update(remote.id, buildCollectionPayload(remote, remainingFields));
|
||||
|
||||
const refreshed = await pb.collections.getOne(remote.name);
|
||||
const replacementFields = fileFields.map((field) => buildReplacementTextField(field));
|
||||
await pb.collections.update(
|
||||
refreshed.id,
|
||||
buildCollectionPayload(refreshed, (refreshed.fields || []).concat(replacementFields)),
|
||||
);
|
||||
}
|
||||
|
||||
changed.push({
|
||||
name: remote.name,
|
||||
convertedFileFields: remote.name === 'tbl_attachments'
|
||||
? []
|
||||
: fileFields.map((field) => field.name),
|
||||
attachmentsRulesOpened: remote.name === 'tbl_attachments',
|
||||
});
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
async function verifyResult() {
|
||||
const collections = await pb.collections.getFullList({ sort: 'name' });
|
||||
const residualFileFields = [];
|
||||
let attachmentsRules = null;
|
||||
|
||||
for (const remote of collections) {
|
||||
for (const field of (remote.fields || [])) {
|
||||
if (field.type === 'file' && remote.name !== 'tbl_attachments') {
|
||||
residualFileFields.push(`${remote.name}.${field.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (remote.name === 'tbl_attachments') {
|
||||
attachmentsRules = {
|
||||
listRule: remote.listRule,
|
||||
viewRule: remote.viewRule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (residualFileFields.length) {
|
||||
throw new Error(`仍存在未迁移的 file 字段: ${residualFileFields.join(', ')}`);
|
||||
}
|
||||
|
||||
if (!attachmentsRules || attachmentsRules.listRule !== '' || attachmentsRules.viewRule !== '') {
|
||||
throw new Error('tbl_attachments 的公开 list/view 规则未生效');
|
||||
}
|
||||
|
||||
console.log('✅ 校验通过:除 tbl_attachments 外已无 file 字段。');
|
||||
console.log('✅ 校验通过:tbl_attachments 已开放公共 list/view。');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const changed = await migrateCollections();
|
||||
console.log('📝 本次更新集合:');
|
||||
console.log(JSON.stringify(changed, null, 2));
|
||||
await verifyResult();
|
||||
console.log('🎉 文件字段迁移完成!');
|
||||
} catch (error) {
|
||||
console.error('❌ 文件字段迁移失败:', error.response?.data || error.message || error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -81,10 +81,10 @@ const collections = [
|
||||
{ name: 'company_id', type: 'text' },
|
||||
{ name: 'users_parent_id', type: 'text' },
|
||||
{ name: 'users_promo_code', type: 'text' },
|
||||
{ name: 'users_id_pic_a', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_id_pic_b', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_title_picture', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_picture', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_id_pic_a', type: 'text' },
|
||||
{ name: 'users_id_pic_b', type: 'text' },
|
||||
{ name: 'users_title_picture', type: 'text' },
|
||||
{ name: 'users_picture', type: 'text' },
|
||||
{ name: 'usergroups_id', type: 'text' }
|
||||
],
|
||||
indexes: [
|
||||
@@ -174,4 +174,4 @@ async function verifyCollections(targetCollections) {
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
init();
|
||||
|
||||
@@ -56,9 +56,9 @@ const collections = [
|
||||
{ name: 'company_id', type: 'text' },
|
||||
{ name: 'users_parent_id', type: 'text' },
|
||||
{ name: 'users_promo_code', type: 'text' },
|
||||
{ name: 'users_id_pic_a', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_id_pic_b', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_title_picture', type: 'file', options: { maxSelect: 1, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'] } },
|
||||
{ name: 'users_id_pic_a', type: 'text' },
|
||||
{ name: 'users_id_pic_b', type: 'text' },
|
||||
{ name: 'users_title_picture', type: 'text' },
|
||||
{ name: 'users_picture', type: 'text' },
|
||||
{ name: 'usergroups_id', type: 'text' }
|
||||
],
|
||||
@@ -256,4 +256,4 @@ async function verifyCollections(targetCollections) {
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user