You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
893 lines
33 KiB
893 lines
33 KiB
'use strict';
|
|
|
|
const { toInt, normalizeOrderedInput, ensureObjectPayload } = require('./helpers');
|
|
const { buildChapterTree } = require('./tree');
|
|
|
|
const deepCloneJson = value => {
|
|
if (value === null || value === undefined) return value;
|
|
return JSON.parse(JSON.stringify(value));
|
|
};
|
|
|
|
const sanitizeBlockConfig = config => {
|
|
const safeConfig = deepCloneJson(config) || {};
|
|
delete safeConfig.previewSeries;
|
|
delete safeConfig.previewPoints;
|
|
delete safeConfig.previewRows;
|
|
delete safeConfig.previewColumns;
|
|
delete safeConfig.previewTotal;
|
|
return safeConfig;
|
|
};
|
|
|
|
const REPORT_CHART_MARKER_REGEX = /(<div[^>]*data-report-chart=['"])([^'"]+)(['"][^>]*><\/div>)/gi;
|
|
const REPORT_KPI_MARKER_REGEX = /(<(?:span|a)[^>]*data-report-kpi=['"])([^'"]+)(['"][^>]*>)([\s\S]*?)(<\/(?:span|a)>)/gi;
|
|
|
|
const rewriteReportChartMarkers = (html, blockIdMap) => {
|
|
const raw = String(html || '');
|
|
if (!raw || !(blockIdMap instanceof Map) || !blockIdMap.size) return raw;
|
|
return raw.replace(REPORT_CHART_MARKER_REGEX, (match, prefix, encoded, suffix) => {
|
|
try {
|
|
const payload = JSON.parse(decodeURIComponent(encoded));
|
|
const oldBlockId = String(payload?.blockId || '').trim();
|
|
if (!oldBlockId) return match;
|
|
const nextBlockId = blockIdMap.get(oldBlockId);
|
|
if (!nextBlockId) return match;
|
|
return `${prefix}${encodeURIComponent(JSON.stringify({
|
|
...payload,
|
|
blockId: String(nextBlockId),
|
|
}))}${suffix}`;
|
|
} catch (error) {
|
|
return match;
|
|
}
|
|
});
|
|
};
|
|
|
|
const buildKpiInlineLabelFromState = (blockState) => {
|
|
const metrics = Array.isArray(blockState?.contentSnapshot?.metrics)
|
|
? blockState.contentSnapshot.metrics
|
|
: [];
|
|
const label = metrics
|
|
.map(metric => (
|
|
metric?.value === null || metric?.value === undefined ? '' : String(metric.value)
|
|
))
|
|
.filter(Boolean)
|
|
.join(' / ');
|
|
return label;
|
|
};
|
|
|
|
const extractSnapshotContentHtml = (snapshot) => {
|
|
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return '';
|
|
return String(snapshot?.content || '').trim();
|
|
};
|
|
|
|
const rewriteReportKpiMarkers = (html, options = {}) => {
|
|
const raw = String(html || '');
|
|
const blockIdMap = options?.blockIdMap;
|
|
const chapterIdMap = options?.chapterIdMap;
|
|
const getCreatedStateByInstanceId = options?.getCreatedStateByInstanceId;
|
|
const preserveExistingLabelOnEmpty = Boolean(options?.preserveExistingLabelOnEmpty);
|
|
const fallbackBlockIds = Array.isArray(options?.fallbackKpiBlockIds)
|
|
? options.fallbackKpiBlockIds.map(item => String(item || '').trim()).filter(Boolean)
|
|
: [];
|
|
if (!raw || !(blockIdMap instanceof Map) || !blockIdMap.size) return raw;
|
|
let fallbackIndex = 0;
|
|
return raw.replace(
|
|
REPORT_KPI_MARKER_REGEX,
|
|
(match, prefix, encoded, suffix, innerText, closingTag) => {
|
|
try {
|
|
const payload = JSON.parse(decodeURIComponent(encoded));
|
|
const oldBlockId = String(payload?.blockId || '').trim();
|
|
if (!oldBlockId) return match;
|
|
const fallbackBlockId = fallbackBlockIds[fallbackIndex] || null;
|
|
fallbackIndex += 1;
|
|
const nextBlockId = blockIdMap.get(oldBlockId) || fallbackBlockId;
|
|
if (!nextBlockId) return match;
|
|
const oldChapterId = String(payload?.chapterId || '').trim();
|
|
const nextChapterId = oldChapterId
|
|
? String(chapterIdMap?.get(Number(oldChapterId)) || chapterIdMap?.get(oldChapterId) || oldChapterId).trim()
|
|
: '';
|
|
const createdBlockState = typeof getCreatedStateByInstanceId === 'function'
|
|
? getCreatedStateByInstanceId(nextBlockId)
|
|
: null;
|
|
const nextLabel = buildKpiInlineLabelFromState(createdBlockState)
|
|
|| (
|
|
preserveExistingLabelOnEmpty
|
|
? String(payload?.label || innerText || '').trim()
|
|
: ''
|
|
);
|
|
if (!nextLabel) return '';
|
|
const nextPayload = {
|
|
...payload,
|
|
blockId: String(nextBlockId),
|
|
...(nextChapterId ? { chapterId: nextChapterId } : {}),
|
|
label: nextLabel,
|
|
};
|
|
const nextSuffix = String(suffix || '')
|
|
.replace(/data-report-block-id=['"][^'"]*['"]/i, `data-report-block-id="${String(nextBlockId)}"`)
|
|
.replace(/data-report-chapter-id=['"][^'"]*['"]/i, nextChapterId ? `data-report-chapter-id="${nextChapterId}"` : 'data-report-chapter-id=""')
|
|
.replace(/data-report-kpi-text=['"][^'"]*['"]/i, `data-report-kpi-text="${nextLabel}"`);
|
|
return `${prefix}${encodeURIComponent(JSON.stringify(nextPayload))}${nextSuffix}${nextLabel}${closingTag}`;
|
|
} catch (error) {
|
|
return match;
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
const rewriteBlockContentSnapshot = (snapshot, options = {}) => {
|
|
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
return snapshot || null;
|
|
}
|
|
const rawContent = snapshot.content;
|
|
if (
|
|
typeof rawContent !== 'string'
|
|
|| (!rawContent.includes('data-report-chart=') && !rawContent.includes('data-report-kpi='))
|
|
) {
|
|
return snapshot;
|
|
}
|
|
const nextContent = rewriteReportKpiMarkers(
|
|
rewriteReportChartMarkers(rawContent, options?.blockIdMap),
|
|
options,
|
|
);
|
|
if (nextContent === rawContent) return snapshot;
|
|
return {
|
|
...deepCloneJson(snapshot),
|
|
content: nextContent,
|
|
};
|
|
};
|
|
|
|
const rewriteAiAnalysisConfigForTemplate = (config, blockIdMap) => {
|
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
return config || {};
|
|
}
|
|
const nextConfig = deepCloneJson(config) || {};
|
|
if (!(blockIdMap instanceof Map) || !blockIdMap.size) return nextConfig;
|
|
const sourceBlockId = String(
|
|
nextConfig.sourceTemplateBlockId || nextConfig.sourceBlockId || nextConfig.linkedBlockId || ''
|
|
).trim();
|
|
if (!sourceBlockId) return nextConfig;
|
|
const rewrittenId = blockIdMap.get(sourceBlockId);
|
|
if (!rewrittenId) return nextConfig;
|
|
nextConfig.sourceTemplateBlockId = rewrittenId;
|
|
nextConfig.sourceBlockId = rewrittenId;
|
|
nextConfig.linkedBlockId = rewrittenId;
|
|
return nextConfig;
|
|
};
|
|
|
|
const rewriteAnchorKeyWithChapterMap = (rawValue, chapterIdMap) => {
|
|
const normalized = String(rawValue || '').trim();
|
|
if (!normalized || !(chapterIdMap instanceof Map) || !chapterIdMap.size) return normalized;
|
|
const colonIndex = normalized.indexOf(':');
|
|
if (colonIndex <= 0) return normalized;
|
|
const oldChapterId = normalized.slice(0, colonIndex);
|
|
const nextChapterId = chapterIdMap.get(Number(oldChapterId)) || chapterIdMap.get(oldChapterId);
|
|
if (!nextChapterId) return normalized;
|
|
return `${nextChapterId}${normalized.slice(colonIndex)}`;
|
|
};
|
|
|
|
const rewriteInlineAnchorConfig = (config, chapterIdMap) => {
|
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
return config || {};
|
|
}
|
|
if (!(chapterIdMap instanceof Map) || !chapterIdMap.size) {
|
|
return deepCloneJson(config) || {};
|
|
}
|
|
const nextConfig = deepCloneJson(config) || {};
|
|
const inlineAnchor = ensureObjectPayload(nextConfig.inlineAnchor || {}, 'config.inlineAnchor');
|
|
if (!Object.keys(inlineAnchor).length) return nextConfig;
|
|
const nextParaId = rewriteAnchorKeyWithChapterMap(inlineAnchor.paraId, chapterIdMap);
|
|
const nextSecKey = rewriteAnchorKeyWithChapterMap(inlineAnchor.secKey, chapterIdMap);
|
|
nextConfig.inlineAnchor = {
|
|
...inlineAnchor,
|
|
...(nextParaId ? { paraId: nextParaId } : {}),
|
|
...(nextSecKey ? { secKey: nextSecKey } : {}),
|
|
};
|
|
return nextConfig;
|
|
};
|
|
|
|
const getLatestSchemaVersion = async (ctx, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const latest = await models.ReportSchemaVersion.findOne({
|
|
order: [['id', 'DESC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!latest) throw '请先创建 schema version';
|
|
return latest;
|
|
};
|
|
|
|
const resolveSchemaVersion = async (ctx, schemaVersionId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
if (schemaVersionId === null || schemaVersionId === undefined || schemaVersionId === '') {
|
|
return getLatestSchemaVersion(ctx, transaction);
|
|
}
|
|
const parsedId = toInt(schemaVersionId);
|
|
if (!parsedId) throw '参数错误: schemaVersionId';
|
|
const row = await models.ReportSchemaVersion.findByPk(parsedId, { raw: true, transaction });
|
|
if (!row) throw 'schema version 不存在';
|
|
return row;
|
|
};
|
|
|
|
const ensureDataSource = async (ctx, dataSourceId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedId = toInt(dataSourceId);
|
|
if (!parsedId) throw '缺少参数: dataSourceId';
|
|
const row = await models.ReportDataSource.findByPk(parsedId, { raw: true, transaction });
|
|
if (!row) throw 'data source 不存在';
|
|
return row;
|
|
};
|
|
|
|
const ensureInstance = async (ctx, instanceId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedId = toInt(instanceId);
|
|
if (!parsedId) throw '缺少参数: instanceId';
|
|
const row = await models.ReportInstance.findByPk(parsedId, { raw: true, transaction });
|
|
if (!row) throw '报表实例不存在';
|
|
return row;
|
|
};
|
|
|
|
const ensureTemplate = async (ctx, templateId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedId = toInt(templateId);
|
|
if (!parsedId) throw '缺少参数: templateId';
|
|
const row = await models.ReportTemplate.findByPk(parsedId, { raw: true, transaction });
|
|
if (!row) throw '模板不存在';
|
|
return row;
|
|
};
|
|
|
|
const ensureActiveTemplate = async (ctx, templateId, transaction = null) => {
|
|
const template = await ensureTemplate(ctx, templateId, transaction);
|
|
if (!template.isActive) throw '模板未启用';
|
|
return template;
|
|
};
|
|
|
|
const ensureTemplateBlock = async (ctx, templateId, templateBlockId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedTemplateId = toInt(templateId);
|
|
const parsedTemplateBlockId = toInt(templateBlockId);
|
|
if (!parsedTemplateId || !parsedTemplateBlockId) throw '参数错误';
|
|
const row = await models.ReportTemplateBlock.findOne({
|
|
where: {
|
|
id: parsedTemplateBlockId,
|
|
templateId: parsedTemplateId,
|
|
},
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!row) throw '模板块不存在';
|
|
return row;
|
|
};
|
|
|
|
const ensureTemplateChapter = async (ctx, templateId, templateChapterId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedTemplateId = toInt(templateId);
|
|
const parsedTemplateChapterId = toInt(templateChapterId);
|
|
if (!parsedTemplateId || !parsedTemplateChapterId) throw '参数错误';
|
|
const row = await models.ReportTemplateChapter.findOne({
|
|
where: {
|
|
id: parsedTemplateChapterId,
|
|
templateId: parsedTemplateId,
|
|
},
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!row) throw '模板章节不存在';
|
|
return row;
|
|
};
|
|
|
|
const getTemplateBatchDimension = async (ctx, templateId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedTemplateId = toInt(templateId);
|
|
if (!parsedTemplateId) throw '缺少参数: templateId';
|
|
return models.ReportTemplateBatchDimension.findOne({
|
|
where: { templateId: parsedTemplateId },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
};
|
|
|
|
const ensureTemplateBatchDimension = async (ctx, templateId, transaction = null) => {
|
|
const row = await getTemplateBatchDimension(ctx, templateId, transaction);
|
|
if (!row) throw '模板未配置主批量维度';
|
|
return row;
|
|
};
|
|
|
|
const getTemplateBlockBatchBinding = async (ctx, templateId, templateBlockId, transaction = null) => {
|
|
const block = await ensureTemplateBlock(ctx, templateId, templateBlockId, transaction);
|
|
const { models } = ctx.app.fs.dc;
|
|
const row = await models.ReportTemplateBlockBatchBinding.findOne({
|
|
where: { templateBlockId: block.id },
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!row) return null;
|
|
return row;
|
|
};
|
|
|
|
const getTemplateBlockBatchBindingMap = async (ctx, templateId, transaction = null) => {
|
|
const parsedTemplateId = toInt(templateId);
|
|
if (!parsedTemplateId) throw '缺少参数: templateId';
|
|
const { models } = ctx.app.fs.dc;
|
|
const rows = await models.ReportTemplateBlockBatchBinding.findAll({
|
|
include: [{
|
|
model: models.ReportTemplateBlock,
|
|
required: true,
|
|
attributes: [],
|
|
where: { templateId: parsedTemplateId },
|
|
}],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const result = new Map();
|
|
rows.forEach(row => {
|
|
result.set(Number(row.templateBlockId), row);
|
|
});
|
|
return result;
|
|
};
|
|
|
|
const ensureChapter = async (ctx, instanceId, chapterId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedChapterId = toInt(chapterId);
|
|
const parsedInstanceId = toInt(instanceId);
|
|
if (!parsedChapterId || !parsedInstanceId) throw '参数错误';
|
|
const row = await models.ReportChapter.findOne({
|
|
where: {
|
|
id: parsedChapterId,
|
|
instanceId: parsedInstanceId,
|
|
},
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!row) throw '章节不存在';
|
|
return row;
|
|
};
|
|
|
|
const ensureBlock = async (ctx, instanceId, blockId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const parsedBlockId = toInt(blockId);
|
|
const parsedInstanceId = toInt(instanceId);
|
|
if (!parsedBlockId || !parsedInstanceId) throw '参数错误';
|
|
const row = await models.ReportBlock.findOne({
|
|
where: {
|
|
id: parsedBlockId,
|
|
instanceId: parsedInstanceId,
|
|
},
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
if (!row) throw '内容块不存在';
|
|
return row;
|
|
};
|
|
|
|
const getInstanceChapterTree = async (ctx, instanceId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const chapters = await models.ReportChapter.findAll({
|
|
where: { instanceId: toInt(instanceId) },
|
|
order: [['position', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const blocks = await models.ReportBlock.findAll({
|
|
where: { instanceId: toInt(instanceId) },
|
|
order: [['chapterId', 'ASC'], ['position', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
return buildChapterTree(chapters, blocks);
|
|
};
|
|
|
|
const getTemplateChapterTree = async (ctx, templateId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const chapters = await models.ReportTemplateChapter.findAll({
|
|
where: { templateId: toInt(templateId) },
|
|
order: [['position', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const blocks = await models.ReportTemplateBlock.findAll({
|
|
where: { templateId: toInt(templateId) },
|
|
order: [['chapterId', 'ASC'], ['position', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
}).then(rows => rows.map(row => ({
|
|
...row,
|
|
config: row.defaultConfig,
|
|
contentSnapshot: row.contentSnapshot || null,
|
|
})));
|
|
return buildChapterTree(chapters, blocks);
|
|
};
|
|
|
|
const createBlocksFromPayload = async (ctx, instanceId, chapterId, blocks = [], transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const orderedBlocks = normalizeOrderedInput(blocks);
|
|
for (let i = 0; i < orderedBlocks.length; i += 1) {
|
|
const block = orderedBlocks[i] || {};
|
|
await models.ReportBlock.create({
|
|
instanceId: toInt(instanceId),
|
|
chapterId: toInt(chapterId),
|
|
blockType: String(block.blockType || 'text'),
|
|
position: i + 1,
|
|
config: sanitizeBlockConfig(block.config || {}),
|
|
contentSnapshot: block.contentSnapshot || null,
|
|
updatedAt: new Date(),
|
|
}, { transaction });
|
|
}
|
|
};
|
|
|
|
const createChapterTreeFromPayload = async (ctx, instanceId, parentId, chapters = [], transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const orderedChapters = normalizeOrderedInput(chapters);
|
|
for (let i = 0; i < orderedChapters.length; i += 1) {
|
|
const chapter = orderedChapters[i] || {};
|
|
const title = String(chapter.title || '').trim();
|
|
if (!title) throw '章节标题不能为空';
|
|
const created = await models.ReportChapter.create({
|
|
instanceId: toInt(instanceId),
|
|
parentId: parentId === null ? null : toInt(parentId),
|
|
title,
|
|
position: i + 1,
|
|
prompts: chapter.prompts || null,
|
|
status: chapter.status || null,
|
|
}, {
|
|
transaction,
|
|
returning: true,
|
|
});
|
|
if (Array.isArray(chapter.blocks) && chapter.blocks.length) {
|
|
await createBlocksFromPayload(ctx, instanceId, created.id, chapter.blocks, transaction);
|
|
}
|
|
if (Array.isArray(chapter.children) && chapter.children.length) {
|
|
await createChapterTreeFromPayload(ctx, instanceId, created.id, chapter.children, transaction);
|
|
}
|
|
}
|
|
};
|
|
|
|
const cloneTemplateToInstance = async (
|
|
ctx,
|
|
templateId,
|
|
instanceId,
|
|
blockConfigOverrides = new Map(),
|
|
transaction = null,
|
|
options = {}
|
|
) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const templateChapters = await models.ReportTemplateChapter.findAll({
|
|
where: { templateId: toInt(templateId) },
|
|
order: [['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const chapterIdMap = new Map();
|
|
const pending = [...templateChapters];
|
|
while (pending.length) {
|
|
let progressed = false;
|
|
for (let i = pending.length - 1; i >= 0; i -= 1) {
|
|
const chapter = pending[i];
|
|
if (chapter.parentId && !chapterIdMap.has(chapter.parentId)) continue;
|
|
const created = await models.ReportChapter.create({
|
|
instanceId: toInt(instanceId),
|
|
parentId: chapter.parentId ? chapterIdMap.get(chapter.parentId) : null,
|
|
title: chapter.title,
|
|
position: chapter.position,
|
|
prompts: null,
|
|
status: null,
|
|
}, {
|
|
transaction,
|
|
returning: true,
|
|
});
|
|
chapterIdMap.set(chapter.id, created.id);
|
|
pending.splice(i, 1);
|
|
progressed = true;
|
|
}
|
|
if (!progressed) {
|
|
throw '模板章节层级异常,无法复制';
|
|
}
|
|
}
|
|
|
|
const templateBlocks = await models.ReportTemplateBlock.findAll({
|
|
where: { templateId: toInt(templateId) },
|
|
order: [['chapterId', 'ASC'], ['position', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const templateTextCandidateByChapterId = new Map();
|
|
const templateKpiBlockIdsByChapterId = new Map();
|
|
templateBlocks.forEach(block => {
|
|
const chapterKey = String(block.chapterId || '').trim();
|
|
const blockType = String(block?.blockType || '').trim().toLowerCase();
|
|
const contentHtml = extractSnapshotContentHtml(block?.contentSnapshot);
|
|
if (chapterKey && contentHtml) {
|
|
const currentCandidate = templateTextCandidateByChapterId.get(chapterKey) || null;
|
|
const nextCandidate = {
|
|
position: Number(block?.position || 1) || 1,
|
|
contentSnapshot: deepCloneJson(block.contentSnapshot) || { content: contentHtml },
|
|
priority: blockType === 'text' ? 2 : 1,
|
|
};
|
|
if (
|
|
!currentCandidate
|
|
|| nextCandidate.priority > currentCandidate.priority
|
|
|| (
|
|
nextCandidate.priority === currentCandidate.priority
|
|
&& nextCandidate.position < currentCandidate.position
|
|
)
|
|
) {
|
|
templateTextCandidateByChapterId.set(chapterKey, nextCandidate);
|
|
}
|
|
}
|
|
if (blockType !== 'kpi') return;
|
|
if (!chapterKey) return;
|
|
const currentList = templateKpiBlockIdsByChapterId.get(chapterKey) || [];
|
|
currentList.push(String(block.id));
|
|
templateKpiBlockIdsByChapterId.set(chapterKey, currentList);
|
|
});
|
|
const blockIdMap = new Map();
|
|
const createdBlockStateMap = new Map();
|
|
const createdBlockStateByInstanceId = new Map();
|
|
const hasCreatedTextBlockByChapterId = new Map();
|
|
for (const block of templateBlocks) {
|
|
const newChapterId = chapterIdMap.get(block.chapterId);
|
|
if (!newChapterId) continue;
|
|
let blockConfig = blockConfigOverrides.has(block.id)
|
|
? blockConfigOverrides.get(block.id)
|
|
: (block.defaultConfig || {});
|
|
blockConfig = rewriteInlineAnchorConfig(blockConfig, chapterIdMap);
|
|
let contentSnapshot = deepCloneJson(block.contentSnapshot) || null;
|
|
if (typeof options.resolveBlockPayload === 'function') {
|
|
const resolvedPayload = await options.resolveBlockPayload(block, {
|
|
config: blockConfig,
|
|
contentSnapshot,
|
|
});
|
|
if (resolvedPayload && typeof resolvedPayload === 'object') {
|
|
if (Object.prototype.hasOwnProperty.call(resolvedPayload, 'config')) {
|
|
blockConfig = deepCloneJson(resolvedPayload.config) || {};
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(resolvedPayload, 'contentSnapshot')) {
|
|
contentSnapshot = deepCloneJson(resolvedPayload.contentSnapshot) || null;
|
|
}
|
|
}
|
|
} else if (typeof options.resolveBlockConfig === 'function') {
|
|
blockConfig = await options.resolveBlockConfig(block, blockConfig);
|
|
}
|
|
const createdBlock = await models.ReportBlock.create({
|
|
instanceId: toInt(instanceId),
|
|
chapterId: newChapterId,
|
|
blockType: block.blockType,
|
|
position: block.position,
|
|
config: blockConfig,
|
|
contentSnapshot,
|
|
updatedAt: new Date(),
|
|
}, {
|
|
transaction,
|
|
returning: true,
|
|
});
|
|
const oldBlockId = String(block.id);
|
|
blockIdMap.set(oldBlockId, createdBlock.id);
|
|
if (String(block?.blockType || '').trim().toLowerCase() === 'text') {
|
|
hasCreatedTextBlockByChapterId.set(String(newChapterId), true);
|
|
}
|
|
createdBlockStateMap.set(oldBlockId, {
|
|
createdId: createdBlock.id,
|
|
blockType: block.blockType,
|
|
config: blockConfig,
|
|
contentSnapshot,
|
|
});
|
|
createdBlockStateByInstanceId.set(createdBlock.id, {
|
|
createdId: createdBlock.id,
|
|
blockType: block.blockType,
|
|
config: blockConfig,
|
|
contentSnapshot,
|
|
});
|
|
}
|
|
|
|
for (const block of templateBlocks) {
|
|
const oldBlockId = String(block.id);
|
|
const createdState = createdBlockStateMap.get(oldBlockId);
|
|
if (!createdState) continue;
|
|
let rewrittenConfig = deepCloneJson(createdState.config) || {};
|
|
let rewrittenContentSnapshot = rewriteBlockContentSnapshot(createdState.contentSnapshot, {
|
|
blockIdMap,
|
|
chapterIdMap,
|
|
fallbackKpiBlockIds: (
|
|
templateKpiBlockIdsByChapterId.get(String(block.chapterId || '').trim()) || []
|
|
)
|
|
.map(templateBlockId => blockIdMap.get(String(templateBlockId)))
|
|
.filter(Boolean),
|
|
getCreatedStateByInstanceId: instanceBlockId => (
|
|
createdBlockStateByInstanceId.get(Number(instanceBlockId)) || null
|
|
),
|
|
});
|
|
if (typeof options.finalizeCreatedBlock === 'function') {
|
|
const finalizedPayload = await options.finalizeCreatedBlock(
|
|
block,
|
|
{
|
|
createdId: createdState.createdId,
|
|
blockType: createdState.blockType,
|
|
config: rewrittenConfig,
|
|
contentSnapshot: rewrittenContentSnapshot,
|
|
},
|
|
{
|
|
blockIdMap,
|
|
getCreatedStateByTemplateBlockId: templateBlockId => (
|
|
createdBlockStateMap.get(String(templateBlockId)) || null
|
|
),
|
|
getCreatedStateByInstanceId: instanceBlockId => (
|
|
createdBlockStateByInstanceId.get(Number(instanceBlockId)) || null
|
|
),
|
|
}
|
|
);
|
|
if (finalizedPayload && typeof finalizedPayload === 'object') {
|
|
if (Object.prototype.hasOwnProperty.call(finalizedPayload, 'config')) {
|
|
rewrittenConfig = deepCloneJson(finalizedPayload.config) || {};
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(finalizedPayload, 'contentSnapshot')) {
|
|
rewrittenContentSnapshot = deepCloneJson(finalizedPayload.contentSnapshot) || null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const configChanged = JSON.stringify(rewrittenConfig) !== JSON.stringify(createdState.config);
|
|
const snapshotChanged = JSON.stringify(rewrittenContentSnapshot) !== JSON.stringify(createdState.contentSnapshot);
|
|
if (!configChanged && !snapshotChanged) continue;
|
|
|
|
await models.ReportBlock.update({
|
|
...(configChanged ? { config: rewrittenConfig } : {}),
|
|
...(snapshotChanged ? { contentSnapshot: rewrittenContentSnapshot } : {}),
|
|
updatedAt: new Date(),
|
|
}, {
|
|
where: { id: createdState.createdId },
|
|
transaction,
|
|
});
|
|
createdState.config = rewrittenConfig;
|
|
createdState.contentSnapshot = rewrittenContentSnapshot;
|
|
createdBlockStateByInstanceId.set(createdState.createdId, {
|
|
...createdBlockStateByInstanceId.get(createdState.createdId),
|
|
config: rewrittenConfig,
|
|
contentSnapshot: rewrittenContentSnapshot,
|
|
});
|
|
}
|
|
|
|
for (const [oldChapterId, candidate] of templateTextCandidateByChapterId.entries()) {
|
|
const newChapterId = chapterIdMap.get(Number(oldChapterId)) || chapterIdMap.get(oldChapterId);
|
|
if (!newChapterId) continue;
|
|
if (hasCreatedTextBlockByChapterId.get(String(newChapterId))) continue;
|
|
const rewrittenContentSnapshot = rewriteBlockContentSnapshot(candidate.contentSnapshot, {
|
|
blockIdMap,
|
|
chapterIdMap,
|
|
fallbackKpiBlockIds: (
|
|
templateKpiBlockIdsByChapterId.get(String(oldChapterId || '').trim()) || []
|
|
)
|
|
.map(templateBlockId => blockIdMap.get(String(templateBlockId)))
|
|
.filter(Boolean),
|
|
getCreatedStateByInstanceId: instanceBlockId => (
|
|
createdBlockStateByInstanceId.get(Number(instanceBlockId)) || null
|
|
),
|
|
});
|
|
if (!extractSnapshotContentHtml(rewrittenContentSnapshot)) continue;
|
|
|
|
const chapterBlocks = await models.ReportBlock.findAll({
|
|
where: {
|
|
instanceId: toInt(instanceId),
|
|
chapterId: toInt(newChapterId),
|
|
},
|
|
order: [['position', 'DESC'], ['id', 'DESC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const insertPosition = Math.max(1, Number(candidate?.position || 1) || 1);
|
|
for (const row of chapterBlocks) {
|
|
const currentPosition = Number(row?.position || 0);
|
|
if (currentPosition < insertPosition) continue;
|
|
await models.ReportBlock.update({
|
|
position: currentPosition + 1,
|
|
}, {
|
|
where: { id: row.id },
|
|
transaction,
|
|
});
|
|
}
|
|
await models.ReportBlock.create({
|
|
instanceId: toInt(instanceId),
|
|
chapterId: toInt(newChapterId),
|
|
blockType: 'text',
|
|
position: insertPosition,
|
|
config: {
|
|
title: '正文',
|
|
},
|
|
contentSnapshot: rewrittenContentSnapshot,
|
|
updatedAt: new Date(),
|
|
}, {
|
|
transaction,
|
|
});
|
|
hasCreatedTextBlockByChapterId.set(String(newChapterId), true);
|
|
}
|
|
|
|
return {
|
|
chapterIdMap,
|
|
blockIdMap,
|
|
};
|
|
};
|
|
|
|
const normalizeTemplateBlockConfigOverrides = async (
|
|
ctx,
|
|
templateId,
|
|
blockConfigOverrides,
|
|
transaction = null
|
|
) => {
|
|
if (!Array.isArray(blockConfigOverrides)) {
|
|
throw 'invalid param: blockConfigOverrides';
|
|
}
|
|
|
|
const parsedTemplateId = toInt(templateId);
|
|
if (!parsedTemplateId) throw '缺少参数: templateId';
|
|
|
|
const { models } = ctx.app.fs.dc;
|
|
const overrideMap = new Map();
|
|
for (let i = 0; i < blockConfigOverrides.length; i += 1) {
|
|
const row = ensureObjectPayload(
|
|
blockConfigOverrides[i],
|
|
`blockConfigOverrides[${i}]`
|
|
);
|
|
const templateBlockId = toInt(row.templateBlockId);
|
|
if (!templateBlockId) {
|
|
throw `invalid param: blockConfigOverrides[${i}].templateBlockId`;
|
|
}
|
|
if (overrideMap.has(templateBlockId)) {
|
|
throw `duplicate param: blockConfigOverrides.templateBlockId (${templateBlockId})`;
|
|
}
|
|
const config = ensureObjectPayload(
|
|
row.config,
|
|
`blockConfigOverrides[${i}].config`
|
|
);
|
|
overrideMap.set(templateBlockId, config);
|
|
}
|
|
|
|
if (!overrideMap.size) return overrideMap;
|
|
|
|
const candidateIds = Array.from(overrideMap.keys());
|
|
const rows = await models.ReportTemplateBlock.findAll({
|
|
where: {
|
|
templateId: parsedTemplateId,
|
|
id: candidateIds,
|
|
},
|
|
attributes: ['id'],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const validIdSet = new Set(rows.map(item => Number(item.id)));
|
|
for (const id of candidateIds) {
|
|
if (!validIdSet.has(id)) {
|
|
throw `invalid param: blockConfigOverrides.templateBlockId (${id})`;
|
|
}
|
|
}
|
|
|
|
return overrideMap;
|
|
};
|
|
|
|
const cloneInstanceToTemplate = async (ctx, instanceId, templateId, transaction = null) => {
|
|
const { models } = ctx.app.fs.dc;
|
|
const instanceChapters = await models.ReportChapter.findAll({
|
|
where: { instanceId: toInt(instanceId) },
|
|
order: [['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
|
|
const chapterIdMap = new Map();
|
|
const pending = [...instanceChapters];
|
|
while (pending.length) {
|
|
let progressed = false;
|
|
for (let i = pending.length - 1; i >= 0; i -= 1) {
|
|
const chapter = pending[i];
|
|
if (chapter.parentId && !chapterIdMap.has(chapter.parentId)) continue;
|
|
const created = await models.ReportTemplateChapter.create({
|
|
templateId: toInt(templateId),
|
|
parentId: chapter.parentId ? chapterIdMap.get(chapter.parentId) : null,
|
|
title: chapter.title,
|
|
position: chapter.position,
|
|
}, {
|
|
transaction,
|
|
returning: true,
|
|
});
|
|
chapterIdMap.set(chapter.id, created.id);
|
|
pending.splice(i, 1);
|
|
progressed = true;
|
|
}
|
|
if (!progressed) {
|
|
throw '报表章节层级异常,无法复制为模板';
|
|
}
|
|
}
|
|
|
|
const reportBlocks = await models.ReportBlock.findAll({
|
|
where: { instanceId: toInt(instanceId) },
|
|
order: [['chapterId', 'ASC'], ['position', 'ASC'], ['id', 'ASC']],
|
|
raw: true,
|
|
transaction,
|
|
});
|
|
const reportKpiBlockIdsByChapterId = new Map();
|
|
reportBlocks.forEach(block => {
|
|
if (String(block?.blockType || '').trim().toLowerCase() !== 'kpi') return;
|
|
const chapterKey = String(block.chapterId || '').trim();
|
|
if (!chapterKey) return;
|
|
const currentList = reportKpiBlockIdsByChapterId.get(chapterKey) || [];
|
|
currentList.push(String(block.id));
|
|
reportKpiBlockIdsByChapterId.set(chapterKey, currentList);
|
|
});
|
|
const blockIdMap = new Map();
|
|
const createdBlockStateMap = new Map();
|
|
for (const block of reportBlocks) {
|
|
const newChapterId = chapterIdMap.get(block.chapterId);
|
|
if (!newChapterId) continue;
|
|
const createdBlock = await models.ReportTemplateBlock.create({
|
|
templateId: toInt(templateId),
|
|
chapterId: newChapterId,
|
|
blockType: block.blockType,
|
|
position: block.position,
|
|
defaultConfig: rewriteInlineAnchorConfig(block.config || {}, chapterIdMap),
|
|
contentSnapshot: block.contentSnapshot || null,
|
|
}, {
|
|
transaction,
|
|
returning: true,
|
|
});
|
|
const oldBlockId = String(block.id);
|
|
blockIdMap.set(oldBlockId, createdBlock.id);
|
|
createdBlockStateMap.set(oldBlockId, {
|
|
createdId: createdBlock.id,
|
|
blockType: block.blockType,
|
|
config: block.config || {},
|
|
contentSnapshot: block.contentSnapshot || null,
|
|
});
|
|
}
|
|
|
|
for (const block of reportBlocks) {
|
|
const oldBlockId = String(block.id);
|
|
const createdState = createdBlockStateMap.get(oldBlockId);
|
|
if (!createdState) continue;
|
|
const rewrittenConfig = String(block?.blockType || '').trim().toLowerCase() === 'ai_analysis'
|
|
? rewriteAiAnalysisConfigForTemplate(createdState.config, blockIdMap)
|
|
: createdState.config;
|
|
const rewrittenContentSnapshot = rewriteBlockContentSnapshot(createdState.contentSnapshot, {
|
|
blockIdMap,
|
|
chapterIdMap,
|
|
preserveExistingLabelOnEmpty: true,
|
|
fallbackKpiBlockIds: (
|
|
reportKpiBlockIdsByChapterId.get(String(block.chapterId || '').trim()) || []
|
|
)
|
|
.map(reportBlockId => blockIdMap.get(String(reportBlockId)))
|
|
.filter(Boolean),
|
|
});
|
|
const configChanged = JSON.stringify(rewrittenConfig) !== JSON.stringify(createdState.config);
|
|
const snapshotChanged = JSON.stringify(rewrittenContentSnapshot) !== JSON.stringify(createdState.contentSnapshot);
|
|
if (!configChanged && !snapshotChanged) {
|
|
continue;
|
|
}
|
|
await models.ReportTemplateBlock.update({
|
|
...(configChanged ? { defaultConfig: rewrittenConfig } : {}),
|
|
...(snapshotChanged ? { contentSnapshot: rewrittenContentSnapshot } : {}),
|
|
}, {
|
|
where: { id: createdState.createdId },
|
|
transaction,
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getLatestSchemaVersion,
|
|
resolveSchemaVersion,
|
|
ensureDataSource,
|
|
ensureInstance,
|
|
ensureTemplate,
|
|
ensureActiveTemplate,
|
|
ensureTemplateChapter,
|
|
ensureChapter,
|
|
ensureBlock,
|
|
ensureTemplateBlock,
|
|
getTemplateBatchDimension,
|
|
ensureTemplateBatchDimension,
|
|
getTemplateBlockBatchBinding,
|
|
getTemplateBlockBatchBindingMap,
|
|
getInstanceChapterTree,
|
|
getTemplateChapterTree,
|
|
createBlocksFromPayload,
|
|
createChapterTreeFromPayload,
|
|
cloneTemplateToInstance,
|
|
normalizeTemplateBlockConfigOverrides,
|
|
cloneInstanceToTemplate,
|
|
};
|
|
|