feat: 添加订单相关字段和自动生成规则,包括订单商品数量和支付状态;更新文档和迁移脚本

This commit is contained in:
2026-04-11 17:36:16 +08:00
parent ec6b59b4fa
commit c7681e50fe
9 changed files with 739 additions and 16 deletions

View File

@@ -12,7 +12,10 @@
"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:add-order-product-quantity-field": "node pocketbase.add-order-product-quantity-field.js",
"migrate:add-order-pay-status-field": "node pocketbase.add-order-pay-status-field.js",
"migrate:apply-soft-delete-rules": "node pocketbase.apply-soft-delete-rules.js",
"migrate:ensure-order-autogen-fields": "node pocketbase.ensure-order-autogen-fields.js",
"migrate:ensure-cart-order-autogen-id": "node pocketbase.ensure-cart-order-autogen-id.js",
"migrate:cart-active-unique-index": "node pocketbase.cart-active-unique-index.js",
"migrate:product-params-array": "node migrate-product-parameters-to-array.js",

View File

@@ -0,0 +1,187 @@
import { createRequire } from 'module';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import PocketBase from 'pocketbase';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let runtimeConfig = {};
try {
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
} catch (_error) {
runtimeConfig = {};
}
function readEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf8');
const result = {};
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const index = line.indexOf('=');
if (index === -1) continue;
const key = line.slice(0, index).trim();
const value = line.slice(index + 1).trim();
result[key] = value;
}
return result;
}
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
const PB_URL = String(
process.env.PB_URL
|| backendEnv.POCKETBASE_API_URL
|| runtimeConfig.POCKETBASE_API_URL
|| 'http://127.0.0.1:8090'
).replace(/\/+$/, '');
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行 tbl_order 字段迁移。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
const TARGET_FIELD = {
name: 'order_pay_status',
type: 'text',
required: false,
};
function normalizeFieldPayload(field, overrides) {
const payload = Object.assign({}, field, overrides || {});
if (payload.type === 'number') {
if (Object.prototype.hasOwnProperty.call(payload, 'onlyInt')) {
payload.onlyInt = !!payload.onlyInt;
}
}
if (payload.type === 'autodate') {
payload.onCreate = typeof payload.onCreate === 'boolean' ? payload.onCreate : true;
payload.onUpdate = typeof payload.onUpdate === 'boolean' ? payload.onUpdate : false;
}
if (payload.type === 'file') {
payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 0;
payload.maxSize = typeof payload.maxSize === 'number' ? payload.maxSize : 0;
payload.mimeTypes = Array.isArray(payload.mimeTypes) ? payload.mimeTypes : null;
}
if (payload.type === 'relation') {
payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 1;
payload.cascadeDelete = typeof payload.cascadeDelete === 'boolean' ? payload.cascadeDelete : false;
}
return payload;
}
function buildCollectionPayload(collection, fields) {
return {
name: collection.name,
type: collection.type,
listRule: collection.listRule,
viewRule: collection.viewRule,
createRule: collection.createRule,
updateRule: collection.updateRule,
deleteRule: collection.deleteRule,
fields,
indexes: collection.indexes || [],
};
}
async function migrateField() {
const collection = await pb.collections.getOne('tbl_order');
const fields = Array.isArray(collection.fields) ? collection.fields : [];
const existingField = fields.find((field) => field && field.name === TARGET_FIELD.name);
const fieldReady = !!existingField
&& existingField.type === TARGET_FIELD.type
&& existingField.required === TARGET_FIELD.required;
if (fieldReady) {
return {
action: 'skipped',
field: {
name: existingField.name,
type: existingField.type,
required: !!existingField.required,
},
};
}
const nextFields = fields
.filter((field) => field && field.name !== 'id')
.map((field) => {
if (field.name === TARGET_FIELD.name) {
return normalizeFieldPayload(field, TARGET_FIELD);
}
return normalizeFieldPayload(field);
});
if (!existingField) {
nextFields.push(normalizeFieldPayload(null, TARGET_FIELD));
}
await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields));
return {
action: existingField ? 'normalized' : 'added',
};
}
async function verifyField() {
const collection = await pb.collections.getOne('tbl_order');
const field = (collection.fields || []).find((item) => item && item.name === TARGET_FIELD.name);
const result = {
name: field ? field.name : '',
type: field ? field.type : '',
required: !!(field && field.required),
ok: !!field && field.type === TARGET_FIELD.type && field.required === TARGET_FIELD.required,
};
if (!result.ok) {
throw new Error('order_pay_status 字段校验失败: ' + JSON.stringify(result, null, 2));
}
return result;
}
async function main() {
try {
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
const migrated = await migrateField();
console.log('📝 迁移结果:');
console.log(JSON.stringify(migrated, null, 2));
const verified = await verifyField();
console.log('📝 校验结果:');
console.log(JSON.stringify(verified, null, 2));
console.log('🎉 tbl_order.order_pay_status 字段已就绪。');
} catch (error) {
console.error('❌ tbl_order 字段迁移失败:');
console.error('status:', error && error.status);
console.error('message:', error && error.message);
console.error('response:', error && error.response);
console.error('originalError:', error && error.originalError);
console.error('stack:', error && error.stack);
process.exitCode = 1;
}
}
main();

