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();