283 lines
8.5 KiB
JavaScript
283 lines
8.5 KiB
JavaScript
|
|
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();
|