View File

@@ -0,0 +1,187 @@
import { createRequire } from 'module';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import PocketBase from 'pocketbase';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let runtimeConfig = {};
try {
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
} catch (_error) {
runtimeConfig = {};
}
function readEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf8');
const result = {};
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const index = line.indexOf('=');
if (index === -1) continue;
const key = line.slice(0, index).trim();
const value = line.slice(index + 1).trim();
result[key] = value;
}
return result;
}
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
const PB_URL = String(
process.env.PB_URL
|| backendEnv.POCKETBASE_API_URL
|| runtimeConfig.POCKETBASE_API_URL
|| 'http://127.0.0.1:8090'
).replace(/\/+$/, '');
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行 tbl_order 字段迁移。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
const TARGET_FIELD = {
name: 'order_product_quantity',
type: 'number',
required: false,
};
function normalizeFieldPayload(field, overrides) {
const payload = Object.assign({}, field, overrides || {});
if (payload.type === 'number') {
if (Object.prototype.hasOwnProperty.call(payload, 'onlyInt')) {
payload.onlyInt = !!payload.onlyInt;
}
}
if (payload.type === 'autodate') {
payload.onCreate = typeof payload.onCreate === 'boolean' ? payload.onCreate : true;
payload.onUpdate = typeof payload.onUpdate === 'boolean' ? payload.onUpdate : false;
}
if (payload.type === 'file') {
payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 0;
payload.maxSize = typeof payload.maxSize === 'number' ? payload.maxSize : 0;
payload.mimeTypes = Array.isArray(payload.mimeTypes) ? payload.mimeTypes : null;
}
if (payload.type === 'relation') {
payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 1;
payload.cascadeDelete = typeof payload.cascadeDelete === 'boolean' ? payload.cascadeDelete : false;
}
return payload;
}
function buildCollectionPayload(collection, fields) {
return {
name: collection.name,
type: collection.type,
listRule: collection.listRule,
viewRule: collection.viewRule,
createRule: collection.createRule,
updateRule: collection.updateRule,
deleteRule: collection.deleteRule,
fields,
indexes: collection.indexes || [],
};
}
async function migrateField() {
const collection = await pb.collections.getOne('tbl_order');
const fields = Array.isArray(collection.fields) ? collection.fields : [];
const existingField = fields.find((field) => field && field.name === TARGET_FIELD.name);
const fieldReady = !!existingField
&& existingField.type === TARGET_FIELD.type
&& existingField.required === TARGET_FIELD.required;
if (fieldReady) {
return {
action: 'skipped',
field: {
name: existingField.name,
type: existingField.type,
required: !!existingField.required,
},
};
}
const nextFields = fields
.filter((field) => field && field.name !== 'id')
.map((field) => {
if (field.name === TARGET_FIELD.name) {
return normalizeFieldPayload(field, TARGET_FIELD);
}
return normalizeFieldPayload(field);
});
if (!existingField) {
nextFields.push(normalizeFieldPayload(null, TARGET_FIELD));
}
await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields));
return {
action: existingField ? 'normalized' : 'added',
};
}
async function verifyField() {
const collection = await pb.collections.getOne('tbl_order');
const field = (collection.fields || []).find((item) => item && item.name === TARGET_FIELD.name);
const result = {
name: field ? field.name : '',
type: field ? field.type : '',
required: !!(field && field.required),
ok: !!field && field.type === TARGET_FIELD.type && field.required === TARGET_FIELD.required,
};
if (!result.ok) {
throw new Error('order_product_quantity 字段校验失败: ' + JSON.stringify(result, null, 2));
}
return result;
}
async function main() {
try {
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
const migrated = await migrateField();
console.log('📝 迁移结果:');
console.log(JSON.stringify(migrated, null, 2));
const verified = await verifyField();
console.log('📝 校验结果:');
console.log(JSON.stringify(verified, null, 2));
console.log('🎉 tbl_order.order_product_quantity 字段已就绪。');
} catch (error) {
console.error('❌ tbl_order 字段迁移失败:');
console.error('status:', error && error.status);
console.error('message:', error && error.message);
console.error('response:', error && error.response);
console.error('originalError:', error && error.originalError);
console.error('stack:', error && error.stack);
process.exitCode = 1;
}
}
main();

