feat: 添加 PocketBase 管理端与自定义 hooks 的 API 文档

- 新增 openapi.yaml 文件,定义管理端与自定义 hooks 的接口文档,包括系统、微信认证、平台认证、字典管理、附件管理、文档管理、购物车和订单等接口。
- 新增 order.yaml 文件,定义订单相关的接口,包括查询订单列表、查询订单详情、新增订单记录、修改订单记录和删除订单记录的请求和响应结构。
- 新增 openapi-wx/openapi.yaml 文件,定义 PocketBase 原生 API 文档,包含企业信息、附件信息、产品信息、文档信息、购物车和订单的接口。
- 新增 pocketbase.scheme.js 文件,包含 PocketBase 集合的创建和更新逻辑,定义了多个集合的字段、索引和权限规则。
This commit is contained in:
2026-04-08 20:14:22 +08:00
parent e47060f54f
commit 0bdaf54eed
36 changed files with 4391 additions and 2418 deletions

View File

@@ -9,6 +9,7 @@
"init:documents": "node pocketbase.documents.js",
"init:product-list": "node pocketbase.product-list.js",
"init:dictionary": "node pocketbase.dictionary.js",
"init:scheme": "node pocketbase.scheme.js",
"migrate:file-fields": "node pocketbase.file-fields-to-attachments.js",
"migrate:add-is-delete-field": "node pocketbase.add-is-delete-field.js",
"migrate:apply-soft-delete-rules": "node pocketbase.apply-soft-delete-rules.js",

View File

