diff --git a/docs/pb_tbl_product_list.md b/docs/pb_tbl_product_list.md index d453215..e542694 100644 --- a/docs/pb_tbl_product_list.md +++ b/docs/pb_tbl_product_list.md @@ -19,7 +19,7 @@ | `prod_list_icon` | `text` | 否 | 产品图标,保存 `tbl_attachments.attachments_id` | | `prod_list_description` | `text` | 否 | 产品说明(editor 内容,建议保存 Markdown 或已净化 HTML) | | `prod_list_feature` | `text` | 否 | 产品特色 | -| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 对象/数组) | +| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 数组,格式为 `[ { "name": "属性名", "value": "属性值" } ]`) | | `prod_list_plantype` | `text` | 否 | 产品方案 | | `prod_list_category` | `text` | 是 | 产品分类(必填,单选) | | `prod_list_sort` | `number` | 否 | 排序值(同分类内按升序) | @@ -49,7 +49,7 @@ - `prod_list_icon` 仅保存附件业务 ID,真实文件统一在 `tbl_attachments`。 - 当前预构建脚本中已将 `listRule` 与 `viewRule` 设置为空字符串(`""`),对应 PocketBase 的“任何人可查看”。 -- `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交对象/数组,不建议再二次字符串化。 +- `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"name":"属性名","value":"属性值"}]`。 - `prod_list_description` 作为 editor 内容字段,建议统一为 Markdown 或净化后的 HTML;若允许 HTML,需在写入前做 XSS 白名单过滤。 - 前端渲染 `prod_list_description` 时应保持和存储格式一致(Markdown 渲染器或受控 HTML 渲染),避免直接注入未净化内容。 - `prod_list_basic_price` 使用 `number`,如需货币精度策略建议后续统一为“分”或固定小数位。 diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js index 92a6d04..2b778d8 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js @@ -2,7 +2,7 @@ module.exports = { NODE_ENV: 'production', APP_VERSION: '0.1.21', APP_BASE_URL: 'https://bai-api.blv-oa.com', - POCKETBASE_API_URL: 'http://127.0.0.1:8090', + POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb', POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo', WECHAT_APPID: 'wx3bd7a7b19679da7a', WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1', diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js index 621a214..d9ba824 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js @@ -349,24 +349,51 @@ function validateDocumentDeleteBody(e) { function normalizeProductParameters(value) { if (value === null || typeof value === 'undefined' || value === '') { - return {} + return [] } - if (typeof value !== 'object' || Array.isArray(value)) { - throw createAppError(400, 'prod_list_parameters 必须为对象') + const result = [] + const indexByName = {} + + function upsert(nameValue, rawValue) { + const name = String(nameValue || '').trim() + if (!name) { + return + } + + const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue) + const existingIndex = indexByName[name] + if (typeof existingIndex === 'number') { + result[existingIndex].value = normalizedValue + return + } + + indexByName[name] = result.length + result.push({ + name: name, + value: normalizedValue, + }) + } + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i += 1) { + const item = value[i] && typeof value[i] === 'object' ? value[i] : null + if (!item) { + continue + } + upsert(item.name || item.key, item.value) + } + return result + } + + if (typeof value !== 'object') { + throw createAppError(400, 'prod_list_parameters 必须为数组或对象') } - const result = {} const keys = Object.keys(value) for (let i = 0; i < keys.length; i += 1) { - const key = String(keys[i] || '').trim() - if (!key) { - continue - } - - const current = value[keys[i]] - result[key] = current === null || typeof current === 'undefined' ? '' : String(current) + upsert(keys[i], value[keys[i]]) } return result diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js index aabac2f..1544735 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js @@ -112,28 +112,50 @@ function buildCategoryRankMap(records) { function normalizeParameters(value) { if (value === null || typeof value === 'undefined' || value === '') { - return {} + return [] } - if (typeof value !== 'object' || Array.isArray(value)) { - throw createAppError(400, 'prod_list_parameters 必须是对象') + const result = [] + const indexByName = {} + + function upsert(nameValue, rawValue) { + const name = normalizeText(nameValue) + if (!name) { + return + } + + const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue) + const existingIndex = indexByName[name] + if (typeof existingIndex === 'number') { + result[existingIndex].value = normalizedValue + return + } + + indexByName[name] = result.length + result.push({ + name: name, + value: normalizedValue, + }) + } + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i += 1) { + const item = value[i] && typeof value[i] === 'object' ? value[i] : null + if (!item) { + continue + } + upsert(item.name || item.key, item.value) + } + return result + } + + if (typeof value !== 'object') { + throw createAppError(400, 'prod_list_parameters 必须是数组或对象') } - const result = {} const keys = Object.keys(value) for (let i = 0; i < keys.length; i += 1) { - const key = normalizeText(keys[i]) - if (!key) { - continue - } - - const current = value[keys[i]] - if (current === null || typeof current === 'undefined') { - result[key] = '' - continue - } - - result[key] = String(current) + upsert(keys[i], value[keys[i]]) } return result @@ -141,7 +163,7 @@ function normalizeParameters(value) { function normalizeParametersForOutput(value) { if (value === null || typeof value === 'undefined' || value === '') { - return {} + return [] } let source = value @@ -153,9 +175,10 @@ function normalizeParametersForOutput(value) { if (raw.indexOf('map[') === 0 && raw.endsWith(']')) { const body = raw.slice(4, -1).trim() if (!body) { - return {} + return [] } - const result = {} + const result = [] + const indexByName = {} const pairs = body.split(/\s+/) for (let i = 0; i < pairs.length; i += 1) { const pair = pairs[i] @@ -168,32 +191,47 @@ function normalizeParametersForOutput(value) { continue } const val = pair.slice(separatorIndex + 1) - result[key] = val === null || typeof val === 'undefined' ? '' : String(val) + const normalizedValue = val === null || typeof val === 'undefined' ? '' : String(val) + const existingIndex = indexByName[key] + if (typeof existingIndex === 'number') { + result[existingIndex].value = normalizedValue + } else { + indexByName[key] = result.length + result.push({ name: key, value: normalizedValue }) + } } return result } - return {} + return [] } } if (Array.isArray(source)) { - const mapped = {} + const mapped = [] + const indexByName = {} for (let i = 0; i < source.length; i += 1) { const item = source[i] && typeof source[i] === 'object' ? source[i] : null if (!item) { continue } - const key = normalizeText(item.key) - if (!key) { + const name = normalizeText(item.name || item.key) + if (!name) { continue } - mapped[key] = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value) + const normalizedValue = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value) + const existingIndex = indexByName[name] + if (typeof existingIndex === 'number') { + mapped[existingIndex].value = normalizedValue + } else { + indexByName[name] = mapped.length + mapped.push({ name: name, value: normalizedValue }) + } } return mapped } if (typeof source !== 'object') { - return {} + return [] } // Some PocketBase/Goja map-like values are not directly enumerable; roundtrip to plain object. @@ -201,15 +239,23 @@ function normalizeParametersForOutput(value) { source = JSON.parse(JSON.stringify(source)) } catch (_error) {} - const result = {} + const result = [] + const indexByName = {} const keys = Object.keys(source) for (let i = 0; i < keys.length; i += 1) { - const key = normalizeText(keys[i]) - if (!key) { + const name = normalizeText(keys[i]) + if (!name) { continue } const current = source[keys[i]] - result[key] = current === null || typeof current === 'undefined' ? '' : String(current) + const normalizedValue = current === null || typeof current === 'undefined' ? '' : String(current) + const existingIndex = indexByName[name] + if (typeof existingIndex === 'number') { + result[existingIndex].value = normalizedValue + } else { + indexByName[name] = result.length + result.push({ name: name, value: normalizedValue }) + } } return result @@ -252,7 +298,7 @@ function exportProductRecord(record, extra) { const parametersFromText = parametersText ? normalizeParametersForOutput(parametersText) : {} const parametersRaw = record.get('prod_list_parameters') const parametersFromRaw = normalizeParametersForOutput(parametersRaw) - const parameters = Object.keys(parametersFromText).length ? parametersFromText : parametersFromRaw + const parameters = parametersFromText.length ? parametersFromText : parametersFromRaw return { pb_id: record.id, diff --git a/pocket-base/bai_web_pb_hooks/pages/product-manage.js b/pocket-base/bai_web_pb_hooks/pages/product-manage.js index 45eb205..d06a88b 100644 --- a/pocket-base/bai_web_pb_hooks/pages/product-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/product-manage.js @@ -778,7 +778,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { paramsBodyEl.innerHTML = state.parameterRows.map(function (row, index) { return '' - + '' + + '' + '' + '' + '' @@ -808,13 +808,13 @@ routerAdd('GET', '/manage/product-manage', function (e) { const incomingRows = [] const keys = Object.keys(parsed) for (let i = 0; i < keys.length; i += 1) { - const key = normalizeText(keys[i]) - if (!key) { + const name = normalizeText(keys[i]) + if (!name) { continue } const value = parsed[keys[i]] incomingRows.push({ - key: key, + name: name, value: value === null || typeof value === 'undefined' ? '' : String(value), }) } @@ -826,12 +826,12 @@ routerAdd('GET', '/manage/product-manage', function (e) { const existingIndexMap = {} for (let i = 0; i < state.parameterRows.length; i += 1) { - const key = normalizeText(state.parameterRows[i].key) - if (!key) { + const name = normalizeText(state.parameterRows[i].name) + if (!name) { continue } - if (!Object.prototype.hasOwnProperty.call(existingIndexMap, key)) { - existingIndexMap[key] = i + if (!Object.prototype.hasOwnProperty.call(existingIndexMap, name)) { + existingIndexMap[name] = i } } @@ -839,13 +839,13 @@ routerAdd('GET', '/manage/product-manage', function (e) { let updatedCount = 0 for (let i = 0; i < incomingRows.length; i += 1) { const row = incomingRows[i] - const idx = existingIndexMap[row.key] + const idx = existingIndexMap[row.name] if (typeof idx === 'number') { state.parameterRows[idx].value = row.value updatedCount += 1 } else { - state.parameterRows.push({ key: row.key, value: row.value }) - existingIndexMap[row.key] = state.parameterRows.length - 1 + state.parameterRows.push({ name: row.name, value: row.value }) + existingIndexMap[row.name] = state.parameterRows.length - 1 addedCount += 1 } } @@ -854,22 +854,33 @@ routerAdd('GET', '/manage/product-manage', function (e) { setStatus('参数增量导入完成:新增 ' + addedCount + ' 项,更新 ' + updatedCount + ' 项,当前共 ' + state.parameterRows.length + ' 项。', 'success') } - function collectParameterObject() { - const result = {} + function collectParameterArray() { + const result = [] + const indexByName = {} for (let i = 0; i < state.parameterRows.length; i += 1) { - const key = normalizeText(state.parameterRows[i].key) + const name = normalizeText(state.parameterRows[i].name) const value = normalizeText(state.parameterRows[i].value) - if (!key) { + if (!name) { continue } - result[key] = value + + const existingIndex = indexByName[name] + if (typeof existingIndex === 'number') { + result[existingIndex].value = value + } else { + indexByName[name] = result.length + result.push({ + name: name, + value: value, + }) + } } return result } - function normalizeParameterObject(value) { + function normalizeParameterRows(value) { if (value === null || typeof value === 'undefined' || value === '') { - return {} + return [] } let source = value @@ -877,28 +888,41 @@ routerAdd('GET', '/manage/product-manage', function (e) { try { source = JSON.parse(source) } catch (_error) { - return {} + return [] + } + } + + const rows = [] + const indexByName = {} + + function upsert(nameValue, rawValue) { + const name = normalizeText(nameValue) + if (!name) { + return + } + const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue) + const existingIndex = indexByName[name] + if (typeof existingIndex === 'number') { + rows[existingIndex].value = normalizedValue + } else { + indexByName[name] = rows.length + rows.push({ name: name, value: normalizedValue }) } } if (Array.isArray(source)) { - const mapped = {} for (let i = 0; i < source.length; i += 1) { const item = source[i] && typeof source[i] === 'object' ? source[i] : null if (!item) { continue } - const key = normalizeText(item.key) - if (!key) { - continue - } - mapped[key] = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value) + upsert(item.name || item.key, item.value) } - return mapped + return rows } if (typeof source !== 'object') { - return {} + return [] } // Some PocketBase/Goja payloads are object-like values; roundtrip makes keys enumerable. @@ -906,18 +930,12 @@ routerAdd('GET', '/manage/product-manage', function (e) { source = JSON.parse(JSON.stringify(source)) } catch (_error) {} - const result = {} const keys = Object.keys(source) for (let i = 0; i < keys.length; i += 1) { - const key = normalizeText(keys[i]) - if (!key) { - continue - } - const current = source[keys[i]] - result[key] = current === null || typeof current === 'undefined' ? '' : String(current) + upsert(keys[i], source[keys[i]]) } - return result + return rows } function updateEditorMode() { @@ -1002,11 +1020,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { state.selections.series = splitPipe(item.prod_list_series) state.selections.powerSupply = splitPipe(item.prod_list_power_supply) state.selections.tags = splitPipe(item.prod_list_tags) - const params = normalizeParameterObject(item.prod_list_parameters) - state.parameterRows = Object.keys(params).map(function (key) { - const current = params[key] - return { key: key, value: current === null || typeof current === 'undefined' ? '' : String(current) } - }) + state.parameterRows = normalizeParameterRows(item.prod_list_parameters) state.currentIconAttachment = item.prod_list_icon_attachment || null clearPendingIconPreview() state.removedIconId = '' @@ -1142,7 +1156,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { prod_list_icon: finalIconId, prod_list_description: normalizeText(fields.description.value), prod_list_feature: normalizeText(fields.feature.value), - prod_list_parameters: collectParameterObject(), + prod_list_parameters: collectParameterArray(), prod_list_plantype: joinPipe(state.selections.plantype), prod_list_category: joinPipe(state.selections.category), prod_list_comm_type: joinPipe(state.selections.commType), @@ -1281,10 +1295,10 @@ routerAdd('GET', '/manage/product-manage', function (e) { return } - if (target.matches('input[data-param-key]')) { - const index = Number(target.getAttribute('data-param-key')) + if (target.matches('input[data-param-name]')) { + const index = Number(target.getAttribute('data-param-name')) if (Number.isInteger(index) && state.parameterRows[index]) { - state.parameterRows[index].key = target.value + state.parameterRows[index].name = target.value } return } @@ -1345,7 +1359,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { }) document.getElementById('addParamBtn').addEventListener('click', function () { - state.parameterRows.push({ key: '', value: '' }) + state.parameterRows.push({ name: '', value: '' }) renderParameterRows() }) diff --git a/pocket-base/spec/openapi-wx.yaml b/pocket-base/spec/openapi-wx.yaml index 43c207a..b6ffa77 100644 --- a/pocket-base/spec/openapi-wx.yaml +++ b/pocket-base/spec/openapi-wx.yaml @@ -731,7 +731,9 @@ paths: prod_list_icon: <产品图标附件ID>| prod_list_description: <产品说明>| prod_list_feature: <产品特色>| - prod_list_parameters: <产品参数JSON>| + prod_list_parameters: + - name: <属性名>| + value: <属性值>| prod_list_plantype: <产品方案>| prod_list_category: <产品分类>| prod_list_sort: <排序值>| @@ -1628,11 +1630,20 @@ components: description: 产品特色 example: <产品特色>| prod_list_parameters: - type: object - additionalProperties: true - description: 产品参数(JSON 对象/数组) + type: array + description: 产品参数数组,每项包含 name/value + items: + type: object + properties: + name: + type: string + example: <属性名>| + value: + type: string + example: <属性值>| example: - key: <参数值>| + - name: <属性名>| + value: <属性值>| prod_list_plantype: type: string description: 产品方案 @@ -1694,7 +1705,8 @@ components: prod_list_description: <产品说明>| prod_list_feature: <产品特色>| prod_list_parameters: - key: <参数值>| + - name: <属性名>| + value: <属性值>| prod_list_plantype: <产品方案>| prod_list_category: <产品分类>| prod_list_sort: <排序值>| @@ -1756,7 +1768,8 @@ components: prod_list_description: <产品说明>| prod_list_feature: <产品特色>| prod_list_parameters: - key: <参数值>| + - name: <属性名>| + value: <属性值>| prod_list_plantype: <产品方案>| prod_list_category: <产品分类>| prod_list_sort: <排序值>| diff --git a/script/migrate-product-parameters-to-array.js b/script/migrate-product-parameters-to-array.js new file mode 100644 index 0000000..b49c875 --- /dev/null +++ b/script/migrate-product-parameters-to-array.js @@ -0,0 +1,282 @@ +import { createRequire } from 'module'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +let runtimeConfig = {}; +try { + runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js'); +} catch (_error) { + runtimeConfig = {}; +} + +function readEnvFile(filePath) { + if (!fs.existsSync(filePath)) return {}; + + const content = fs.readFileSync(filePath, 'utf8'); + const result = {}; + + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + const index = line.indexOf('='); + if (index === -1) continue; + const key = line.slice(0, index).trim(); + const value = line.slice(index + 1).trim(); + result[key] = value; + } + + return result; +} + +const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env')); + +const PB_URL = (process.env.PB_URL || backendEnv.POCKETBASE_API_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, ''); +const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || ''; +const DRY_RUN = String(process.env.DRY_RUN || '').trim() === '1'; + +if (!AUTH_TOKEN) { + console.error('❌ 缺少认证信息,请提供 POCKETBASE_AUTH_TOKEN。'); + process.exit(1); +} + +function getUrlCandidates(url) { + const trimmed = String(url || '').replace(/\/+$/, ''); + const candidates = []; + if (trimmed) { + candidates.push(trimmed); + } + + const withoutPb = trimmed.replace(/\/pb$/i, ''); + if (withoutPb && candidates.indexOf(withoutPb) === -1) { + candidates.push(withoutPb); + } + + return candidates; +} + +function normalizeText(value) { + return String(value || '').trim(); +} + +function toParameterArray(value) { + if (value === null || typeof value === 'undefined' || value === '') { + return []; + } + + let source = value; + if (typeof source === 'string') { + try { + source = JSON.parse(source); + } catch (_error) { + const raw = normalizeText(source); + if (raw.indexOf('map[') === 0 && raw.endsWith(']')) { + const mapped = {}; + const body = raw.slice(4, -1).trim(); + const pairs = body ? body.split(/\s+/) : []; + for (let i = 0; i < pairs.length; i += 1) { + const pair = pairs[i]; + const separatorIndex = pair.indexOf(':'); + if (separatorIndex <= 0) continue; + const key = normalizeText(pair.slice(0, separatorIndex)); + if (!key) continue; + mapped[key] = pair.slice(separatorIndex + 1); + } + source = mapped; + } else { + return []; + } + } + } + + const rows = []; + const indexByName = {}; + + function upsert(nameValue, rawValue) { + const name = normalizeText(nameValue); + if (!name) return; + + const valueText = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue); + const existingIndex = indexByName[name]; + if (typeof existingIndex === 'number') { + rows[existingIndex].value = valueText; + return; + } + + indexByName[name] = rows.length; + rows.push({ name, value: valueText }); + } + + if (Array.isArray(source)) { + for (let i = 0; i < source.length; i += 1) { + const item = source[i] && typeof source[i] === 'object' ? source[i] : null; + if (!item) continue; + upsert(item.name || item.key, item.value); + } + return rows; + } + + if (typeof source !== 'object') { + return []; + } + + try { + source = JSON.parse(JSON.stringify(source)); + } catch (_error) {} + + const keys = Object.keys(source || {}); + for (let i = 0; i < keys.length; i += 1) { + upsert(keys[i], source[keys[i]]); + } + + return rows; +} + +function isSameArray(left, right) { + return JSON.stringify(left || []) === JSON.stringify(right || []); +} + +async function requestPbJson(baseUrl, method, apiPath, token, body) { + const res = await fetch(baseUrl + apiPath, { + method: method, + headers: Object.assign( + { + Authorization: `Bearer ${token}`, + }, + body ? { 'Content-Type': 'application/json' } : {}, + ), + body: body ? JSON.stringify(body) : undefined, + }); + + const text = await res.text(); + let json = {}; + try { + json = text ? JSON.parse(text) : {}; + } catch (_error) { + json = { raw: text }; + } + + if (!res.ok) { + const err = new Error(json.message || `HTTP ${res.status}`); + err.status = res.status; + err.response = json; + throw err; + } + + return json; +} + +async function migrate() { + try { + const urlCandidates = getUrlCandidates(PB_URL); + let activeUrl = ''; + let lastError = null; + + for (let i = 0; i < urlCandidates.length; i += 1) { + const candidate = urlCandidates[i]; + try { + await requestPbJson( + candidate, + 'GET', + '/api/collections/tbl_product_list/records?page=1&perPage=1', + AUTH_TOKEN, + ); + activeUrl = candidate; + console.log(`✅ 已加载 POCKETBASE_AUTH_TOKEN(${candidate})。`); + break; + } catch (error) { + lastError = error; + console.log(`⚠️ 候选地址探测失败(${candidate}):`, { + status: error && error.status, + message: error && error.message, + }); + } + } + + if (!activeUrl) { + throw lastError || new Error('无法连接 PocketBase 或认证失败'); + } + + console.log(`🔄 开始迁移 tbl_product_list.prod_list_parameters,PB: ${activeUrl}`); + if (DRY_RUN) { + console.log('🧪 当前为 DRY_RUN=1,仅预览不会写入数据库。'); + } + + let page = 1; + const perPage = 200; + let total = 0; + let changed = 0; + let skipped = 0; + let failed = 0; + + while (true) { + const list = await requestPbJson( + activeUrl, + 'GET', + `/api/collections/tbl_product_list/records?page=${page}&perPage=${perPage}`, + AUTH_TOKEN, + ); + + const items = Array.isArray(list.items) ? list.items : []; + if (!items.length) { + break; + } + + for (let i = 0; i < items.length; i += 1) { + const item = items[i]; + total += 1; + + try { + const current = item.prod_list_parameters; + const normalized = toParameterArray(current); + + if (isSameArray(current, normalized)) { + skipped += 1; + continue; + } + + if (!DRY_RUN) { + await requestPbJson( + activeUrl, + 'PATCH', + `/api/collections/tbl_product_list/records/${item.id}`, + AUTH_TOKEN, + { prod_list_parameters: normalized }, + ); + } + + changed += 1; + console.log(`✅ 已迁移: ${item.prod_list_id || item.id}`); + } catch (error) { + failed += 1; + console.error(`❌ 迁移失败: ${item.prod_list_id || item.id}`, error.message || error); + } + } + + const totalPages = Number(list.totalPages || 1); + if (page >= totalPages) { + break; + } + page += 1; + } + + console.log('\n🎯 迁移完成'); + console.log(`- 总记录: ${total}`); + console.log(`- 已变更: ${changed}`); + console.log(`- 跳过不变: ${skipped}`); + console.log(`- 失败: ${failed}`); + } catch (error) { + console.error('❌ 迁移任务执行失败:', { + status: error && error.status, + message: error && error.message, + response: error && error.response, + }); + process.exitCode = 1; + } +} + +migrate(); diff --git a/script/package.json b/script/package.json index 2efa242..1cb5ffc 100644 --- a/script/package.json +++ b/script/package.json @@ -10,6 +10,7 @@ "init:product-list": "node pocketbase.product-list.js", "init:dictionary": "node pocketbase.dictionary.js", "migrate:file-fields": "node pocketbase.file-fields-to-attachments.js", + "migrate:product-params-array": "node migrate-product-parameters-to-array.js", "test:company-native-api": "node test-tbl-company-native-api.js", "test:company-owner-sync": "node test-company-owner-sync.js" },