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

@@ -0,0 +1,132 @@
const fs = require('fs');
const path = require('path');
const YAML = require('yaml');
const repoRoot = path.resolve(process.cwd(), '..');
const specRoot = path.join(repoRoot, 'pocket-base', 'spec');
const folders = ['openapi-wx', 'openapi-manage'];
const allFiles = [];
for (const folder of folders) {
const dir = path.join(specRoot, folder);
for (const name of fs.readdirSync(dir)) {
if (name.endsWith('.yaml')) allFiles.push(path.join(dir, name));
}
}
const docCache = new Map();
function loadDoc(filePath) {
if (!docCache.has(filePath)) {
const text = fs.readFileSync(filePath, 'utf8');
docCache.set(filePath, YAML.parse(text));
}
return docCache.get(filePath);
}
function deepClone(v) {
return v == null ? v : JSON.parse(JSON.stringify(v));
}
function decodePointer(seg) {
return seg.replace(/~1/g, '/').replace(/~0/g, '~');
}
function getByPointer(doc, pointer) {
if (!pointer || pointer === '#' || pointer === '') return doc;
const parts = pointer.replace(/^#/, '').split('/').filter(Boolean).map(decodePointer);
let cur = doc;
for (const p of parts) {
if (cur == null) return undefined;
cur = cur[p];
}
return cur;
}
function mergeObjects(base, extra) {
if (base && typeof base === 'object' && !Array.isArray(base) && extra && typeof extra === 'object' && !Array.isArray(extra)) {
return { ...base, ...extra };
}
return base;
}
function isExternalRef(ref) {
if (typeof ref !== 'string') return false;
if (ref.startsWith('#')) return false;
if (/^https?:\/\//i.test(ref)) return false;
const [filePart] = ref.split('#');
return !!filePart;
}
function resolveExternalRef(ref, baseFile) {
const [filePart, hashPart = ''] = ref.split('#');
const targetFile = path.resolve(path.dirname(baseFile), filePart);
const targetDoc = loadDoc(targetFile);
const pointer = hashPart ? `#${hashPart}` : '#';
const targetNode = getByPointer(targetDoc, pointer);
return { targetFile, targetNode: deepClone(targetNode) };
}
function derefExternals(node, baseFile, seen = new Set()) {
if (Array.isArray(node)) {
return node.map((item) => derefExternals(item, baseFile, seen));
}
if (!node || typeof node !== 'object') {
return node;
}
if (typeof node.$ref === 'string' && isExternalRef(node.$ref)) {
const key = `${baseFile}::${node.$ref}`;
if (seen.has(key)) {
return node;
}
seen.add(key);
const { targetFile, targetNode } = resolveExternalRef(node.$ref, baseFile);
let inlined = derefExternals(targetNode, targetFile, seen);
const siblings = { ...node };
delete siblings.$ref;
if (Object.keys(siblings).length > 0) {
inlined = mergeObjects(inlined, derefExternals(siblings, baseFile, seen));
}
return inlined;
}
const out = {};
for (const [k, v] of Object.entries(node)) {
out[k] = derefExternals(v, baseFile, seen);
}
return out;
}
function ensureTopLevel(doc, filePath) {
if (!doc.openapi) doc.openapi = '3.1.0';
if (!doc.info || typeof doc.info !== 'object') {
const base = path.basename(filePath, '.yaml');
const group = filePath.includes('openapi-wx') ? 'WX Native' : 'Manage Hooks';
doc.info = {
title: `BAI ${group} API - ${base}`,
version: '1.0.0'
};
} else {
if (!doc.info.title) doc.info.title = `BAI API - ${path.basename(filePath, '.yaml')}`;
if (!doc.info.version) doc.info.version = '1.0.0';
}
if (!Array.isArray(doc.servers)) {
doc.servers = [
{ url: 'https://bai-api.blv-oa.com', description: '生产环境' },
{ url: 'http://localhost:8090', description: 'PocketBase 本地环境' }
];
}
}
for (const filePath of allFiles) {
const original = loadDoc(filePath);
const transformed = derefExternals(deepClone(original), filePath);
ensureTopLevel(transformed, filePath);
const outText = YAML.stringify(transformed, { lineWidth: 0 });
fs.writeFileSync(filePath, outText, 'utf8');
docCache.set(filePath, transformed);
}
console.log(`Processed ${allFiles.length} yaml files under openapi-wx/openapi-manage.`);

View File

@@ -16,6 +16,7 @@
| `prod_list_id` | `text` | 是 | 产品列表业务 ID唯一标识 |
| `prod_list_name` | `text` | 是 | 产品名称 |
| `prod_list_modelnumber` | `text` | 否 | 产品型号 |
| `prod_list_barcode` | `text` | 否 | 产品料号 |
| `prod_list_icon` | `text` | 否 | 产品图标,保存 `tbl_attachments.attachments_id`,多图时以 `|` 分隔 |
| `prod_list_description` | `text` | 否 | 产品说明editor 内容,建议保存 Markdown 或已净化 HTML |
| `prod_list_feature` | `text` | 否 | 产品特色 |
@@ -50,6 +51,7 @@
## 补充约定
- `prod_list_icon` 仅保存附件业务 ID真实文件统一在 `tbl_attachments`;多图时按上传顺序使用 `|` 聚合。
- `prod_list_barcode` 为可空文本字段,用于保存产品料号;当前已接入产品管理页录入、产品列表模糊检索以及方案模板中的产品快照展示。
- `is_delete` 用于软删除控制,产品停用/删除时建议优先标记为 `1`
- 集合默认查询规则已内置 `is_delete = 0`,产品列表默认不返回已软删除记录。
- 当前预构建脚本中已将 `listRule``viewRule` 设置为空字符串(`""`),对应 PocketBase 的“任何人可查看”。
@@ -61,4 +63,4 @@
- 前端渲染 `prod_list_description` 时应保持和存储格式一致Markdown 渲染器或受控 HTML 渲染),避免直接注入未净化内容。
- `prod_list_basic_price` 使用 `number`,如需货币精度策略建议后续统一为“分”或固定小数位。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。
- 本文档为预构建结构说明,尚未执行线上建表
- 本文档已同步当前产品表建表脚本;若已执行脚本,则以 PocketBase 实际结构为准

View File

@@ -22,7 +22,7 @@
| `scheme_template_status` | `text` | 否 | 模板状态,如有效 / 主推 / 过期 |
| `scheme_template_solution_type` | `text` | 否 | 适用方案类型 |
| `scheme_template_solution_feature` | `text` | 否 | 适用方案特点 |
| `scheme_template_product_list` | `json` | 否 | 产品清单,建议格式:`[{"product_id":"PROD-xxx","qty":5,"note":"客厅使用"}]` |
| `scheme_template_product_list` | `json` | 否 | 产品清单,支持保存精简结构或完整产品快照数组 |
| `scheme_template_description` | `text` | 否 | 模板说明 |
| `scheme_template_remark` | `text` | 否 | 备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
@@ -53,7 +53,11 @@
## 补充约定
- `scheme_template_icon` 建议延续现有项目做法,仅保存 `attachments_id`,真实文件统一走 `tbl_attachments`
- `scheme_template_product_list` 建议使用 `json` 数组,单项推荐结构:`product_id``qty``note`;不要保存自由格式长字符串
- `scheme_template_product_list` 建议使用 `json` 数组。
- 兼容两种结构:
- 精简结构:`{"product_id":"PROD-xxx","qty":5,"note":"客厅使用"}`
- 完整快照结构:直接保存 `tbl_product_list` 导出的整行产品对象,至少应包含 `prod_list_id`,如存在产品型号 / 产品料号也建议一并保留(如 `prod_list_modelnumber``prod_list_barcode`)。
- 页面 `/manage/scheme` 当前优先通过产品选择器保存完整产品快照,便于后续直接展示产品摘要与排序。
- 若模板将来需要“官方模板 + 用户私有模板”并存,则需要额外引入发布状态字段或放宽公共模板读取规则;当前文档严格按 owner 私有模板设计。
- `is_delete` 用于软删除控制,模板删除时建议优先标记为 `1`
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -525,6 +525,7 @@ function validateProductMutationBody(e, isUpdate) {
prod_list_id: payload.prod_list_id || '',
prod_list_name: payload.prod_list_name || '',
prod_list_modelnumber: payload.prod_list_modelnumber || '',
prod_list_barcode: payload.prod_list_barcode || '',
prod_list_icon: normalizeAttachmentIdList(payload.prod_list_icon, 'prod_list_icon').join('|'),
prod_list_description: payload.prod_list_description || '',
prod_list_feature: payload.prod_list_feature || '',
@@ -698,7 +699,7 @@ function validateCartMutationBody(e, isUpdate) {
if (!isUpdate) {
if (!payload.cart_product_id) {
throw createAppError(400, 'cart_product_id 为必填项')
throw createAppError(400, 'cart_product_id 为必填项,且必须传 tbl_product_list 的 recordId')
}
if (typeof payload.cart_product_quantity === 'undefined') {
throw createAppError(400, 'cart_product_quantity 为必填项')

View File

@@ -227,6 +227,7 @@ function buildProductInfo(record) {
prod_list_id: '',
prod_list_name: '',
prod_list_modelnumber: '',
prod_list_barcode: '',
prod_list_basic_price: null,
}
}
@@ -235,6 +236,7 @@ function buildProductInfo(record) {
prod_list_id: record.getString('prod_list_id'),
prod_list_name: record.getString('prod_list_name'),
prod_list_modelnumber: record.getString('prod_list_modelnumber'),
prod_list_barcode: record.getString('prod_list_barcode'),
prod_list_basic_price: record.get('prod_list_basic_price'),
}
}
@@ -295,6 +297,7 @@ function exportCartRecord(record, productRecord) {
cart_remark: record.getString('cart_remark'),
product_name: productInfo.prod_list_name,
product_modelnumber: productInfo.prod_list_modelnumber,
product_barcode: productInfo.prod_list_barcode,
product_basic_price: productInfo.prod_list_basic_price,
created: String(record.created || ''),
updated: String(record.updated || ''),
@@ -340,6 +343,7 @@ function exportAdminCartRecord(record) {
cart_remark: record && typeof record.getString === 'function' ? record.getString('cart_remark') : String(record && record.cart_remark || ''),
product_name: productInfo.prod_list_name,
product_modelnumber: productInfo.prod_list_modelnumber,
product_barcode: productInfo.prod_list_barcode,
product_basic_price: productInfo.prod_list_basic_price,
created: String(record && record.created || ''),
updated: String(record && record.updated || ''),

View File

@@ -580,6 +580,7 @@ function exportProductRecord(record, extra) {
prod_list_id: record.getString('prod_list_id'),
prod_list_name: record.getString('prod_list_name'),
prod_list_modelnumber: record.getString('prod_list_modelnumber'),
prod_list_barcode: record.getString('prod_list_barcode'),
prod_list_icon: iconIds.join('|'),
prod_list_icon_ids: iconIds,
prod_list_icon_attachments: iconAttachments,
@@ -639,6 +640,7 @@ function listProducts(payload) {
|| item.prod_list_id.toLowerCase().indexOf(keyword) !== -1
|| item.prod_list_name.toLowerCase().indexOf(keyword) !== -1
|| item.prod_list_modelnumber.toLowerCase().indexOf(keyword) !== -1
|| item.prod_list_barcode.toLowerCase().indexOf(keyword) !== -1
|| item.prod_list_tags.toLowerCase().indexOf(keyword) !== -1
const matchedStatus = !status || item.prod_list_status === status
const matchedCategory = !category || item.prod_list_category === category
@@ -695,6 +697,7 @@ function createProduct(_userOpenid, payload) {
record.set('prod_list_id', targetProductId)
record.set('prod_list_name', normalizeText(payload.prod_list_name))
record.set('prod_list_modelnumber', normalizeText(payload.prod_list_modelnumber))
record.set('prod_list_barcode', normalizeText(payload.prod_list_barcode))
record.set('prod_list_icon', joinUniquePipeValues(payload.prod_list_icon))
record.set('prod_list_description', normalizeText(payload.prod_list_description))
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))
@@ -746,6 +749,7 @@ function updateProduct(_userOpenid, payload) {
record.set('prod_list_name', normalizeText(payload.prod_list_name))
record.set('prod_list_modelnumber', normalizeText(payload.prod_list_modelnumber))
record.set('prod_list_barcode', normalizeText(payload.prod_list_barcode))
record.set('prod_list_icon', joinUniquePipeValues(payload.prod_list_icon))
record.set('prod_list_description', normalizeText(payload.prod_list_description))
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))

View File

@@ -237,12 +237,29 @@ function normalizeTemplateProductList(value) {
continue
}
const productId = normalizeText(item.product_id)
const productId = normalizeText(item.prod_list_id || item.product_id)
if (!productId) {
throw createAppError(400, 'scheme_template_product_list 第 ' + (i + 1) + ' 项 product_id 为必填项')
throw createAppError(400, 'scheme_template_product_list 第 ' + (i + 1) + ' 项必须包含 product_id 或 prod_list_id')
}
ensureProductExists(productId, 'scheme_template_product_list.product_id')
const hasProductSnapshot = Object.keys(item).some(function (key) {
return key.indexOf('prod_list_') === 0 || key === 'pb_id'
})
if (hasProductSnapshot) {
let snapshot = null
try {
snapshot = JSON.parse(JSON.stringify(item))
} catch (_error) {
throw createAppError(400, 'scheme_template_product_list 第 ' + (i + 1) + ' 项必须为可序列化的产品对象')
}
snapshot.prod_list_id = productId
result.push(snapshot)
continue
}
const qty = Number(item.qty)
if (!Number.isFinite(qty) || qty <= 0) {
throw createAppError(400, 'scheme_template_product_list 第 ' + (i + 1) + ' 项 qty 必须为正数')

View File

@@ -574,12 +574,12 @@
}
return '<div class="table-wrap"><table><thead><tr>'
+ '<th>商品名称</th><th>型号</th><th>数量</th><th>单价</th><th>状态</th><th>加入时间</th><th>购物车名</th>'
+ '<th>商品名称</th><th>型号/料号</th><th>数量</th><th>单价</th><th>状态</th><th>加入时间</th><th>购物车名</th>'
+ '</tr></thead><tbody>'
+ items.map(function (item) {
return '<tr>'
+ '<td data-label="商品名称"><div>' + escapeHtml(item.product_name || item.cart_product_business_id || item.cart_product_id || '-') + '</div><div class="muted">recordId' + escapeHtml(item.cart_product_id || '-') + '</div><div class="muted">业务ID' + escapeHtml(item.cart_product_business_id || '-') + '</div></td>'
+ '<td data-label="型号">' + escapeHtml(item.product_modelnumber || '-') + '</td>'
+ '<td data-label="型号/料号"><div>' + escapeHtml(item.product_modelnumber || '-') + '</div><div class="muted">料号:' + escapeHtml(item.product_barcode || '-') + '</div></td>'
+ '<td data-label="数量">' + escapeHtml(item.cart_product_quantity || 0) + '</td>'
+ '<td data-label="单价">¥' + escapeHtml(item.cart_at_price || 0) + '</td>'
+ '<td data-label="状态">' + escapeHtml(item.cart_status || '-') + '</td>'

View File

@@ -122,7 +122,7 @@
<section class="panel">
<div class="toolbar">
<input id="keywordInput" placeholder="按名称/型号/标签搜索" />
<input id="keywordInput" placeholder="按名称/型号/料号/标签搜索" />
<select id="statusFilter"><option value="">全部状态</option></select>
<select id="categoryFilter"><option value="">全部分类</option></select>
<button class="btn btn-secondary" id="searchBtn" type="button">查询</button>
@@ -135,13 +135,19 @@
<div class="muted" id="editorMode">当前模式:新建</div>
</div>
<div class="grid" style="margin-top:14px;">
<div>
<div class="full quarter-row">
<div class="quarter-col-2">
<label for="prodListName">产品名称</label>
<input id="prodListName" placeholder="必填" />
</div>
<div>
</div>
<div class="quarter-col">
<label for="prodListBarcode">产品料号</label>
<input id="prodListBarcode" placeholder="可选" />
</div>
<div class="quarter-col">
<label for="prodListModel">产品型号</label>
<input id="prodListModel" placeholder="可选" />
</div>
</div>
<div>
<label>产品分类(必填,单选)</label>
@@ -344,6 +350,10 @@
<label for="copyModelInput">新产品型号</label>
<input id="copyModelInput" placeholder="请输入新产品型号" />
</div>
<div>
<label for="copyBarcodeInput">新产品料号</label>
<input id="copyBarcodeInput" placeholder="请输入新产品料号" />
</div>
</div>
<div class="modal-actions">
<button class="btn btn-light" id="copyCancelBtn" type="button">取消</button>
@@ -388,6 +398,7 @@
const copyModalEl = document.getElementById('copyModal')
const copyNameInputEl = document.getElementById('copyNameInput')
const copyModelInputEl = document.getElementById('copyModelInput')
const copyBarcodeInputEl = document.getElementById('copyBarcodeInput')
const loadingMaskEl = document.getElementById('loadingMask')
const loadingTextEl = document.getElementById('loadingText')
const iconPreviewListEl = document.getElementById('iconPreviewList')
@@ -401,6 +412,7 @@
keyword: document.getElementById('keywordInput'),
name: document.getElementById('prodListName'),
model: document.getElementById('prodListModel'),
barcode: document.getElementById('prodListBarcode'),
basicPrice: document.getElementById('prodListBasicPrice'),
feature: document.getElementById('prodListFeature'),
sort: document.getElementById('prodListSort'),
@@ -611,6 +623,7 @@
prod_list_id: normalizeText(item && item.prod_list_id),
prod_list_name: normalizeText(item && item.prod_list_name),
prod_list_modelnumber: normalizeText(item && item.prod_list_modelnumber),
prod_list_barcode: normalizeText(item && item.prod_list_barcode),
prod_list_icon: normalizeText(item && item.prod_list_icon),
prod_list_description: normalizeText(item && item.prod_list_description),
prod_list_feature: normalizeText(item && item.prod_list_feature),
@@ -683,6 +696,7 @@
|| normalizeText(item.prod_list_id).toLowerCase().indexOf(keyword) !== -1
|| normalizeText(item.prod_list_name).toLowerCase().indexOf(keyword) !== -1
|| normalizeText(item.prod_list_modelnumber).toLowerCase().indexOf(keyword) !== -1
|| normalizeText(item.prod_list_barcode).toLowerCase().indexOf(keyword) !== -1
|| normalizeText(item.prod_list_tags).toLowerCase().indexOf(keyword) !== -1
const matchedStatus = !status || normalizeText(item.prod_list_status) === status
const matchedCategory = !category || normalizeText(item.prod_list_category) === category
@@ -1741,6 +1755,7 @@
function resetForm() {
fields.name.value = ''
fields.model.value = ''
fields.barcode.value = ''
fields.basicPrice.value = ''
fields.feature.value = ''
fields.sort.value = '0'
@@ -1791,6 +1806,7 @@
function fillFormFromItem(item) {
fields.name.value = item.prod_list_name || ''
fields.model.value = item.prod_list_modelnumber || ''
fields.barcode.value = item.prod_list_barcode || ''
state.productStatus = item.prod_list_status || '有效'
fields.basicPrice.value = item.prod_list_basic_price === null || typeof item.prod_list_basic_price === 'undefined' ? '' : String(item.prod_list_basic_price)
fields.feature.value = item.prod_list_feature || ''
@@ -1849,7 +1865,7 @@
tableBodyEl.innerHTML = state.list.map(function (item) {
return '<tr>'
+ '<td data-label="名称/型号"><div><strong>' + escapeHtml(item.prod_list_name || '') + '</strong></div><div class="muted">' + escapeHtml(item.prod_list_modelnumber || '') + '</div></td>'
+ '<td data-label="名称/型号"><div><strong>' + escapeHtml(item.prod_list_name || '') + '</strong></div><div class="muted">型号:' + escapeHtml(item.prod_list_modelnumber || '-') + '</div><div class="muted">料号:' + escapeHtml(item.prod_list_barcode || '-') + '</div></td>'
+ '<td data-label="分类信息"><div>' + escapeHtml(item.prod_list_category || '-') + '</div><div class="muted">方案:' + escapeHtml(item.prod_list_plantype || '-') + ' / 系列:' + escapeHtml(item.prod_list_series || '-') + '</div><div class="muted">分类排序: ' + escapeHtml(item.prod_list_sort === null || typeof item.prod_list_sort === 'undefined' ? '-' : item.prod_list_sort) + '(第 ' + escapeHtml(item.prod_list_category_rank || '-') + ' 位)</div></td>'
+ '<td data-label="标签"><div>' + escapeHtml(item.prod_list_tags || '-') + '</div></td>'
+ '<td data-label="状态/价格"><div>' + escapeHtml(item.prod_list_status || '-') + '</div><div class="muted">¥' + escapeHtml(item.prod_list_basic_price === null || typeof item.prod_list_basic_price === 'undefined' ? '-' : item.prod_list_basic_price) + '</div></td>'
@@ -1863,12 +1879,13 @@
}).join('')
}
function buildCopyPayload(source, nextName, nextModel) {
function buildCopyPayload(source, nextName, nextModel, nextBarcode) {
return {
id: generatePocketBaseRecordId(),
prod_list_id: '',
prod_list_name: normalizeText(nextName),
prod_list_modelnumber: normalizeText(nextModel),
prod_list_barcode: normalizeText(nextBarcode),
prod_list_icon: source && source.prod_list_icon ? normalizeText(source.prod_list_icon) : '',
prod_list_description: source && source.prod_list_description ? normalizeText(source.prod_list_description) : '',
prod_list_feature: source && source.prod_list_feature ? normalizeText(source.prod_list_feature) : '',
@@ -1894,9 +1911,11 @@
state.copySourceProductId = source ? normalizeText(source.prod_list_id) : ''
const defaultName = normalizeText(source && source.prod_list_name)
const defaultModel = normalizeText(source && source.prod_list_modelnumber)
const defaultBarcode = normalizeText(source && source.prod_list_barcode)
copyNameInputEl.value = defaultName ? (defaultName + '-副本') : ''
copyModelInputEl.value = defaultModel
copyBarcodeInputEl.value = defaultBarcode
copyModalEl.classList.add('show')
setTimeout(function () {
@@ -1910,6 +1929,7 @@
copyModalEl.classList.remove('show')
copyNameInputEl.value = ''
copyModelInputEl.value = ''
copyBarcodeInputEl.value = ''
}
async function copyProduct(productId) {
@@ -1949,7 +1969,7 @@
return
}
const payload = buildCopyPayload(source, normalizedName, copyModelInputEl.value)
const payload = buildCopyPayload(source, normalizedName, copyModelInputEl.value, copyBarcodeInputEl.value)
setStatus('正在复制产品...', '')
showLoading('正在复制产品,请稍候...')
@@ -2126,6 +2146,7 @@
prod_list_id: state.mode === 'edit' ? state.editingId : '',
prod_list_name: normalizeText(fields.name.value),
prod_list_modelnumber: normalizeText(fields.model.value),
prod_list_barcode: normalizeText(fields.barcode.value),
prod_list_icon: joinPipe(finalIconIds),
prod_list_description: normalizeText(fields.description.value),
prod_list_feature: normalizeText(fields.feature.value),
@@ -2497,6 +2518,12 @@
}
})
copyBarcodeInputEl.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
confirmCopyProduct()
}
})
document.getElementById('submitBtn').addEventListener('click', function () {
submitProduct()
})

View File

@@ -17,10 +17,10 @@
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #edf6ff 0%, #f8fafc 100%); color: #0f172a; }
.container { max-width: 1520px; margin: 0 auto; padding: 24px 14px 42px; }
.topbar { background: rgba(255,255,255,0.96); border: 1px solid #dbe3f0; border-radius: 20px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); padding: 18px; margin-bottom: 14px; }
.panel { background: rgba(255,255,255,0.96); border: 1px solid #dbe3f0; border-radius: 20px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); padding: 18px; }
.panel + .panel { margin-top: 14px; }
.header-row, .actions, .toolbar, .form-actions, .upload-row, .summary { display: flex; flex-wrap: wrap; gap: 10px; }
.header-row { justify-content: space-between; align-items: center; }
.actions, .toolbar, .form-actions, .upload-row, .summary, .product-picker-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.toolbar { display: grid; grid-template-columns: 1.2fr 1fr 1fr 1fr auto; gap: 10px; }
.toolbar.simple { grid-template-columns: 1.5fr 1fr 1fr auto; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
@@ -60,6 +60,20 @@
.thumb { width: 120px; height: 120px; border-radius: 16px; border: 1px solid #dbe3f0; object-fit: cover; background: #f8fafc; }
.thumb-empty { display: flex; align-items: center; justify-content: center; color: #94a3b8; font-size: 13px; }
.section-title { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; }
.product-picker-box { border: 1px solid #dbe3f0; border-radius: 16px; background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%); padding: 14px; }
.product-picker-grid { display: grid; grid-template-columns: 1fr 1.2fr auto auto; gap: 10px; align-items: end; }
.product-picker-actions { justify-content: flex-end; }
.product-list-summary { margin-top: 12px; border: 1px solid #dbe3f0; border-radius: 14px; background: #fff; overflow: hidden; }
.product-list-summary:empty { display: none; }
.product-list-empty { padding: 16px; color: #64748b; font-size: 13px; text-align: center; }
.product-summary-row { display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; padding: 12px 14px; border-bottom: 1px solid #e5e7eb; }
.product-summary-row:last-child { border-bottom: none; }
.product-summary-order { width: 28px; height: 28px; border-radius: 999px; background: #eff6ff; color: #1d4ed8; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; }
.product-summary-main { min-width: 0; }
.product-summary-title { font-weight: 700; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.product-summary-meta { margin-top: 4px; color: #64748b; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.product-summary-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
.product-summary-actions .btn { padding: 8px 12px; font-size: 13px; }
.loading-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.42); z-index: 9999; }
.loading-mask.show { display: flex; }
.loading-card { min-width: min(92vw, 320px); padding: 24px 20px; border-radius: 20px; background: rgba(255,255,255,0.98); border: 1px solid #dbe3f0; text-align: center; box-shadow: 0 28px 70px rgba(15, 23, 42, 0.2); }
@@ -67,7 +81,7 @@
.muted-block { padding: 12px 14px; border-radius: 14px; background: #f8fafc; border: 1px solid #e2e8f0; color: #475569; font-size: 13px; line-height: 1.7; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (max-width: 1080px) {
.grid, .toolbar, .toolbar.simple, .thumb-box { grid-template-columns: 1fr; }
.grid, .toolbar, .toolbar.simple, .thumb-box, .product-picker-grid { grid-template-columns: 1fr; }
}
@media (max-width: 920px) {
table, thead, tbody, th, td, tr { display: block; }
@@ -75,22 +89,20 @@
tr { margin-bottom: 12px; border-bottom: 1px solid #e5e7eb; }
td { display: flex; justify-content: space-between; gap: 10px; }
td::before { content: attr(data-label); font-weight: 700; color: #475569; }
.product-summary-row { grid-template-columns: 1fr; }
.product-summary-actions { justify-content: flex-start; }
}
</style>
{{ template "theme_head" . }}
</head>
<body>
<div class="container">
<section class="panel">
<div class="header-row">
<div>
<h1>方案预设管理</h1>
<div class="tiny" style="margin-top:8px;">页面负责管理 `tbl_scheme_template` 与 `tbl_scheme`。模板图标上传会写入附件表,再把附件 ID 回填到模板记录。</div>
</div>
<div class="actions">
<a class="btn btn-light" href="/pb/manage">返回主页</a>
<button class="btn btn-light" id="refreshAllBtn" type="button">刷新全部</button>
</div>
<section class="topbar">
<h1>方案预设管理</h1>
<div class="tiny" style="margin-top:8px;">页面负责管理 `tbl_scheme_template` 与 `tbl_scheme`。模板图标上传会写入附件表,再把附件 ID 回填到模板记录。</div>
<div class="actions" style="margin-top:12px;">
<a class="btn btn-light" href="/pb/manage">返回主页</a>
<button class="btn btn-light" id="refreshAllBtn" type="button">刷新全部</button>
</div>
<div class="summary" style="margin-top:16px;">
<div class="summary-card">
@@ -113,7 +125,7 @@
<div class="section-title">
<div>
<h2>方案模板</h2>
<div class="tiny" style="margin-top:6px;">录入和管理 `tbl_scheme_template`。`scheme_template_product_list` 使用 JSON 数组编辑</div>
<div class="tiny" style="margin-top:6px;">录入和管理 `tbl_scheme_template`。产品清单支持按分类挑选产品,并自动同步为 JSON 数组。</div>
</div>
<div class="actions">
<button class="btn btn-secondary" id="newTemplateBtn" type="button">新建模板</button>
@@ -202,10 +214,39 @@
</div>
</div>
</div>
<div class="full">
<label>产品清单构建器</label>
<div class="product-picker-box">
<div class="product-picker-grid">
<div>
<label for="templateProductCategoryFilter">产品分类</label>
<select id="templateProductCategoryFilter">
<option value="">请先加载分类</option>
</select>
</div>
<div>
<label for="templateProductSelect">选择产品</label>
<select id="templateProductSelect">
<option value="">请先选择分类</option>
</select>
</div>
<div class="product-picker-actions">
<button class="btn btn-secondary" id="addTemplateProductBtn" type="button">加入产品</button>
</div>
<div class="product-picker-actions">
<button class="btn btn-light" id="reloadTemplateProductsBtn" type="button">刷新产品源</button>
</div>
</div>
<div class="hint">选择逻辑与产品管理页一致:先选分类,再选产品。加入后会把该产品整行数据写入下方 JSON 数组。</div>
<div class="product-list-summary" id="templateProductSummary">
<div class="product-list-empty">暂无已选产品,请先选择分类和产品后加入。</div>
</div>
</div>
</div>
<div class="full">
<label for="templateProductList">产品清单 JSON</label>
<textarea id="templateProductList" placeholder='示例:[{"product_id":"PROD-001","qty":2,"note":"主卧"}]'></textarea>
<div class="hint">必须是数组,每项至少包含 `product_id` 与正数 `qty`</div>
<textarea id="templateProductList" placeholder='示例:[{"prod_list_id":"PROD-001","prod_list_name":"智能面板","prod_list_barcode":"PLB-001","prod_list_category":"客控主机"}]'></textarea>
<div class="hint">默认由上方构建器自动同步。你也可以直接手动编辑 JSON失去焦点后页面会尝试重新渲染简要清单</div>
</div>
<div class="full">
<label for="templateDescription">模板说明</label>
@@ -373,6 +414,11 @@
const currentModeEl = document.getElementById('currentMode')
const templateCountEl = document.getElementById('templateCount')
const schemeCountEl = document.getElementById('schemeCount')
const templateProductSummaryEl = document.getElementById('templateProductSummary')
const productDictNameMap = {
category: '产品-产品分类',
}
const state = {
templateMode: 'idle',
@@ -382,6 +428,9 @@
templates: [],
schemes: [],
lastUploadedTemplateIcon: null,
productDictionariesByName: {},
selectableProducts: [],
templateProductItems: [],
}
const templateFields = {
@@ -394,6 +443,9 @@
solutionFeature: document.getElementById('templateSolutionFeature'),
iconId: document.getElementById('templateIconId'),
iconFile: document.getElementById('templateIconFile'),
productCategoryFilter: document.getElementById('templateProductCategoryFilter'),
productSelect: document.getElementById('templateProductSelect'),
productSummary: templateProductSummaryEl,
productList: document.getElementById('templateProductList'),
description: document.getElementById('templateDescription'),
remark: document.getElementById('templateRemark'),
@@ -583,6 +635,290 @@
}
}
function cloneJsonValue(value) {
try {
return JSON.parse(JSON.stringify(value))
} catch (_error) {
return null
}
}
function findProductDictByName(name) {
return state.productDictionariesByName[name] || null
}
function getProductDictItems(dictName) {
const dict = findProductDictByName(dictName)
if (!dict || !Array.isArray(dict.items)) {
return []
}
return dict.items
}
function fillSelectByDict(selectEl, dictName, defaultLabel) {
if (!selectEl) {
return
}
const items = getProductDictItems(dictName)
const currentValue = selectEl.value || ''
let html = '<option value="">' + escapeHtml(defaultLabel || '请选择') + '</option>'
for (let i = 0; i < items.length; i += 1) {
const value = normalizeText(items[i].enum || '')
const label = normalizeText(items[i].description || value)
html += '<option value="' + escapeHtml(value) + '">' + escapeHtml(label) + '</option>'
}
selectEl.innerHTML = html
if (currentValue) {
selectEl.value = currentValue
}
}
function getTemplateProductId(item) {
return normalizeText(item && (item.prod_list_id || item.product_id))
}
function getProductSourceById(productId) {
const targetId = normalizeText(productId)
if (!targetId) {
return null
}
for (let i = 0; i < state.selectableProducts.length; i += 1) {
if (normalizeText(state.selectableProducts[i].prod_list_id) === targetId) {
return state.selectableProducts[i]
}
}
return null
}
function buildTemplateProductSummary(item) {
const current = item && typeof item === 'object' ? item : {}
const productId = getTemplateProductId(current)
const source = getProductSourceById(productId) || current
const title = normalizeText(source.prod_list_name || source.product_name || productId || '未命名产品')
const model = normalizeText(source.prod_list_modelnumber)
const barcode = normalizeText(source.prod_list_barcode || source.product_barcode)
const category = normalizeText(source.prod_list_category)
const status = normalizeText(source.prod_list_status)
const price = source && (source.prod_list_basic_price || source.prod_list_basic_price === 0)
? ('¥' + source.prod_list_basic_price)
: ''
const qty = current && (current.qty || current.qty === 0) ? ('数量 ' + current.qty) : ''
const note = normalizeText(current && current.note)
return {
productId: productId,
title: title,
meta: [productId, model, barcode, category, status, price, qty, note].filter(Boolean).join(' / '),
}
}
function syncTemplateProductListTextarea() {
templateFields.productList.value = toJsonText(state.templateProductItems)
}
function renderTemplateProductSummary() {
if (!templateFields.productSummary) {
return
}
if (!state.templateProductItems.length) {
templateFields.productSummary.innerHTML = '<div class="product-list-empty">暂无已选产品,请先选择分类和产品后加入。</div>'
return
}
const rows = []
for (let i = 0; i < state.templateProductItems.length; i += 1) {
const summary = buildTemplateProductSummary(state.templateProductItems[i])
rows.push(
'<div class="product-summary-row">'
+ '<div class="product-summary-order">' + (i + 1) + '</div>'
+ '<div class="product-summary-main">'
+ '<div class="product-summary-title">' + escapeHtml(summary.title) + '</div>'
+ '<div class="product-summary-meta">' + escapeHtml(summary.meta || '未补充摘要信息') + '</div>'
+ '</div>'
+ '<div class="product-summary-actions">'
+ '<button class="btn btn-light" type="button" data-template-product-up="' + i + '"' + (i === 0 ? ' disabled' : '') + '>上移</button>'
+ '<button class="btn btn-light" type="button" data-template-product-down="' + i + '"' + (i === state.templateProductItems.length - 1 ? ' disabled' : '') + '>下移</button>'
+ '<button class="btn btn-danger" type="button" data-template-product-remove="' + i + '">移除</button>'
+ '</div>'
+ '</div>'
)
}
templateFields.productSummary.innerHTML = rows.join('')
}
function setTemplateProductItems(items, options) {
const safeItems = Array.isArray(items) ? items : []
const nextItems = []
for (let i = 0; i < safeItems.length; i += 1) {
const cloned = cloneJsonValue(safeItems[i])
if (cloned && typeof cloned === 'object') {
nextItems.push(cloned)
}
}
state.templateProductItems = nextItems
if (!options || !options.skipTextareaSync) {
syncTemplateProductListTextarea()
}
renderTemplateProductSummary()
}
function hydrateTemplateProductItemsFromTextarea() {
try {
const parsed = parseJsonTextarea(templateFields.productList.value, 'scheme_template_product_list')
setTemplateProductItems(parsed)
setStatus(templateStatusEl, '产品清单 JSON 已同步为简要列表。', 'success')
return parsed
} catch (error) {
setStatus(templateStatusEl, error.message || '产品清单 JSON 解析失败', 'error')
throw error
}
}
function renderTemplateProductCategoryOptions() {
fillSelectByDict(templateFields.productCategoryFilter, productDictNameMap.category, '全部分类')
}
function renderTemplateProductOptions() {
const category = normalizeText(templateFields.productCategoryFilter.value)
const options = ['<option value="">' + escapeHtml(category ? '请选择产品' : '请先选择分类') + '</option>']
const items = []
for (let i = 0; i < state.selectableProducts.length; i += 1) {
const item = state.selectableProducts[i]
if (category && normalizeText(item.prod_list_category) !== category) {
continue
}
items.push(item)
}
items.sort(function (a, b) {
const sortDiff = Number(a.prod_list_sort || 0) - Number(b.prod_list_sort || 0)
if (sortDiff !== 0) {
return sortDiff
}
return String(a.prod_list_name || '').localeCompare(String(b.prod_list_name || ''))
})
for (let i = 0; i < items.length; i += 1) {
const item = items[i]
const value = normalizeText(item.prod_list_id)
const label = [
normalizeText(item.prod_list_name),
normalizeText(item.prod_list_modelnumber),
normalizeText(item.prod_list_barcode),
normalizeText(item.prod_list_category),
].filter(Boolean).join(' / ')
options.push('<option value="' + escapeHtml(value) + '">' + escapeHtml(label || value) + '</option>')
}
templateFields.productSelect.innerHTML = options.join('')
}
async function loadTemplateProductResources(skipStatus) {
if (!skipStatus) {
showLoading('正在加载产品分类与产品库...')
}
try {
const dictionaries = await requestJson('/dictionary/list', {})
const dictItems = Array.isArray(dictionaries && dictionaries.items) ? dictionaries.items : []
state.productDictionariesByName = {}
for (let i = 0; i < dictItems.length; i += 1) {
state.productDictionariesByName[dictItems[i].dict_name] = dictItems[i]
}
const products = await requestJson('/product/list', {
keyword: '',
status: '',
prod_list_category: '',
})
state.selectableProducts = Array.isArray(products && products.items) ? products.items : []
renderTemplateProductCategoryOptions()
renderTemplateProductOptions()
renderTemplateTable()
renderTemplateProductSummary()
if (!skipStatus) {
setStatus(pageStatusEl, '模板可选产品数据已刷新。', 'success')
}
} catch (error) {
if (!skipStatus) {
setStatus(pageStatusEl, error.message || '加载模板可选产品失败', 'error')
}
throw error
} finally {
if (!skipStatus) {
hideLoading()
}
}
}
async function addSelectedTemplateProduct() {
const productId = normalizeText(templateFields.productSelect.value)
if (!productId) {
setStatus(templateStatusEl, '请先选择要加入模板的产品。', 'error')
return
}
for (let i = 0; i < state.templateProductItems.length; i += 1) {
if (getTemplateProductId(state.templateProductItems[i]) === productId) {
setStatus(templateStatusEl, '该产品已在清单中,可直接调整顺序。', 'error')
return
}
}
showLoading('正在读取产品详情...')
try {
const detail = await requestJson('/product/detail', { prod_list_id: productId })
const nextItems = state.templateProductItems.slice()
nextItems.push(detail)
setTemplateProductItems(nextItems)
setStatus(templateStatusEl, '产品已加入模板清单。', 'success')
} catch (error) {
setStatus(templateStatusEl, error.message || '加入产品失败', 'error')
} finally {
hideLoading()
}
}
function moveTemplateProductItem(index, offset) {
const currentIndex = Number(index)
const targetIndex = currentIndex + Number(offset)
if (!Number.isInteger(currentIndex) || currentIndex < 0 || currentIndex >= state.templateProductItems.length) {
return
}
if (!Number.isInteger(targetIndex) || targetIndex < 0 || targetIndex >= state.templateProductItems.length) {
return
}
const nextItems = state.templateProductItems.slice()
const currentItem = nextItems[currentIndex]
nextItems[currentIndex] = nextItems[targetIndex]
nextItems[targetIndex] = currentItem
setTemplateProductItems(nextItems)
setStatus(templateStatusEl, '产品顺序已调整。', 'success')
}
function removeTemplateProductItem(index) {
const currentIndex = Number(index)
if (!Number.isInteger(currentIndex) || currentIndex < 0 || currentIndex >= state.templateProductItems.length) {
return
}
const nextItems = state.templateProductItems.slice()
nextItems.splice(currentIndex, 1)
setTemplateProductItems(nextItems)
setStatus(templateStatusEl, '产品已从模板清单移除。', 'success')
}
function formatDateTimeInput(value) {
const text = normalizeText(value)
if (!text) {
@@ -633,6 +969,12 @@
for (let i = 0; i < state.templates.length; i += 1) {
const item = state.templates[i]
const productList = Array.isArray(item.scheme_template_product_list) ? item.scheme_template_product_list : []
const productSummary = productList.length
? productList.map(function (productItem, index) {
const summary = buildTemplateProductSummary(productItem)
return (index + 1) + '. ' + summary.title + (summary.meta ? (' / ' + summary.meta) : '')
}).join('\n')
: '-'
rows.push(
'<tr>'
+ '<td data-label="模板 ID"><div class="code">' + escapeHtml(item.scheme_template_id) + '</div></td>'
@@ -640,7 +982,7 @@
+ '<td data-label="Owner"><div class="code">' + escapeHtml(item.scheme_template_owner) + '</div></td>'
+ '<td data-label="标签 / 状态">' + escapeHtml(item.scheme_template_label || '-') + '<br /><span class="tiny">' + escapeHtml(item.scheme_template_status || '-') + '</span></td>'
+ '<td data-label="方案类型 / 特色">' + escapeHtml(item.scheme_template_solution_type || '-') + '<br /><span class="tiny">' + escapeHtml(item.scheme_template_solution_feature || '-') + '</span></td>'
+ '<td data-label="产品清单"><div class="json-preview tiny">' + escapeHtml(toJsonText(productList)) + '</div></td>'
+ '<td data-label="产品清单"><div class="json-preview tiny">' + escapeHtml(productSummary) + '</div></td>'
+ '<td data-label="操作"><div class="actions">'
+ '<button class="btn btn-light" type="button" data-template-edit="' + escapeHtml(item.scheme_template_id) + '">编辑</button>'
+ '<button class="btn btn-danger" type="button" data-template-delete="' + escapeHtml(item.scheme_template_id) + '">删除</button>'
@@ -698,11 +1040,14 @@
templateFields.solutionFeature.value = ''
templateFields.iconId.value = ''
templateFields.iconFile.value = ''
templateFields.productList.value = '[]'
templateFields.productCategoryFilter.value = ''
templateFields.productSelect.innerHTML = '<option value="">请先选择分类</option>'
templateFields.description.value = ''
templateFields.remark.value = ''
state.lastUploadedTemplateIcon = null
setTemplateProductItems([])
renderTemplateIcon('')
renderTemplateProductOptions()
}
function resetSchemeForm() {
@@ -796,8 +1141,11 @@
async function refreshAll() {
showLoading('正在刷新模板与方案数据...')
try {
await loadTemplates()
await loadSchemes()
await Promise.all([
loadTemplates(),
loadSchemes(),
loadTemplateProductResources(true),
])
setStatus(pageStatusEl, '数据刷新完成。', 'success')
} catch (error) {
setStatus(pageStatusEl, error.message || '刷新失败', 'error')
@@ -824,7 +1172,7 @@
templateFields.solutionType.value = detail.scheme_template_solution_type || ''
templateFields.solutionFeature.value = detail.scheme_template_solution_feature || ''
templateFields.iconId.value = detail.scheme_template_icon || ''
templateFields.productList.value = toJsonText(detail.scheme_template_product_list || [])
setTemplateProductItems(detail.scheme_template_product_list || [])
templateFields.description.value = detail.scheme_template_description || ''
templateFields.remark.value = detail.scheme_template_remark || ''
state.lastUploadedTemplateIcon = detail.scheme_template_icon_attachment || null
@@ -877,6 +1225,8 @@
}
function buildTemplatePayload() {
const productList = parseJsonTextarea(templateFields.productList.value, 'scheme_template_product_list')
setTemplateProductItems(productList)
return {
scheme_template_id: normalizeText(templateFields.id.value),
scheme_template_name: normalizeText(templateFields.name.value),
@@ -886,7 +1236,7 @@
scheme_template_status: normalizeText(templateFields.status.value),
scheme_template_solution_type: normalizeText(templateFields.solutionType.value),
scheme_template_solution_feature: normalizeText(templateFields.solutionFeature.value),
scheme_template_product_list: parseJsonTextarea(templateFields.productList.value, 'scheme_template_product_list'),
scheme_template_product_list: productList,
scheme_template_description: templateFields.description.value || '',
scheme_template_remark: templateFields.remark.value || '',
}
@@ -1066,12 +1416,31 @@
document.getElementById('uploadTemplateIconBtn').addEventListener('click', function () {
handleTemplateIconUpload()
})
document.getElementById('addTemplateProductBtn').addEventListener('click', function () {
addSelectedTemplateProduct()
})
document.getElementById('reloadTemplateProductsBtn').addEventListener('click', function () {
loadTemplateProductResources()
})
document.getElementById('clearTemplateIconBtn').addEventListener('click', function () {
templateFields.iconId.value = ''
templateFields.iconFile.value = ''
state.lastUploadedTemplateIcon = null
renderTemplateIcon('')
})
templateFields.productCategoryFilter.addEventListener('change', function () {
renderTemplateProductOptions()
})
templateFields.productList.addEventListener('blur', function () {
if (!normalizeText(templateFields.productList.value)) {
setTemplateProductItems([])
return
}
try {
hydrateTemplateProductItemsFromTextarea()
} catch (_error) {}
})
templateFields.iconId.addEventListener('input', function () {
if (!normalizeText(templateFields.iconId.value)) {
@@ -1101,6 +1470,24 @@
return
}
const templateProductUpIndex = target.getAttribute('data-template-product-up')
if (templateProductUpIndex !== null) {
moveTemplateProductItem(templateProductUpIndex, -1)
return
}
const templateProductDownIndex = target.getAttribute('data-template-product-down')
if (templateProductDownIndex !== null) {
moveTemplateProductItem(templateProductDownIndex, 1)
return
}
const templateProductRemoveIndex = target.getAttribute('data-template-product-remove')
if (templateProductRemoveIndex !== null) {
removeTemplateProductItem(templateProductRemoveIndex)
return
}
const schemeEditId = target.getAttribute('data-scheme-edit')
if (schemeEditId) {
editScheme(schemeEditId)

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,17 @@
openapi: 3.1.0
info:
title: BAI PocketBase Manage Hooks API - Cart
version: 1.0.0
description: |
hooks 购物车接口文档。
本文件可单独导入使用,不依赖其他 YAML。
servers:
- url: https://bai-api.blv-oa.com
description: 生产环境
- url: http://localhost:8090
description: PocketBase 本地环境
paths:
cartList:
/pb/api/cart/list:
post:
tags: [购物车]
summary: 查询购物车列表
@@ -31,7 +43,7 @@ paths:
description: 请求体必须为 application/json
'500':
description: 服务端错误
cartDetail:
/pb/api/cart/detail:
post:
tags: [购物车]
summary: 查询购物车详情
@@ -65,7 +77,7 @@ paths:
description: 请求体必须为 application/json
'500':
description: 服务端错误
cartCreate:
/pb/api/cart/create:
post:
tags: [购物车]
summary: 新增购物车记录
@@ -102,7 +114,7 @@ paths:
description: 重复请求过于频繁
'500':
description: 服务端错误
cartUpdate:
/pb/api/cart/update:
post:
tags: [购物车]
summary: 修改购物车记录
@@ -144,7 +156,7 @@ paths:
description: 重复请求过于频繁
'500':
description: 服务端错误
cartDelete:
/pb/api/cart/delete:
post:
tags: [购物车]
summary: 删除购物车记录
@@ -233,6 +245,9 @@ components:
product_modelnumber:
type: string
description: 产品型号
product_barcode:
type: string
description: 产品料号
product_basic_price:
type: [integer, number, 'null']
description: 产品基础价格
@@ -253,6 +268,7 @@ components:
is_delete: 删除标记|integer
product_name: 产品名称|string
product_modelnumber: 产品型号|string
product_barcode: 产品料号|string
product_basic_price: 产品基础价格|integer
CartListRequest:
type: object
@@ -360,6 +376,10 @@ components:
cart_status: 购物车状态|string
cart_at_price: 加入购物车时价格|integer
cart_remark: 备注|string
product_name: 产品名称|string
product_modelnumber: 产品型号|string
product_barcode: 产品料号|string
product_basic_price: 产品基础价格|integer
CartDeleteResponse:
type: object
properties:

View File

@@ -6,6 +6,8 @@ info:
面向管理端与自定义 hooks 的接口文档。
本目录仅收敛自定义 hooks API不包含 PocketBase 原生 records API。
本文件为目录索引,支持单文件独立导入,不依赖其他 YAML。
文档约定:
- 不单独配置鉴权组件;如接口需要登录,请直接在说明中关注 `Authorization: Bearer <token>`
- 示例字段值统一使用 `<字段说明>|<类型>` 风格
@@ -34,70 +36,8 @@ tags:
description: hooks 购物车接口
- name: 订单
description: hooks 订单接口
paths:
/pb/api/system/test-helloworld:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1system~1test-helloworld'
/pb/api/system/health:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1system~1health'
/pb/api/system/users-count:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1system~1users-count'
/pb/api/system/refresh-token:
$ref: '../openapi.yaml#/paths/~1pb~1api~1system~1refresh-token'
/pb/api/wechat/login:
$ref: '../openapi.yaml#/paths/~1pb~1api~1wechat~1login'
/pb/api/wechat/profile:
$ref: '../openapi.yaml#/paths/~1pb~1api~1wechat~1profile'
/pb/api/platform/register:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1platform~1register'
/pb/api/platform/login:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1platform~1login'
/pb/api/dictionary/list:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1dictionary~1list'
/pb/api/dictionary/detail:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1dictionary~1detail'
/pb/api/dictionary/create:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1dictionary~1create'
/pb/api/dictionary/update:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1dictionary~1update'
/pb/api/dictionary/delete:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1dictionary~1delete'
/pb/api/attachment/list:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1attachment~1list'
/pb/api/attachment/detail:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1attachment~1detail'
/pb/api/attachment/upload:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1attachment~1upload'
/pb/api/attachment/delete:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1attachment~1delete'
/pb/api/document/list:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1document~1list'
/pb/api/document/detail:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1document~1detail'
/pb/api/document/create:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1document~1create'
/pb/api/document/update:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1document~1update'
/pb/api/document/delete:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1document~1delete'
/pb/api/document-history/list:
$ref: '../openapi-manage.yaml#/paths/~1pb~1api~1document-history~1list'
/pb/api/cart/list:
$ref: './cart.yaml#/paths/cartList'
/pb/api/cart/detail:
$ref: './cart.yaml#/paths/cartDetail'
/pb/api/cart/create:
$ref: './cart.yaml#/paths/cartCreate'
/pb/api/cart/update:
$ref: './cart.yaml#/paths/cartUpdate'
/pb/api/cart/delete:
$ref: './cart.yaml#/paths/cartDelete'
/pb/api/order/list:
$ref: './order.yaml#/paths/orderList'
/pb/api/order/detail:
$ref: './order.yaml#/paths/orderDetail'
/pb/api/order/create:
$ref: './order.yaml#/paths/orderCreate'
/pb/api/order/update:
$ref: './order.yaml#/paths/orderUpdate'
/pb/api/order/delete:
$ref: './order.yaml#/paths/orderDelete'
paths: {}
x-index:
files:
- cart.yaml
- order.yaml

View File

@@ -1,5 +1,17 @@
openapi: 3.1.0
info:
title: BAI PocketBase Manage Hooks API - Order
version: 1.0.0
description: |
hooks 订单接口文档。
本文件可单独导入使用,不依赖其他 YAML。
servers:
- url: https://bai-api.blv-oa.com
description: 生产环境
- url: http://localhost:8090
description: PocketBase 本地环境
paths:
orderList:
/pb/api/order/list:
post:
tags: [订单]
summary: 查询订单列表
@@ -31,7 +43,7 @@ paths:
description: 请求体必须为 application/json
'500':
description: 服务端错误
orderDetail:
/pb/api/order/detail:
post:
tags: [订单]
summary: 查询订单详情
@@ -65,7 +77,7 @@ paths:
description: 请求体必须为 application/json
'500':
description: 服务端错误
orderCreate:
/pb/api/order/create:
post:
tags: [订单]
summary: 新增订单记录
@@ -104,7 +116,7 @@ paths:
description: 重复请求过于频繁
'500':
description: 服务端错误
orderUpdate:
/pb/api/order/update:
post:
tags: [订单]
summary: 修改订单记录
@@ -148,7 +160,7 @@ paths:
description: 重复请求过于频繁
'500':
description: 服务端错误
orderDelete:
/pb/api/order/delete:
post:
tags: [订单]
summary: 删除订单记录

View File

@@ -1,571 +0,0 @@
openapi: 3.1.0
info:
title: PocketBase MiniApp Company API
version: 1.0.0
summary: 小程序端通过 PocketBase JS SDK 直连 tbl_company 的基础 CRUD 文档
description: >-
本文档面向小程序端直接使用 PocketBase JS SDK / REST API 访问 `tbl_company`。
本文档统一以 PocketBase 原生记录主键 `id` 作为唯一识别键。
`company_id` 保留为普通业务字段,可用于展示、筛选和业务关联,但不再作为 CRUD 的唯一键。
当前线上 `tbl_company` 还包含 `company_owner_openid` 字段,用于保存公司所有者 openid并带普通索引。
同时新增了国家、省、市、区的名称与编码字段,便于前端直接按行政区划存取。
license:
name: Proprietary
identifier: LicenseRef-Proprietary
servers:
- url: https://bai-api.blv-oa.com/pb
description: 线上 PocketBase 服务
tags:
- name: Company
description: tbl_company 公司信息基础 CRUD
paths:
/api/collections/tbl_company/records:
get:
tags: [Company]
operationId: listCompanyRecords
summary: 查询公司列表
description: >-
使用 PocketBase 原生 records list/search 接口查询 `tbl_company`。
支持三种常见模式:
1. 全表查询:不传 `filter`
2. 精确查询:`filter=id="q1w2e3r4t5y6u7i"`
3. 模糊查询:`filter=(company_name~"华住" || company_usci~"9131" || company_entity~"张三")`
4. 按 `company_id` 查询单条:`filter=company_id="WX-COMPANY-10001"&perPage=1&page=1`。
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage'
- $ref: '#/components/parameters/Sort'
- $ref: '#/components/parameters/Filter'
- $ref: '#/components/parameters/Fields'
- $ref: '#/components/parameters/SkipTotal'
responses:
'200':
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/CompanyListResponse'
examples:
all:
summary: 全表查询
value:
page: 1
perPage: 30
totalItems: 2
totalPages: 1
items:
- id: q1w2e3r4t5y6u7i
collectionId: pbc_company_demo
collectionName: tbl_company
created: '2026-03-27 10:00:00.000Z'
updated: '2026-03-27 10:00:00.000Z'
company_id: C10001
company_name: 宝镜科技
company_type: 渠道商
company_entity: 张三
company_usci: '91310000123456789A'
company_nationality: 中国
company_nationality_code: CN
company_province: 上海
company_province_code: '310000'
company_city: 上海
company_city_code: '310100'
company_district: 浦东新区
company_district_code: '310115'
company_postalcode: '200000'
company_add: 上海市浦东新区XX路1号
company_status: 有效
company_level: A
company_owner_openid: wx-openid-owner-001
company_remark: ''
exact:
summary: 按 id 精确查询
value:
page: 1
perPage: 1
totalItems: 1
totalPages: 1
items:
- id: q1w2e3r4t5y6u7i
collectionId: pbc_company_demo
collectionName: tbl_company
created: '2026-03-27 10:00:00.000Z'
updated: '2026-03-27 10:00:00.000Z'
company_id: C10001
company_name: 宝镜科技
company_type: 渠道商
company_entity: 张三
company_usci: '91310000123456789A'
company_nationality: 中国
company_nationality_code: CN
company_province: 上海
company_province_code: '310000'
company_city: 上海
company_city_code: '310100'
company_district: 浦东新区
company_district_code: '310115'
company_postalcode: '200000'
company_add: 上海市浦东新区XX路1号
company_status: 有效
company_level: A
company_owner_openid: wx-openid-owner-001
company_remark: ''
'400':
description: 过滤表达式或查询参数不合法
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'403':
description: 当前调用方没有 list 权限
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
post:
tags: [Company]
operationId: createCompanyRecord
summary: 新增公司
description: >-
创建一条 `tbl_company` 记录。当前文档以 `id` 作为记录唯一识别键,
新建成功后由 PocketBase 自动生成 `id``company_id` 也由数据库自动生成,
客户端创建时不需要传入,但仍可作为后续业务查询字段。
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CompanyCreateRequest'
examples:
default:
value:
company_name: 宝镜科技
company_type: 渠道商
company_entity: 张三
company_usci: '91310000123456789A'
company_nationality: 中国
company_nationality_code: CN
company_province: 上海
company_province_code: '310000'
company_city: 上海
company_city_code: '310100'
company_district: 浦东新区
company_district_code: '310115'
company_postalcode: '200000'
company_add: 上海市浦东新区XX路1号
company_status: 有效
company_level: A
company_owner_openid: wx-openid-owner-001
company_remark: 首次创建
responses:
'200':
description: 创建成功
content:
application/json:
schema:
$ref: '#/components/schemas/CompanyRecord'
'400':
description: 校验失败,例如字段类型不合法或违反当前集合约束
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'403':
description: 当前调用方没有 create 权限
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'404':
description: 集合不存在
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
/api/collections/tbl_company/records/{recordId}:
get:
tags: [Company]
operationId: getCompanyRecordByRecordId
summary: 按 PocketBase 记录 id 查询公司
description: >-
这是 PocketBase 原生单条查询接口,路径参数必须传记录主键 `id`。
parameters:
- $ref: '#/components/parameters/RecordId'
- $ref: '#/components/parameters/Fields'
responses:
'200':
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/CompanyRecord'
'403':
description: 当前调用方没有 view 权限
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'404':
description: 记录不存在
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
patch:
tags: [Company]
operationId: updateCompanyRecordByRecordId
summary: 按 PocketBase 记录 id 更新公司
description: >-
这是 PocketBase 原生更新接口,路径参数统一使用记录主键 `id`。
如果业务侧只有 `company_id`,标准流程是先调用 list 接口
`filter=company_id="..."&perPage=1&page=1` 查出对应记录,再用返回的 `id` 调用本接口。
parameters:
- $ref: '#/components/parameters/RecordId'
- $ref: '#/components/parameters/Fields'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CompanyUpdateRequest'
examples:
default:
value:
company_name: 宝镜科技(更新)
company_status: 有效
company_level: S
company_owner_openid: wx-openid-owner-002
company_remark: 已更新基础资料
responses:
'200':
description: 更新成功
content:
application/json:
schema:
$ref: '#/components/schemas/CompanyRecord'
'400':
description: 更新参数不合法
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'403':
description: 当前调用方没有 update 权限
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'404':
description: 记录不存在
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
delete:
tags: [Company]
operationId: deleteCompanyRecordByRecordId
summary: 按 PocketBase 记录 id 删除公司
description: >-
这是 PocketBase 原生删除接口,路径参数统一使用记录主键 `id`。
parameters:
- $ref: '#/components/parameters/RecordId'
responses:
'204':
description: 删除成功
'400':
description: 删除失败,例如仍被必填 relation 引用
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'403':
description: 当前调用方没有 delete 权限
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
'404':
description: 记录不存在
content:
application/json:
schema:
$ref: '#/components/schemas/PocketBaseError'
components:
parameters:
Page:
name: page
in: query
description: 页码,默认 1
schema:
type: integer
minimum: 1
default: 1
PerPage:
name: perPage
in: query
description: 每页返回条数,默认 30
schema:
type: integer
minimum: 1
default: 30
Sort:
name: sort
in: query
description: 排序字段,例如 `-created,+company_name`
schema:
type: string
Filter:
name: filter
in: query
description: >-
PocketBase 过滤表达式。
精确查询示例:`id="q1w2e3r4t5y6u7i"`
模糊查询示例:`(company_name~"宝镜" || company_usci~"9131" || company_entity~"张三")`
schema:
type: string
Fields:
name: fields
in: query
description: 逗号分隔的返回字段列表,例如 `id,company_id,company_name`
schema:
type: string
SkipTotal:
name: skipTotal
in: query
description: 是否跳过 totalItems/totalPages 统计
schema:
type: boolean
default: false
RecordId:
name: recordId
in: path
required: true
description: PocketBase 记录主键 id
schema:
type: string
schemas:
CompanyBase:
type: object
properties:
company_id:
type: string
description: 公司业务编号字段,不再作为 CRUD 唯一键
company_name:
type: string
description: 公司名称
company_type:
type: string
description: 公司类型
company_entity:
type: string
description: 公司法人
company_usci:
type: string
description: 统一社会信用代码
company_nationality:
type: string
description: 国家
company_nationality_code:
type: string
description: 国家编码
company_province:
type: string
description: 省份
company_province_code:
type: string
description: 省份编码
company_city:
type: string
description: 城市
company_city_code:
type: string
description: 城市编码
company_district:
type: string
description: 区/县
company_district_code:
type: string
description: 区/县编码
company_postalcode:
type: string
description: 邮编
company_add:
type: string
description: 地址
company_status:
type: string
description: 公司状态
company_level:
type: string
description: 公司等级
company_owner_openid:
type: string
description: 公司所有者 openid
company_remark:
type: string
description: 备注
CompanyCreateRequest:
type: object
description: 创建时不需要传 `company_id`,由数据库自动生成。
properties:
company_name:
description: "公司名称"
type: string
company_type:
description: "公司类型"
type: string
company_entity:
description: "公司法人"
type: string
company_usci:
description: "统一社会信用代码"
type: string
company_nationality:
description: "国家名称"
type: string
company_nationality_code:
description: "国家编码"
type: string
company_province:
description: "省份名称"
type: string
company_province_code:
description: "省份编码"
type: string
company_city:
description: "城市名称"
type: string
company_city_code:
description: "城市编码"
type: string
company_district:
description: "区 / 县名称"
type: string
company_district_code:
description: "区 / 县编码"
type: string
company_postalcode:
description: "邮编"
type: string
company_add:
description: "地址"
type: string
company_status:
description: "公司状态"
type: string
company_level:
description: "公司等级"
type: string
company_owner_openid:
description: "公司所有者 openid"
type: string
company_remark:
description: "备注"
type: string
additionalProperties: false
CompanyUpdateRequest:
type: object
description: >-
更新时可只传需要修改的字段;记录定位统一依赖路径参数 `id`。
properties:
company_id:
description: "所属公司业务 ID"
type: string
company_name:
description: "公司名称"
type: string
company_type:
description: "公司类型"
type: string
company_entity:
description: "公司法人"
type: string
company_usci:
description: "统一社会信用代码"
type: string
company_nationality:
description: "国家名称"
type: string
company_nationality_code:
description: "国家编码"
type: string
company_province:
description: "省份名称"
type: string
company_province_code:
description: "省份编码"
type: string
company_city:
description: "城市名称"
type: string
company_city_code:
description: "城市编码"
type: string
company_district:
description: "区 / 县名称"
type: string
company_district_code:
description: "区 / 县编码"
type: string
company_postalcode:
description: "邮编"
type: string
company_add:
description: "地址"
type: string
company_status:
description: "公司状态"
type: string
company_level:
description: "公司等级"
type: string
company_owner_openid:
description: "公司所有者 openid"
type: string
company_remark:
description: "备注"
type: string
CompanyRecord:
allOf:
- type: object
properties:
id:
type: string
description: PocketBase 记录主键 id
collectionId:
type: string
collectionName:
type: string
created:
description: "记录创建时间"
type: string
updated:
description: "记录更新时间"
type: string
- $ref: '#/components/schemas/CompanyBase'
CompanyListResponse:
type: object
properties:
page:
type: integer
perPage:
type: integer
totalItems:
type: integer
totalPages:
type: integer
items:
type: array
items:
$ref: '#/components/schemas/CompanyRecord'
PocketBaseError:
type: object
properties:
status:
type: integer
message:
type: string
data:
description: "业务响应数据"
type: object
additionalProperties: true

View File

@@ -1,44 +0,0 @@
openapi: 3.1.0
info:
title: BAI PocketBase Native API
version: 1.0.0-wx
description: |
顶层兼容入口。
当前原生 API 文档已整理到 `spec/openapi-wx/` 目录下;本文件保留为兼容总入口。
servers:
- url: https://bai-api.blv-oa.com
description: 生产环境
- url: http://127.0.0.1:8090
description: PocketBase 本地环境
tags:
- name: 企业信息
description: PocketBase 原生公司记录接口
- name: 附件信息
description: PocketBase 原生附件记录接口
- name: 产品信息
description: PocketBase 原生产品记录接口
- name: 文档信息
description: PocketBase 原生文档记录接口
- name: 购物车
description: PocketBase 原生购物车记录接口
- name: 订单
description: PocketBase 原生订单记录接口
paths:
/pb/api/collections/tbl_company/records:
$ref: './openapi-wx/company.yaml#/paths/companyRecords'
/pb/api/collections/tbl_company/records/{recordId}:
$ref: './openapi-wx/company.yaml#/paths/companyRecordById'
/pb/api/collections/tbl_attachments/records:
$ref: './openapi-wx/attachments.yaml#/paths/attachmentRecords'
/pb/api/collections/tbl_product_list/records:
$ref: './openapi-wx/products.yaml#/paths/productRecords'
/pb/api/collections/tbl_document/records:
$ref: './openapi-wx/documents.yaml#/paths/documentRecords'
/pb/api/collections/tbl_cart/records:
$ref: './openapi-wx/cart.yaml#/paths/cartRecords'
/pb/api/collections/tbl_cart/records/{recordId}:
$ref: './openapi-wx/cart.yaml#/paths/cartRecordById'
/pb/api/collections/tbl_order/records:
$ref: './openapi-wx/order.yaml#/paths/orderRecords'
/pb/api/collections/tbl_order/records/{recordId}:
$ref: './openapi-wx/order.yaml#/paths/orderRecordById'

View File

@@ -1,5 +1,17 @@
openapi: 3.1.0
info:
title: BAI PocketBase Native API - Attachments
version: 1.0.0
description: |
PocketBase 原生 `tbl_attachments` records API 文档。
本文件可单独导入使用,不依赖其他 YAML。
servers:
- url: https://bai-api.blv-oa.com
description: 生产环境
- url: http://localhost:8090
description: PocketBase 本地环境
paths:
attachmentRecords:
/pb/api/collections/tbl_attachments/records:
get:
operationId: getPocketBaseAttachmentRecords
tags:
@@ -38,7 +50,7 @@ paths:
- 不传该参数时,返回分页列表
schema:
type: string
example: attachments_id="ATT-1774599142438-8n1UcU"
example: 过滤表达式|string
- name: page
in: query
required: false
@@ -60,8 +72,6 @@ paths:
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseAttachmentListResponse
example:
page: page|integer
perPage: perPage|integer
@@ -85,8 +95,6 @@ paths:
description: 查询参数错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -97,7 +105,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -108,7 +116,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -240,8 +248,6 @@ components:
example: 总页数 | integer
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseAttachmentRecord
example:
page: page|integer
perPage: perPage|integer

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,17 @@
openapi: 3.1.0
info:
title: BAI PocketBase Native API - Company
version: 1.0.0
description: |
PocketBase 原生 `tbl_company` records API 文档。
本文件可单独导入使用,不依赖其他 YAML。
servers:
- url: https://bai-api.blv-oa.com
description: 生产环境
- url: http://localhost:8090
description: PocketBase 本地环境
paths:
companyRecords:
/pb/api/collections/tbl_company/records:
post:
operationId: postPocketBaseCompanyRecord
tags:
@@ -21,7 +33,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyCreateRequest
$ref: #/components/schemas/PocketBaseCompanyCreateRequest
example:
company_name: 公司名称|string
company_type: 公司类型|string
@@ -47,7 +59,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord
$ref: #/components/schemas/PocketBaseCompanyRecord
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
@@ -78,7 +90,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -89,7 +101,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -100,7 +112,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -142,7 +154,7 @@ paths:
- 查询整个列表时:不传该参数
schema:
type: string
example: company_id="WX-COMPANY-10001"
example: 过滤表达式|string
- name: page
in: query
required: false
@@ -165,7 +177,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyListResponse
$ref: #/components/schemas/PocketBaseCompanyListResponse
example:
page: page|integer
perPage: perPage|integer
@@ -201,7 +213,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -212,7 +224,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -223,13 +235,13 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
companyRecordById:
/pb/api/collections/tbl_company/records/{recordId}:
patch:
operationId: patchPocketBaseCompanyRecordByRecordId
tags:
@@ -253,13 +265,13 @@ paths:
description: 通过 `company_id` 查询结果拿到的 PocketBase 记录主键 `id`
schema:
type: string
example: l2r3nq7rqhuob0h
example: PocketBase记录主键|string
requestBody:
required: true
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyUpdateRequest
$ref: #/components/schemas/PocketBaseCompanyUpdateRequest
example:
company_id: 所属公司业务 ID|string
company_name: 公司名称|string
@@ -286,7 +298,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord
$ref: #/components/schemas/PocketBaseCompanyRecord
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
@@ -317,7 +329,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -328,7 +340,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -339,7 +351,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -350,7 +362,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -653,8 +665,8 @@ components:
company_remark: 备注|string
PocketBaseCompanyRecord:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyFields
- $ref: #/components/schemas/PocketBaseRecordBase
- $ref: #/components/schemas/PocketBaseCompanyFields
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
@@ -870,7 +882,7 @@ components:
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord
$ref: #/components/schemas/PocketBaseCompanyRecord
example:
page: page|integer
perPage: perPage|integer

View File

@@ -1,5 +1,17 @@
openapi: 3.1.0
info:
title: BAI PocketBase Native API - Documents
version: 1.0.0
description: |
PocketBase 原生 `tbl_document` records API 文档。
本文件可单独导入使用,不依赖其他 YAML。
servers:
- url: https://bai-api.blv-oa.com
description: 生产环境
- url: http://localhost:8090
description: PocketBase 本地环境
paths:
documentRecords:
/pb/api/collections/tbl_document/records:
get:
operationId: getPocketBaseDocumentRecords
tags:
@@ -57,7 +69,7 @@ paths:
- 例如:`document_type ~ "DICT-1774599144591-hAEFQj" && document_type ~ "@UT1"`
schema:
type: string
example: document_type ~ "DICT-1774599144591-hAEFQj" && document_type ~ "@UT1"
example: 过滤表达式|string
- name: page
in: query
required: false
@@ -84,14 +96,14 @@ paths:
- `-document_create`:按最新上传倒序返回
schema:
type: string
example: -document_create
example: 排序表达式|string
responses:
"200":
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseDocumentListResponse
$ref: #/components/schemas/PocketBaseDocumentListResponse
example:
page: page|integer
perPage: perPage|integer
@@ -135,7 +147,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -146,7 +158,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -157,7 +169,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -289,15 +301,15 @@ components:
document_share_count:
type: number
description: 分享次数
example: 0
example: 分享次数|number
document_download_count:
type: number
description: 下载次数
example: 0
example: 下载次数|number
document_favorite_count:
type: number
description: 收藏次数
example: 0
example: 收藏次数|number
document_embedding_status:
type: string
description: 文档嵌入状态
@@ -360,8 +372,8 @@ components:
document_remark: 备注|string
PocketBaseDocumentRecord:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseDocumentFields
- $ref: #/components/schemas/PocketBaseRecordBase
- $ref: #/components/schemas/PocketBaseDocumentFields
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
@@ -427,7 +439,7 @@ components:
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseDocumentRecord
$ref: #/components/schemas/PocketBaseDocumentRecord
example:
page: page|integer
perPage: perPage|integer

View File

@@ -5,6 +5,8 @@ info:
description: |
本目录仅收敛 PocketBase 原生 API 文档,不包含自定义 hooks API。
本文件为目录索引,支持单文件独立导入,不依赖其他 YAML。
文档约定:
- 不单独配置鉴权组件;如接口需要登录,请直接在说明中关注 `Authorization: Bearer <token>`
- 示例字段值统一使用 `<字段说明>|<类型>` 风格
@@ -27,22 +29,12 @@ tags:
description: PocketBase 原生购物车记录接口
- name: 订单
description: PocketBase 原生订单记录接口
paths:
/pb/api/collections/tbl_company/records:
$ref: './company.yaml#/paths/companyRecords'
/pb/api/collections/tbl_company/records/{recordId}:
$ref: './company.yaml#/paths/companyRecordById'
/pb/api/collections/tbl_attachments/records:
$ref: './attachments.yaml#/paths/attachmentRecords'
/pb/api/collections/tbl_product_list/records:
$ref: './products.yaml#/paths/productRecords'
/pb/api/collections/tbl_document/records:
$ref: './documents.yaml#/paths/documentRecords'
/pb/api/collections/tbl_cart/records:
$ref: './cart.yaml#/paths/cartRecords'
/pb/api/collections/tbl_cart/records/{recordId}:
$ref: './cart.yaml#/paths/cartRecordById'
/pb/api/collections/tbl_order/records:
$ref: './order.yaml#/paths/orderRecords'
/pb/api/collections/tbl_order/records/{recordId}:
$ref: './order.yaml#/paths/orderRecordById'
paths: {}
x-index:
files:
- company.yaml
- attachments.yaml
- products.yaml
- documents.yaml
- cart.yaml
- order.yaml

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,17 @@
openapi: 3.1.0
info:
title: BAI PocketBase Native API - Products
version: 1.0.0
description: |
PocketBase 原生 `tbl_product_list` records API 文档。
本文件可单独导入使用,不依赖其他 YAML。
servers:
- url: https://bai-api.blv-oa.com
description: 生产环境
- url: http://localhost:8090
description: PocketBase 本地环境
paths:
productRecords:
/pb/api/collections/tbl_product_list/records:
get:
operationId: getPocketBaseProductListRecords
tags:
@@ -30,7 +42,7 @@ paths:
推荐写法:`prod_list_category="<产品分类>"`
schema:
type: string
example: prod_list_category="<产品分类>"
example: 过滤表达式|string
- name: page
in: query
required: false
@@ -57,14 +69,14 @@ paths:
- `prod_list_sort`:按分类排序值从小到大
schema:
type: string
example: prod_list_sort
example: 排序表达式|string
responses:
"200":
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseProductListListResponse
$ref: #/components/schemas/PocketBaseProductListListResponse
example:
page: page|integer
perPage: perPage|integer
@@ -79,6 +91,7 @@ paths:
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_barcode: 产品料号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string
@@ -103,7 +116,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -114,7 +127,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -125,7 +138,7 @@ paths:
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
$ref: #/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
@@ -202,6 +215,10 @@ components:
type: string
description: 产品型号
example: <产品型号>|<string>
prod_list_barcode:
type: string
description: 产品料号
example: <产品料号>|<string>
prod_list_icon:
type: string
description: 产品图标附件 ID保存 `tbl_attachments.attachments_id`
@@ -242,7 +259,7 @@ components:
- number
- integer
description: 排序值(同分类内按升序)
example: 10
example: 排序值|number
prod_list_comm_type:
type: string
description: 通讯类型
@@ -268,7 +285,7 @@ components:
- number
- integer
description: 基础价格
example: 1999
example: 基础价格|number
prod_list_vip_price:
type: array
description: 会员价数组,每项包含会员等级枚举值与价格
@@ -282,10 +299,10 @@ components:
type:
- number
- integer
example: 1899
example: 会员价格|number
example:
- viplevel: VIP1
price: 1899
- viplevel: 会员等级|string
price: 会员价格|number
prod_list_remark:
type: string
description: 备注
@@ -294,6 +311,7 @@ components:
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_barcode: 产品料号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string
@@ -315,8 +333,8 @@ components:
prod_list_remark: 备注|string
PocketBaseProductListRecord:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseProductListFields
- $ref: #/components/schemas/PocketBaseRecordBase
- $ref: #/components/schemas/PocketBaseProductListFields
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
@@ -326,6 +344,7 @@ components:
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_barcode: 产品料号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string
@@ -377,7 +396,7 @@ components:
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseProductListRecord
$ref: #/components/schemas/PocketBaseProductListRecord
example:
page: page|integer
perPage: perPage|integer
@@ -392,6 +411,7 @@ components:
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_barcode: 产品料号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string

File diff suppressed because it is too large Load Diff

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' },