- 新增购物车与订单管理页面,包含用户列表、购物车详情和订单记录展示功能。 - 实现用户搜索、刷新、重置和退出登录功能。 - 新增购物车和订单数据表结构初始化脚本,包含字段、索引及权限规则设置。 - 实现数据表的创建与更新逻辑,并进行结构校验。
283 lines
12 KiB
JavaScript
283 lines
12 KiB
JavaScript
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 CART_OWNER_MATCH_RULE = 'cart_owner = @request.auth.openid';
|
||
const ORDER_OWNER_MATCH_RULE = 'order_owner = @request.auth.openid';
|
||
|
||
if (!AUTH_TOKEN) {
|
||
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行建表。');
|
||
process.exit(1);
|
||
}
|
||
|
||
const pb = new PocketBase(PB_URL);
|
||
|
||
const collections = [
|
||
{
|
||
name: 'tbl_cart',
|
||
type: 'base',
|
||
listRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
|
||
viewRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
|
||
createRule: `${OWNER_AUTH_RULE} && @request.body.cart_owner = @request.auth.openid`,
|
||
updateRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
|
||
deleteRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
|
||
fields: [
|
||
{ name: 'cart_id', type: 'text', required: true },
|
||
{ name: 'cart_number', type: 'text', required: true },
|
||
{ 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_remark', type: 'text' },
|
||
],
|
||
indexes: [
|
||
'CREATE UNIQUE INDEX idx_tbl_cart_cart_id ON tbl_cart (cart_id)',
|
||
'CREATE INDEX idx_tbl_cart_cart_number ON tbl_cart (cart_number)',
|
||
'CREATE INDEX idx_tbl_cart_cart_owner ON tbl_cart (cart_owner)',
|
||
'CREATE INDEX idx_tbl_cart_cart_product_id ON tbl_cart (cart_product_id)',
|
||
'CREATE INDEX idx_tbl_cart_cart_status ON tbl_cart (cart_status)',
|
||
'CREATE INDEX idx_tbl_cart_cart_create ON tbl_cart (cart_create)',
|
||
'CREATE INDEX idx_tbl_cart_owner_number ON tbl_cart (cart_owner, cart_number)',
|
||
'CREATE INDEX idx_tbl_cart_owner_status ON tbl_cart (cart_owner, cart_status)',
|
||
],
|
||
},
|
||
{
|
||
name: 'tbl_order',
|
||
type: 'base',
|
||
listRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
|
||
viewRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
|
||
createRule: `${OWNER_AUTH_RULE} && @request.body.order_owner = @request.auth.openid`,
|
||
updateRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
|
||
deleteRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
|
||
fields: [
|
||
{ name: 'order_id', type: 'text', required: true },
|
||
{ name: 'order_number', type: 'text', required: true },
|
||
{ name: 'order_create', type: 'autodate', onCreate: true, onUpdate: false },
|
||
{ name: 'order_owner', type: 'text', required: true },
|
||
{ name: 'order_source', type: 'text', required: true },
|
||
{ name: 'order_status', type: 'text', required: true },
|
||
{ name: 'order_source_id', type: 'text', required: true },
|
||
{ name: 'order_snap', type: 'json', required: true },
|
||
{ name: 'order_amount', type: 'number', required: true },
|
||
{ name: 'order_remark', type: 'text' },
|
||
],
|
||
indexes: [
|
||
'CREATE UNIQUE INDEX idx_tbl_order_order_id ON tbl_order (order_id)',
|
||
'CREATE UNIQUE INDEX idx_tbl_order_order_number ON tbl_order (order_number)',
|
||
'CREATE INDEX idx_tbl_order_order_owner ON tbl_order (order_owner)',
|
||
'CREATE INDEX idx_tbl_order_order_source ON tbl_order (order_source)',
|
||
'CREATE INDEX idx_tbl_order_order_status ON tbl_order (order_status)',
|
||
'CREATE INDEX idx_tbl_order_order_source_id ON tbl_order (order_source_id)',
|
||
'CREATE INDEX idx_tbl_order_order_create ON tbl_order (order_create)',
|
||
'CREATE INDEX idx_tbl_order_owner_status ON tbl_order (order_owner, order_status)',
|
||
],
|
||
},
|
||
];
|
||
|
||
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;
|
||
}
|
||
|
||
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();
|