feat: 添加文档管理和登录页面功能,包含文档上传、列表展示及用户登录逻辑;新增数据库表结构初始化脚本
This commit is contained in:
272
script/pocketbase.documents.js
Normal file
272
script/pocketbase.documents.js
Normal file
@@ -0,0 +1,272 @@
|
||||
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);
|
||||
|
||||
const collections = [
|
||||
{
|
||||
name: 'tbl_attachments',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'attachments_id', type: 'text', required: true },
|
||||
{ name: 'attachments_link', type: 'file', maxSelect: 0, maxSize: 104857600, mimeTypes: [] },
|
||||
{ name: 'attachments_filename', type: 'text' },
|
||||
{ name: 'attachments_filetype', type: 'text' },
|
||||
{ name: 'attachments_size', type: 'number' },
|
||||
{ name: 'attachments_owner', type: 'text' },
|
||||
{ name: 'attachments_md5', type: 'text' },
|
||||
{ name: 'attachments_ocr', type: 'text' },
|
||||
{ name: 'attachments_status', type: 'text' },
|
||||
{ name: 'attachments_remark', type: 'text' },
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_attachments_attachments_id ON tbl_attachments (attachments_id)',
|
||||
'CREATE INDEX idx_tbl_attachments_attachments_owner ON tbl_attachments (attachments_owner)',
|
||||
'CREATE INDEX idx_tbl_attachments_attachments_status ON tbl_attachments (attachments_status)',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'tbl_document',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'document_id', type: 'text', required: true },
|
||||
{ name: 'document_effect_date', type: 'date' },
|
||||
{ name: 'document_expiry_date', type: 'date' },
|
||||
{ name: 'document_type', type: 'text' },
|
||||
{ name: 'document_title', type: 'text' },
|
||||
{ name: 'document_subtitle', type: 'text' },
|
||||
{ name: 'document_summary', type: 'text' },
|
||||
{ name: 'document_content', type: 'text' },
|
||||
{ name: 'document_image', type: 'text' },
|
||||
{ name: 'document_video', type: 'text' },
|
||||
{ name: 'document_owner', type: 'text' },
|
||||
{ name: 'document_relation_model', type: 'text' },
|
||||
{ name: 'document_keywords', type: 'text' },
|
||||
{ name: 'document_share_count', type: 'number' },
|
||||
{ name: 'document_download_count', type: 'number' },
|
||||
{ name: 'document_favorite_count', type: 'number' },
|
||||
{ name: 'document_status', type: 'text' },
|
||||
{ name: 'document_embedding_status', type: 'text' },
|
||||
{ name: 'document_embedding_error', type: 'text' },
|
||||
{ name: 'document_embedding_lasttime', type: 'date' },
|
||||
{ name: 'document_vector_version', type: 'text' },
|
||||
{ name: 'document_product_categories', type: 'text' },
|
||||
{ name: 'document_application_scenarios', type: 'text' },
|
||||
{ name: 'document_hotel_type', type: 'text' },
|
||||
{ name: 'document_remark', type: 'text' },
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)',
|
||||
'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)',
|
||||
'CREATE INDEX idx_tbl_document_document_embedding_status ON tbl_document (document_embedding_status)',
|
||||
'CREATE INDEX idx_tbl_document_document_effect_date ON tbl_document (document_effect_date)',
|
||||
'CREATE INDEX idx_tbl_document_document_expiry_date ON tbl_document (document_expiry_date)',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'tbl_document_operation_history',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'doh_id', type: 'text', required: true },
|
||||
{ name: 'doh_document_id', type: 'text', required: true },
|
||||
{ name: 'doh_operation_type', type: 'text' },
|
||||
{ name: 'doh_user_id', type: 'text' },
|
||||
{ name: 'doh_current_count', type: 'number' },
|
||||
{ name: 'doh_remark', type: 'text' },
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_tbl_document_operation_history_doh_id ON tbl_document_operation_history (doh_id)',
|
||||
'CREATE INDEX idx_tbl_document_operation_history_doh_document_id ON tbl_document_operation_history (doh_document_id)',
|
||||
'CREATE INDEX idx_tbl_document_operation_history_doh_user_id ON tbl_document_operation_history (doh_user_id)',
|
||||
'CREATE INDEX idx_tbl_document_operation_history_doh_operation_type ON tbl_document_operation_history (doh_operation_type)',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeFieldPayload(field, existingField) {
|
||||
const payload = existingField
|
||||
? Object.assign({}, existingField)
|
||||
: {
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
};
|
||||
|
||||
if (existingField && existingField.id) {
|
||||
payload.id = existingField.id;
|
||||
}
|
||||
|
||||
payload.name = field.name;
|
||||
payload.type = field.type;
|
||||
|
||||
if (typeof field.required !== 'undefined') {
|
||||
payload.required = field.required;
|
||||
}
|
||||
|
||||
if (field.type === 'file') {
|
||||
payload.maxSelect = typeof field.maxSelect === 'number' ? field.maxSelect : 0;
|
||||
payload.maxSize = typeof field.maxSize === 'number' ? field.maxSize : 0;
|
||||
payload.mimeTypes = Array.isArray(field.mimeTypes) && field.mimeTypes.length ? field.mimeTypes : null;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildCollectionPayload(collectionData, existingCollection) {
|
||||
if (!existingCollection) {
|
||||
return {
|
||||
name: collectionData.name,
|
||||
type: collectionData.type,
|
||||
fields: collectionData.fields.map((field) => normalizeFieldPayload(field, null)),
|
||||
indexes: collectionData.indexes,
|
||||
};
|
||||
}
|
||||
|
||||
const targetFieldMap = new Map(collectionData.fields.map((field) => [field.name, field]));
|
||||
const fields = (existingCollection.fields || []).map((existingField) => {
|
||||
const targetField = targetFieldMap.get(existingField.name);
|
||||
if (!targetField) {
|
||||
return existingField;
|
||||
}
|
||||
|
||||
targetFieldMap.delete(existingField.name);
|
||||
return normalizeFieldPayload(targetField, existingField);
|
||||
});
|
||||
|
||||
for (const field of targetFieldMap.values()) {
|
||||
fields.push(normalizeFieldPayload(field, null));
|
||||
}
|
||||
|
||||
return {
|
||||
name: collectionData.name,
|
||||
type: collectionData.type,
|
||||
fields: fields,
|
||||
indexes: collectionData.indexes,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFieldList(fields) {
|
||||
return (fields || []).map((field) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
}));
|
||||
}
|
||||
|
||||
async function createOrUpdateCollection(collectionData) {
|
||||
console.log(`🔄 正在处理表: ${collectionData.name} ...`);
|
||||
|
||||
try {
|
||||
const existing = await pb.collections.getOne(collectionData.name);
|
||||
await pb.collections.update(existing.id, buildCollectionPayload(collectionData, existing));
|
||||
console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`);
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
try {
|
||||
await pb.collections.create(buildCollectionPayload(collectionData, null));
|
||||
console.log(`✅ ${collectionData.name} 创建完成。`);
|
||||
return;
|
||||
} catch (createError) {
|
||||
console.error(`❌ 创建集合 ${collectionData.name} 失败:`, {
|
||||
status: createError.status,
|
||||
message: createError.message,
|
||||
response: createError.response?.data,
|
||||
});
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`❌ 处理集合 ${collectionData.name} 失败:`, {
|
||||
status: error.status,
|
||||
message: error.message,
|
||||
response: error.response?.data,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCollections(targetCollections) {
|
||||
console.log('\n🔍 开始校验文档相关表结构与索引...');
|
||||
|
||||
for (const target of targetCollections) {
|
||||
const remote = await pb.collections.getOne(target.name);
|
||||
const remoteFields = normalizeFieldList(remote.fields);
|
||||
const targetFields = normalizeFieldList(target.fields);
|
||||
const remoteFieldMap = new Map(remoteFields.map((field) => [field.name, field.type]));
|
||||
const missingFields = [];
|
||||
const mismatchedTypes = [];
|
||||
|
||||
for (const field of targetFields) {
|
||||
if (!remoteFieldMap.has(field.name)) {
|
||||
missingFields.push(field.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (remoteFieldMap.get(field.name) !== field.type) {
|
||||
mismatchedTypes.push(`${field.name}:${remoteFieldMap.get(field.name)}!=${field.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
const remoteIndexes = new Set(remote.indexes || []);
|
||||
const missingIndexes = target.indexes.filter((indexSql) => !remoteIndexes.has(indexSql));
|
||||
|
||||
if (remote.type !== target.type) {
|
||||
throw new Error(`${target.name} 类型不匹配,期望 ${target.type},实际 ${remote.type}`);
|
||||
}
|
||||
|
||||
if (!missingFields.length && !mismatchedTypes.length && !missingIndexes.length) {
|
||||
console.log(`✅ ${target.name} 校验通过。`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`❌ ${target.name} 校验失败:`);
|
||||
if (missingFields.length) {
|
||||
console.log(` - 缺失字段: ${missingFields.join(', ')}`);
|
||||
}
|
||||
if (mismatchedTypes.length) {
|
||||
console.log(` - 字段类型不匹配: ${mismatchedTypes.join(', ')}`);
|
||||
}
|
||||
if (missingIndexes.length) {
|
||||
console.log(` - 缺失索引: ${missingIndexes.join(' | ')}`);
|
||||
}
|
||||
|
||||
throw new Error(`${target.name} 结构与预期不一致`);
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
|
||||
pb.authStore.save(AUTH_TOKEN, null);
|
||||
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
|
||||
|
||||
for (const collectionData of collections) {
|
||||
await createOrUpdateCollection(collectionData);
|
||||
}
|
||||
|
||||
await verifyCollections(collections);
|
||||
console.log('\n🎉 文档相关表结构初始化并校验完成!');
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化失败:', error.response?.data || error.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
Reference in New Issue
Block a user