@@ -24,24 +24,38 @@ if (!AUTH_TOKEN) {
const pb = new PocketBase(PB_URL);
const collections = [
async function getCollectionIdByName(collectionName) {
const list = await pb.collections.getFullList({
sort: '-created',
});
const target = list.find((item) => item.name === collectionName);
if (!target) {
throw new Error(`未找到集合:${collectionName}`);
}
return target.id;
}
async function buildCollections() {
const productCollectionId = await getCollectionIdByName('tbl_product_list');
return [
{
name: 'tbl_cart',
type: 'base',
listRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
viewRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
createRule: `${OWNER_AUTH_RULE} && @request.body.cart_owner = @request.auth.openid`,
createRule: '@request.body.cart_owner != ""',
updateRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
deleteRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
fields: [
{ name: 'cart_id', type: 'text', required: true, autogeneratePattern: 'CART-[0-9]{13}-[A-Za-z0-9]{6}' },
{ name: 'cart_number', type: 'text', required: true },
{ name: 'cart_number', type: 'text', required: false },
{ name: 'cart_create', type: 'autodate', onCreate: true, onUpdate: false },
{ name: 'cart_owner', type: 'text', required: true },
{ name: 'cart_product_id', type: 'text', required: true },
{ name: 'cart_product_quantity', type: 'number', required: true },
{ name: 'cart_status', type: 'text', required: true },
{ name: 'cart_at_price', type: 'number', required: true },
{ name: 'cart_product_id', type: 'relation', required: true, collectionId: productCollectionId, maxSelect: 1, cascadeDelete: false },
{ name: 'cart_product_quantity', type: 'number', required: false },
{ name: 'cart_status', type: 'text', required: false },
{ name: 'cart_at_price', type: 'number', required: false },
{ name: 'cart_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
],
@@ -88,7 +102,8 @@ const collections = [
'CREATE INDEX idx_tbl_order_owner_status ON tbl_order (order_owner, order_status)',
],
},
];
];
}
function normalizeFieldPayload(field, existingField) {
const payload = existingField
@@ -114,6 +129,12 @@ function normalizeFieldPayload(field, existingField) {
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
}
if (field.type === 'relation') {
payload.collectionId = field.collectionId;
payload.maxSelect = typeof field.maxSelect === 'number' ? field.maxSelect : 1;
payload.cascadeDelete = typeof field.cascadeDelete === 'boolean' ? field.cascadeDelete : false;
}
return payload;
}
@@ -270,6 +291,8 @@ async function init() {
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
const collections = await buildCollections();
for (const collectionData of collections) {
await createOrUpdateCollection(collectionData);
}

328
script/pocketbase.scheme.js Normal file
View File

@@ -0,0 +1,328 @@
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 || '';
const OWNER_AUTH_RULE = '@request.auth.id != ""';
const SOFT_DELETE_RULE = 'is_delete = 0';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行方案相关建表。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
const collections = [
{
name: 'tbl_scheme_template',
type: 'base',
listRule: `${OWNER_AUTH_RULE} && scheme_template_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
viewRule: `${OWNER_AUTH_RULE} && scheme_template_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
createRule: `${OWNER_AUTH_RULE} && @request.body.scheme_template_owner = @request.auth.openid`,
updateRule: `${OWNER_AUTH_RULE} && scheme_template_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
deleteRule: `${OWNER_AUTH_RULE} && scheme_template_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
fields: [
{ name: 'scheme_template_id', type: 'text', required: true, autogeneratePattern: 'SCHTPL-[0-9]{13}-[A-Za-z0-9]{6}' },
{ name: 'scheme_template_icon', type: 'text' },
{ name: 'scheme_template_label', type: 'text' },
{ name: 'scheme_template_name', type: 'text', required: true },
{ name: 'scheme_template_owner', type: 'text', required: true },
{ name: 'scheme_template_status', type: 'text' },
{ name: 'scheme_template_solution_type', type: 'text' },
{ name: 'scheme_template_solution_feature', type: 'text' },
{ name: 'scheme_template_product_list', type: 'json' },
{ name: 'scheme_template_description', type: 'text' },
{ name: 'scheme_template_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
],
indexes: [
'CREATE UNIQUE INDEX idx_tbl_scheme_template_scheme_template_id ON tbl_scheme_template (scheme_template_id)',
'CREATE INDEX idx_tbl_scheme_template_scheme_template_owner ON tbl_scheme_template (scheme_template_owner)',
'CREATE INDEX idx_tbl_scheme_template_scheme_template_name ON tbl_scheme_template (scheme_template_name)',
'CREATE INDEX idx_tbl_scheme_template_scheme_template_label ON tbl_scheme_template (scheme_template_label)',
'CREATE INDEX idx_tbl_scheme_template_scheme_template_status ON tbl_scheme_template (scheme_template_status)',
'CREATE INDEX idx_tbl_scheme_template_solution_type ON tbl_scheme_template (scheme_template_solution_type)',
'CREATE INDEX idx_tbl_scheme_template_solution_feature ON tbl_scheme_template (scheme_template_solution_feature)',
'CREATE INDEX idx_tbl_scheme_template_owner_status ON tbl_scheme_template (scheme_template_owner, scheme_template_status)',
],
},
{
name: 'tbl_scheme',
type: 'base',
listRule: `${OWNER_AUTH_RULE} && scheme_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
viewRule: `${OWNER_AUTH_RULE} && scheme_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
createRule: `${OWNER_AUTH_RULE} && @request.body.scheme_owner = @request.auth.openid`,
updateRule: `${OWNER_AUTH_RULE} && scheme_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
deleteRule: `${OWNER_AUTH_RULE} && scheme_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
fields: [
{ name: 'scheme_id', type: 'text', required: true, autogeneratePattern: 'SCHEME-[0-9]{13}-[A-Za-z0-9]{6}' },
{ name: 'scheme_name', type: 'text', required: true },
{ name: 'scheme_owner', type: 'text', required: true },
{ name: 'scheme_share_status', type: 'text' },
{ name: 'scheme_expires_at', type: 'date' },
{ name: 'scheme_hotel_type', type: 'text' },
{ name: 'scheme_solution_type', type: 'text' },
{ name: 'scheme_solution_feature', type: 'text' },
{ name: 'scheme_room_type', type: 'json' },
{ name: 'scheme_curtains', type: 'text' },
{ name: 'scheme_voice_device', type: 'text' },
{ name: 'scheme_ac_type', type: 'text' },
{ name: 'scheme_template_highend', type: 'text' },
{ name: 'scheme_template_midend', type: 'text' },
{ name: 'scheme_template_lowend', type: 'text' },
{ name: 'scheme_status', type: 'text' },
{ name: 'scheme_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
],
indexes: [
'CREATE UNIQUE INDEX idx_tbl_scheme_scheme_id ON tbl_scheme (scheme_id)',
'CREATE INDEX idx_tbl_scheme_scheme_owner ON tbl_scheme (scheme_owner)',
'CREATE INDEX idx_tbl_scheme_scheme_name ON tbl_scheme (scheme_name)',
'CREATE INDEX idx_tbl_scheme_scheme_share_status ON tbl_scheme (scheme_share_status)',
'CREATE INDEX idx_tbl_scheme_scheme_expires_at ON tbl_scheme (scheme_expires_at)',
'CREATE INDEX idx_tbl_scheme_scheme_hotel_type ON tbl_scheme (scheme_hotel_type)',
'CREATE INDEX idx_tbl_scheme_scheme_solution_type ON tbl_scheme (scheme_solution_type)',
'CREATE INDEX idx_tbl_scheme_scheme_solution_feature ON tbl_scheme (scheme_solution_feature)',
'CREATE INDEX idx_tbl_scheme_scheme_status ON tbl_scheme (scheme_status)',
'CREATE INDEX idx_tbl_scheme_owner_status ON tbl_scheme (scheme_owner, scheme_status)',
],
},
{
name: 'tbl_scheme_share',
type: 'base',
listRule: `${OWNER_AUTH_RULE} && scheme_share_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
viewRule: `${OWNER_AUTH_RULE} && scheme_share_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
createRule: `${OWNER_AUTH_RULE} && @request.body.scheme_share_owner = @request.auth.openid`,
updateRule: `${OWNER_AUTH_RULE} && scheme_share_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
deleteRule: `${OWNER_AUTH_RULE} && scheme_share_owner = @request.auth.openid && ${SOFT_DELETE_RULE}`,
fields: [
{ name: 'scheme_share_id', type: 'text', required: true, autogeneratePattern: 'SCHSHARE-[0-9]{13}-[A-Za-z0-9]{6}' },
{ name: 'scheme_id', type: 'text', required: true },
{ name: 'scheme_share_owner', type: 'text', required: true },
{ name: 'scheme_share_to', type: 'text', required: true },
{ name: 'scheme_share_acceptance_status', type: 'text' },
{ name: 'scheme_share_expires_at', type: 'date' },
{ name: 'scheme_share_permission', type: 'text' },
{ name: 'scheme_share_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
],
indexes: [
'CREATE UNIQUE INDEX idx_tbl_scheme_share_scheme_share_id ON tbl_scheme_share (scheme_share_id)',
'CREATE INDEX idx_tbl_scheme_share_scheme_id ON tbl_scheme_share (scheme_id)',
'CREATE INDEX idx_tbl_scheme_share_scheme_share_owner ON tbl_scheme_share (scheme_share_owner)',
'CREATE INDEX idx_tbl_scheme_share_scheme_share_to ON tbl_scheme_share (scheme_share_to)',
'CREATE INDEX idx_tbl_scheme_share_acceptance_status ON tbl_scheme_share (scheme_share_acceptance_status)',
'CREATE INDEX idx_tbl_scheme_share_expires_at ON tbl_scheme_share (scheme_share_expires_at)',
'CREATE INDEX idx_tbl_scheme_share_owner_to ON tbl_scheme_share (scheme_share_owner, scheme_share_to)',
'CREATE UNIQUE INDEX idx_tbl_scheme_share_unique_map ON tbl_scheme_share (scheme_share_owner, scheme_id, scheme_share_to)',
],
},
];
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 === 'autodate') {
payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true;
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
}
if (field.type === 'date') {
payload.required = !!field.required;
}
return payload;
}
function buildCollectionPayload(collectionData, existingCollection) {
if (!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,
};
}
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,
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,
};
}
function normalizeFieldList(fields) {
return (fields || []).map((field) => ({
name: field.name,
type: field.type,
required: !!field.required,
}));
}
async function createOrUpdateCollection(collectionData) {
console.log(`🔄 正在处理表: ${collectionData.name} ...`);
try {
const list = await pb.collections.getFullList({
sort: '-created',
});
const existing = list.find((item) => item.name === collectionData.name);
if (existing) {
await pb.collections.update(existing.id, buildCollectionPayload(collectionData, existing));
console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`);
return;
}
await pb.collections.create(buildCollectionPayload(collectionData, null));
console.log(`${collectionData.name} 创建完成。`);
} catch (error) {
console.error(`❌ 处理集合 ${collectionData.name} 失败:`, {
status: error.status,
message: error.message,
response: error.response,
});
throw error;
}
}
async function getCollectionByName(collectionName) {
const list = await pb.collections.getFullList({
sort: '-created',
});
return list.find((item) => item.name === collectionName) || null;
}
async function verifyCollections(targetCollections) {
console.log('\n🔍 开始校验方案相关表结构与索引...');
for (const target of targetCollections) {
const remote = await getCollectionByName(target.name);
if (!remote) {
throw new Error(`${target.name} 不存在`);
}
const remoteFields = normalizeFieldList(remote.fields);
const targetFields = normalizeFieldList(target.fields);
const remoteFieldMap = new Map(remoteFields.map((field) => [field.name, field.type]));
const remoteRequiredMap = new Map(remoteFields.map((field) => [field.name, field.required]));
const missingFields = [];
const mismatchedTypes = [];
const mismatchedRequired = [];
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}`);
}
if (remoteRequiredMap.get(field.name) !== !!field.required) {
mismatchedRequired.push(`${field.name}:${remoteRequiredMap.get(field.name)}!=${!!field.required}`);
}
}
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 && !mismatchedRequired.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 (mismatchedRequired.length) {
console.log(` - 字段必填属性不匹配: ${mismatchedRequired.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();