feat: 添加购物车相关迁移和索引功能

- 在 package.json 中添加迁移脚本 `migrate:cart-active-unique-index`。
- 修改 `pocketbase.cart-order.js` 文件,更新 `cart_id` 和 `cart_product_id` 字段的必填属性,并添加唯一索引 `idx_tbl_cart_owner_product_active_unique`。
- 在 `pocketbase.ensure-cart-order-autogen-id.js` 中,调整 `cart_id` 字段的必填属性为可选,并确保 `order_id` 字段为必填。
- 在 `pocketbase.product-list.js` 中,新增 `prod_list_barcode` 字段。
- 新增 `make-openapi-standalone.cjs` 脚本,用于处理 OpenAPI 文档。
- 新增 `pocketbase.cart-active-unique-index.js` 脚本,处理购物车的唯一索引和去重逻辑。
This commit is contained in:
2026-04-09 14:49:12 +08:00
parent 0bdaf54eed
commit ec6b59b4fa
29 changed files with 1240 additions and 6449 deletions

View File

@@ -14,6 +14,7 @@
"migrate:add-is-delete-field": "node pocketbase.add-is-delete-field.js",
"migrate:apply-soft-delete-rules": "node pocketbase.apply-soft-delete-rules.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",
"migrate:add-product-function-field": "node add-product-function-field.js",
"test:company-native-api": "node test-tbl-company-native-api.js",

View File