View File

@@ -81,13 +81,15 @@ async function buildCollections() {
deleteRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
fields: [
{ name: 'order_id', type: 'text', required: true, autogeneratePattern: 'ORDER-[0-9]{13}-[A-Za-z0-9]{6}' },
{ name: 'order_number', type: 'text', required: true },
{ name: 'order_number', type: 'text', required: true, autogeneratePattern: '[0-9]{18}' },
{ 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_product_quantity', type: 'number', required: false },
{ name: 'order_pay_status', type: 'text', required: false },
{ name: 'order_amount', type: 'number', required: true },
{ name: 'order_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },

View File

@@ -0,0 +1,209 @@
import { createRequire } from 'module';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import PocketBase from 'pocketbase';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let runtimeConfig = {};
try {
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
} catch (_error) {
runtimeConfig = {};
}
function readEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf8');
const result = {};
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const index = line.indexOf('=');
if (index === -1) continue;
const key = line.slice(0, index).trim();
const value = line.slice(index + 1).trim();
result[key] = value;
}
return result;
}
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
const PB_URL = String(
process.env.PB_URL
|| backendEnv.POCKETBASE_API_URL
|| runtimeConfig.POCKETBASE_API_URL
|| 'http://127.0.0.1:8090'
).replace(/\/+$/, '');
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行 tbl_order 自动编号迁移。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
const ORDER_ID_PATTERN = 'ORDER-[0-9]{13}-[A-Za-z0-9]{6}';
const ORDER_NUMBER_PATTERN = '[0-9]{18}';
function normalizeFieldPayload(field, overrides) {
const payload = Object.assign({}, field, overrides || {});
if (payload.type === 'number') {
if (Object.prototype.hasOwnProperty.call(payload, 'onlyInt')) payload.onlyInt = !!payload.onlyInt;
}
if (payload.type === 'autodate') {
payload.onCreate = typeof payload.onCreate === 'boolean' ? payload.onCreate : true;
payload.onUpdate = typeof payload.onUpdate === 'boolean' ? payload.onUpdate : false;
}
if (payload.type === 'file') {
payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 0;
payload.maxSize = typeof payload.maxSize === 'number' ? payload.maxSize : 0;
payload.mimeTypes = Array.isArray(payload.mimeTypes) ? payload.mimeTypes : null;
}
if (payload.type === 'relation') {
payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 1;
payload.cascadeDelete = typeof payload.cascadeDelete === 'boolean' ? payload.cascadeDelete : false;
}
return payload;
}
function buildCollectionPayload(collection, fields) {
return {
name: collection.name,
type: collection.type,
listRule: collection.listRule,
viewRule: collection.viewRule,
createRule: collection.createRule,
updateRule: collection.updateRule,
deleteRule: collection.deleteRule,
fields,
indexes: collection.indexes || [],
};
}
async function migrateOrderFields() {
const collection = await pb.collections.getOne('tbl_order');
const fields = Array.isArray(collection.fields) ? collection.fields : [];
const orderIdField = fields.find((field) => field && field.name === 'order_id');
const orderNumberField = fields.find((field) => field && field.name === 'order_number');
if (!orderIdField || !orderNumberField) {
throw new Error('tbl_order 缺少 order_id 或 order_number 字段');
}
const orderIdReady = orderIdField.type === 'text' && String(orderIdField.autogeneratePattern || '') === ORDER_ID_PATTERN;
const orderNumberReady = orderNumberField.type === 'text'
&& String(orderNumberField.autogeneratePattern || '') === ORDER_NUMBER_PATTERN;
if (orderIdReady && orderNumberReady) {
return {
action: 'skipped',
order_id: {
type: orderIdField.type,
autogeneratePattern: String(orderIdField.autogeneratePattern || ''),
},
order_number: {
type: orderNumberField.type,
},
};
}
const nextFields = fields
.filter((field) => field && field.name !== 'id')
.map((field) => {
if (field.name === 'order_id') {
return normalizeFieldPayload(field, {
type: 'text',
required: true,
autogeneratePattern: ORDER_ID_PATTERN,
});
}
if (field.name === 'order_number') {
return normalizeFieldPayload(field, {
type: 'text',
required: true,
autogeneratePattern: ORDER_NUMBER_PATTERN,
pattern: '',
});
}
return normalizeFieldPayload(field);
});
await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields));
return {
action: 'updated',
};
}
async function verifyOrderFields() {
const collection = await pb.collections.getOne('tbl_order');
const orderIdField = (collection.fields || []).find((field) => field && field.name === 'order_id');
const orderNumberField = (collection.fields || []).find((field) => field && field.name === 'order_number');
const result = {
order_id: {
type: orderIdField ? orderIdField.type : '',
autogeneratePattern: String(orderIdField && orderIdField.autogeneratePattern || ''),
ok: !!orderIdField && orderIdField.type === 'text' && String(orderIdField.autogeneratePattern || '') === ORDER_ID_PATTERN,
},
order_number: {
type: orderNumberField ? orderNumberField.type : '',
autogeneratePattern: String(orderNumberField && orderNumberField.autogeneratePattern || ''),
ok: !!orderNumberField
&& orderNumberField.type === 'text'
&& String(orderNumberField.autogeneratePattern || '') === ORDER_NUMBER_PATTERN,
},
};
if (!result.order_id.ok || !result.order_number.ok) {
throw new Error('tbl_order 自动编号规则校验失败: ' + JSON.stringify(result, null, 2));
}
return result;
}
async function main() {
try {
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
const migrated = await migrateOrderFields();
console.log('📝 迁移结果:');
console.log(JSON.stringify(migrated, null, 2));
const verified = await verifyOrderFields();
console.log('📝 校验结果:');
console.log(JSON.stringify(verified, null, 2));
console.log('🎉 tbl_order 自动编号规则已就绪。');
} catch (error) {
console.error('❌ tbl_order 自动编号迁移失败:');
console.error('status:', error && error.status);
console.error('message:', error && error.message);
console.error('response:', error && error.response);
console.error('originalError:', error && error.originalError);
console.error('stack:', error && error.stack);
process.exitCode = 1;
}
}
main();