@@ -0,0 +1,145 @@
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 = String(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 COLLECTION = 'tbl_cart';
const ACTIVE_UNIQUE_INDEX = 'CREATE UNIQUE INDEX idx_tbl_cart_owner_product_active_unique ON tbl_cart (cart_owner, cart_product_id) WHERE is_delete = 0';
const TEMP_RULE = '@request.auth.id != ""';
if (!AUTH_TOKEN) {
console.error('Missing POCKETBASE_AUTH_TOKEN');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
function buildCollectionPayload(base, overrides = {}) {
const rawFields = Array.isArray(base.fields) ? base.fields : [];
const safeFields = rawFields.filter((field) => field && field.name !== 'id');
return {
name: base.name,
type: base.type,
listRule: Object.prototype.hasOwnProperty.call(overrides, 'listRule') ? overrides.listRule : base.listRule,
viewRule: Object.prototype.hasOwnProperty.call(overrides, 'viewRule') ? overrides.viewRule : base.viewRule,
createRule: Object.prototype.hasOwnProperty.call(overrides, 'createRule') ? overrides.createRule : base.createRule,
updateRule: Object.prototype.hasOwnProperty.call(overrides, 'updateRule') ? overrides.updateRule : base.updateRule,
deleteRule: Object.prototype.hasOwnProperty.call(overrides, 'deleteRule') ? overrides.deleteRule : base.deleteRule,
fields: safeFields,
indexes: Object.prototype.hasOwnProperty.call(overrides, 'indexes') ? overrides.indexes : (base.indexes || []),
};
}
async function setTempRules(collection) {
const payload = buildCollectionPayload(collection, {
listRule: TEMP_RULE,
viewRule: TEMP_RULE,
createRule: TEMP_RULE,
updateRule: TEMP_RULE,
deleteRule: TEMP_RULE,
});
await pb.collections.update(collection.id, payload);
}
async function restoreRules(collection) {
await pb.collections.update(collection.id, buildCollectionPayload(collection));
}
function groupKey(record) {
const owner = String(record.cart_owner || '').trim();
const product = String(record.cart_product_id || '').trim();
return `${owner}||${product}`;
}
async function dedupeActiveRows() {
const rows = await pb.collection(COLLECTION).getFullList({
filter: 'is_delete = 0',
sort: '-updated',
fields: 'id,cart_owner,cart_product_id,is_delete,updated',
});
const groups = new Map();
for (const row of rows) {
const key = groupKey(row);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(row);
}
let duplicateGroups = 0;
let softDeleted = 0;
for (const [, items] of groups) {
if (items.length <= 1) continue;
duplicateGroups += 1;
const keep = items[0];
for (let i = 1; i < items.length; i += 1) {
const row = items[i];
await pb.collection(COLLECTION).update(row.id, { is_delete: 1 });
softDeleted += 1;
}
console.log(`dedupe group keep=${keep.id} deleted=${items.length - 1}`);
}
return { total: rows.length, duplicateGroups, softDeleted };
}
async function applyActiveUniqueIndex() {
const collection = await pb.collections.getOne(COLLECTION);
const indexes = Array.isArray(collection.indexes) ? collection.indexes.slice() : [];
if (!indexes.includes(ACTIVE_UNIQUE_INDEX)) {
indexes.push(ACTIVE_UNIQUE_INDEX);
}
const payload = buildCollectionPayload(collection, { indexes });
await pb.collections.update(collection.id, payload);
const latest = await pb.collections.getOne(COLLECTION);
const ok = Array.isArray(latest.indexes) && latest.indexes.includes(ACTIVE_UNIQUE_INDEX);
if (!ok) {
throw new Error('Active unique index was not applied.');
}
}
async function main() {
pb.authStore.save(AUTH_TOKEN, null);
console.log(`connect ${PB_URL}`);
const original = await pb.collections.getOne(COLLECTION);
try {
await setTempRules(original);
const dedupe = await dedupeActiveRows();
console.log(JSON.stringify(dedupe, null, 2));
} finally {
const latest = await pb.collections.getOne(COLLECTION);
const restoreBase = {
...latest,
listRule: original.listRule,
viewRule: original.viewRule,
createRule: original.createRule,
updateRule: original.updateRule,
deleteRule: original.deleteRule,
};
await restoreRules(restoreBase);
}
await applyActiveUniqueIndex();
console.log('active unique index applied');
}
main().catch((error) => {
console.error('migration failed', error?.response || error?.message || error);
process.exitCode = 1;
});

View File

@@ -48,11 +48,11 @@ async function buildCollections() {
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_id', type: 'text', required: false, autogeneratePattern: 'CART-[0-9]{13}-[A-Za-z0-9]{6}' },
{ 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: 'relation', required: true, collectionId: productCollectionId, maxSelect: 1, cascadeDelete: false },
{ name: 'cart_product_id', type: 'relation', required: false, 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 },
@@ -61,6 +61,7 @@ async function buildCollections() {
],
indexes: [
'CREATE UNIQUE INDEX idx_tbl_cart_cart_id ON tbl_cart (cart_id)',
'CREATE UNIQUE INDEX idx_tbl_cart_owner_product_active_unique ON tbl_cart (cart_owner, cart_product_id) WHERE is_delete = 0',
'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)',

View File

@@ -57,11 +57,13 @@ const TARGETS = [
collectionName: 'tbl_cart',
fieldName: 'cart_id',
autogeneratePattern: 'CART-[0-9]{13}-[A-Za-z0-9]{6}',
required: false,
},
{
collectionName: 'tbl_order',
fieldName: 'order_id',
autogeneratePattern: 'ORDER-[0-9]{13}-[A-Za-z0-9]{6}',
required: true,
},
];
@@ -128,7 +130,7 @@ async function ensureAutoGenerateField(target) {
return normalizeFieldPayload(field, {
type: 'text',
required: true,
required: typeof target.required === 'boolean' ? target.required : true,
autogeneratePattern: target.autogeneratePattern,
});
});

View File

@@ -35,6 +35,7 @@ const collections = [
{ name: 'prod_list_id', type: 'text', required: true },
{ name: 'prod_list_name', type: 'text', required: true },
{ name: 'prod_list_modelnumber', type: 'text' },
{ name: 'prod_list_barcode', type: 'text' },
{ name: 'prod_list_icon', type: 'text' },
{ name: 'prod_list_description', type: 'text' },
{ name: 'prod_list_feature', type: 'text' },