'use strict'; const { TEMP_POSITION_BASE } = require('./constants'); const { hasOwn, toInt, parsePagination, ensureCreatedBy, normalizeParentId, normalizeNullableInt, ensureObjectPayload } = require('./helpers'); const { ensureInstance, ensureTemplate, ensureTemplateChapter, ensureTemplateBlock, ensureDataSource, resolveSchemaVersion, getTemplateChapterTree, getInstanceChapterTree, cloneInstanceToTemplate, getTemplateBlockBatchBinding, normalizeTemplateBlockConfigOverrides, cloneTemplateToInstance, } = require('./repository'); const { ensureNoTemplateChapterCycle, reorderTemplateChapterSiblingsWithInsert, reorderTemplateChapterSiblings, reorderTemplateBlockSiblingsWithInsert, reorderTemplateBlockSiblings, } = require('./ordering'); const { getDataSourceTableColumns, normalizeTemplateBlockConfigForStorage, } = require('./dataSourceQuery'); const { sanitizeQueryBlockConfig, normalizeChartContentSnapshot, } = require('./chartSnapshot'); const { resolveBlockPayload, finalizeResolvedBlockPayload, } = require('./blockPayloadResolver'); const extractBlockQueryConfig = config => { const safeConfig = ensureObjectPayload(config || {}, 'config'); if (safeConfig.query && typeof safeConfig.query === 'object' && !Array.isArray(safeConfig.query)) { return safeConfig.query; } return safeConfig; }; const extractBlockQueryTable = config => { const safeConfig = ensureObjectPayload(config || {}, 'config'); const queryConfig = extractBlockQueryConfig(safeConfig); return String( queryConfig.table || queryConfig.tableId || queryConfig.tableName || safeConfig.table || safeConfig.tableId || safeConfig.tableName || safeConfig.previewQuery?.table || '' ).trim(); }; const getFieldSetForTable = async (ctx, dataSourceId, tableId, transaction = null) => { const payload = await getDataSourceTableColumns(ctx, dataSourceId, tableId, transaction); return new Set((payload.rows || []).map(item => String(item.name || '').trim()).filter(Boolean)); }; const validateTemplateBlockBindingConsistency = async ( ctx, template, templateBlockId, config, transaction = null ) => { const binding = await getTemplateBlockBatchBinding(ctx, template.id, templateBlockId, transaction); if (!binding) return; const currentTable = extractBlockQueryTable(config); if (!currentTable) { throw '当前模板块已配置批量绑定,请先补齐查询表/数据集或删除绑定'; } if (binding.targetTable && binding.targetTable !== currentTable) { throw '当前模板块已配置批量绑定,查询表/数据集与绑定规则不一致,请先修改或删除绑定'; } const fieldSet = await getFieldSetForTable(ctx, template.dataSourceId, currentTable, transaction); if (!fieldSet.has(String(binding.targetField || '').trim())) { throw '当前模板块已配置批量绑定,目标字段与块配置不一致,请先修改或删除绑定'; } }; module.exports.getTemplates = async (ctx, next) => { try { const { models, ORM: { Op } } = ctx.app.fs.dc; const { offset, limit } = parsePagination(ctx.request.query); const where = {}; const name = String(ctx.request.query.name || '').trim(); const type = String(ctx.request.query.type || '').trim(); const createdBy = String(ctx.request.query.createdBy || '').trim(); const departmentId = normalizeNullableInt(ctx.request.query.departmentId, 'departmentId'); if (name) where.name = { [Op.like]: `%${name}%` }; if (type) where.type = type; if (createdBy) where.createdBy = createdBy; if (departmentId) where.departmentId = departmentId; if (hasOwn(ctx.request.query, 'isActive')) { where.isActive = String(ctx.request.query.isActive) === 'true'; } const list = await models.ReportTemplate.findAndCountAll({ where, include: [{ model: models.ReportSchemaVersion, required: false }], order: [['id', 'DESC']], offset, limit, }); ctx.body = { count: list.count, rows: list.rows }; ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '获取模板列表失败' }; } }; module.exports.getTemplateDetail = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); if (!templateId) throw '缺少参数: templateId'; const template = await models.ReportTemplate.findByPk(templateId, { include: [{ model: models.ReportSchemaVersion, required: false }], }); if (!template) throw '模板不存在'; const chapters = await getTemplateChapterTree(ctx, templateId); ctx.body = { ...template.toJSON(), chapters, }; ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '获取模板详情失败' }; } }; module.exports.getTemplateInstances = async (ctx, next) => { try { const { models, ORM: { Op } } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); if (!templateId) throw '缺少参数: templateId'; await ensureTemplate(ctx, templateId); const { offset, limit } = parsePagination(ctx.request.query); const where = { templateId }; const name = String(ctx.request.query.name || '').trim(); const type = String(ctx.request.query.type || '').trim(); const createdBy = String(ctx.request.query.createdBy || '').trim(); const departmentId = normalizeNullableInt(ctx.request.query.departmentId, 'departmentId'); if (name) where.name = { [Op.like]: `%${name}%` }; if (type) where.type = type; if (createdBy) where.createdBy = createdBy; if (departmentId) where.departmentId = departmentId; const list = await models.ReportInstance.findAndCountAll({ where, order: [['id', 'DESC']], include: [ { model: models.ReportTemplate, required: false }, { model: models.ReportDataSource, required: false }, { model: models.ReportSchemaVersion, required: false }, ], offset, limit, }); ctx.body = { count: list.count, rows: list.rows }; ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '获取模板下报表实例列表失败' }; } }; module.exports.updateTemplate = async (ctx, next) => { try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); if (!templateId) throw '缺少参数: templateId'; await ensureTemplate(ctx, templateId); const body = ctx.request.body || {}; const updateData = {}; if (hasOwn(body, 'name')) updateData.name = body.name ? String(body.name).trim() : ''; if (hasOwn(body, 'description')) updateData.description = body.description || null; if (hasOwn(body, 'version')) updateData.version = body.version ? String(body.version).trim() : '1.0.0'; if (hasOwn(body, 'type')) updateData.type = body.type ? String(body.type).trim() : 'general'; if (hasOwn(body, 'isActive')) updateData.isActive = Boolean(body.isActive); if (hasOwn(body, 'departmentId')) updateData.departmentId = normalizeNullableInt(body.departmentId, 'departmentId'); if (hasOwn(body, 'schemaVersionId')) { const schema = await resolveSchemaVersion(ctx, body.schemaVersionId); updateData.schemaVersionId = schema.id; } await models.ReportTemplate.update(updateData, { where: { id: templateId } }); ctx.status = 204; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '更新模板失败' }; } }; module.exports.getTemplateChapters = async (ctx, next) => { try { const templateId = toInt(ctx.params.templateId); if (!templateId) throw '缺少参数: templateId'; await ensureTemplate(ctx, templateId); ctx.body = await getTemplateChapterTree(ctx, templateId); ctx.status = 200; } catch (error) { ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '获取模板章节列表失败' }; } }; module.exports.createTemplateChapter = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); if (!templateId) throw '缺少参数: templateId'; await ensureTemplate(ctx, templateId, transaction); const { title, position } = ctx.request.body || {}; if (!String(title || '').trim()) throw '缺少参数: title'; const parentId = normalizeParentId(ctx.request.body?.parentId); await ensureNoTemplateChapterCycle(ctx, templateId, null, parentId, transaction); const created = await models.ReportTemplateChapter.create({ templateId, parentId, title: String(title).trim(), position: TEMP_POSITION_BASE - 1, }, { transaction, returning: true, }); await reorderTemplateChapterSiblingsWithInsert( ctx, templateId, parentId, created.id, position, transaction ); const fresh = await models.ReportTemplateChapter.findByPk(created.id, { transaction }); await transaction.commit(); ctx.body = fresh; ctx.status = 200; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '创建模板章节失败' }; } }; module.exports.updateTemplateChapter = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); const templateChapterId = toInt(ctx.params.templateChapterId); if (!templateId || !templateChapterId) throw '缺少参数'; const chapter = await ensureTemplateChapter(ctx, templateId, templateChapterId, transaction); const body = ctx.request.body || {}; const updateData = {}; if (hasOwn(body, 'title')) updateData.title = body.title ? String(body.title).trim() : ''; const oldParentId = chapter.parentId; const nextParentId = hasOwn(body, 'parentId') ? normalizeParentId(body.parentId) : chapter.parentId; await ensureNoTemplateChapterCycle(ctx, templateId, templateChapterId, nextParentId, transaction); const needMove = hasOwn(body, 'parentId') || hasOwn(body, 'position'); if (!needMove) { await models.ReportTemplateChapter.update(updateData, { where: { id: templateChapterId }, transaction, }); await transaction.commit(); ctx.status = 204; return; } updateData.parentId = nextParentId; updateData.position = TEMP_POSITION_BASE - 1; await models.ReportTemplateChapter.update(updateData, { where: { id: templateChapterId }, transaction, }); if (oldParentId !== nextParentId) { await reorderTemplateChapterSiblings(ctx, templateId, oldParentId, transaction); } const targetPosition = hasOwn(body, 'position') ? body.position : (oldParentId === nextParentId ? chapter.position : undefined); await reorderTemplateChapterSiblingsWithInsert( ctx, templateId, nextParentId, templateChapterId, targetPosition, transaction ); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '更新模板章节失败' }; } }; module.exports.deleteTemplateChapter = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); const templateChapterId = toInt(ctx.params.templateChapterId); if (!templateId || !templateChapterId) throw '缺少参数'; const chapter = await ensureTemplateChapter(ctx, templateId, templateChapterId, transaction); await models.ReportTemplateChapter.destroy({ where: { id: templateChapterId, templateId }, transaction, }); await reorderTemplateChapterSiblings(ctx, templateId, chapter.parentId, transaction); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '删除模板章节失败' }; } }; module.exports.createTemplateBlock = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); const templateChapterId = toInt(ctx.params.templateChapterId); if (!templateId || !templateChapterId) throw '缺少参数'; await ensureTemplate(ctx, templateId, transaction); await ensureTemplateChapter(ctx, templateId, templateChapterId, transaction); const { blockType, position, config, contentSnapshot } = ctx.request.body || {}; const normalizedBlockType = String(blockType || 'text'); const normalizedConfig = await normalizeTemplateBlockConfigForStorage( ctx, templateId, normalizedBlockType, config || {}, transaction ); const created = await models.ReportTemplateBlock.create({ templateId, chapterId: templateChapterId, blockType: normalizedBlockType, position: TEMP_POSITION_BASE - 1, defaultConfig: sanitizeQueryBlockConfig(normalizedConfig), contentSnapshot: String(normalizedBlockType).trim().toLowerCase() === 'chart' ? (normalizeChartContentSnapshot(contentSnapshot) || normalizeChartContentSnapshot(normalizedConfig)) : (contentSnapshot || null), }, { transaction, returning: true, }); await reorderTemplateBlockSiblingsWithInsert( ctx, templateChapterId, created.id, position, transaction ); const fresh = await models.ReportTemplateBlock.findByPk(created.id, { transaction }); await transaction.commit(); ctx.body = fresh; ctx.status = 200; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '创建模板内容块失败' }; } }; module.exports.updateTemplateBlock = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); const templateBlockId = toInt(ctx.params.templateBlockId); if (!templateId || !templateBlockId) throw '缺少参数'; const template = await ensureTemplate(ctx, templateId, transaction); const block = await ensureTemplateBlock(ctx, templateId, templateBlockId, transaction); const body = ctx.request.body || {}; const updateData = {}; const hasConfigMutation = hasOwn(body, 'config') || hasOwn(body, 'blockType'); if (hasOwn(body, 'blockType')) updateData.blockType = String(body.blockType || 'text'); if (hasConfigMutation) { const finalBlockType = hasOwn(body, 'blockType') ? updateData.blockType : block.blockType; const finalConfig = hasOwn(body, 'config') ? (body.config || {}) : (block.defaultConfig || {}); updateData.defaultConfig = await normalizeTemplateBlockConfigForStorage( ctx, templateId, finalBlockType, finalConfig, transaction ); updateData.defaultConfig = sanitizeQueryBlockConfig(updateData.defaultConfig); await validateTemplateBlockBindingConsistency( ctx, template, templateBlockId, updateData.defaultConfig, transaction ); const finalContentSnapshot = hasOwn(body, 'contentSnapshot') ? body.contentSnapshot : block.contentSnapshot; updateData.contentSnapshot = String(finalBlockType || '').trim().toLowerCase() === 'chart' ? (normalizeChartContentSnapshot(finalContentSnapshot) || normalizeChartContentSnapshot(finalConfig)) : (finalContentSnapshot || null); } else if (hasOwn(body, 'contentSnapshot')) { updateData.contentSnapshot = body.contentSnapshot || null; } const oldChapterId = block.chapterId; const nextChapterId = hasOwn(body, 'chapterId') ? toInt(body.chapterId) : block.chapterId; if (!nextChapterId) throw '参数错误: chapterId'; await ensureTemplateChapter(ctx, templateId, nextChapterId, transaction); const needMove = hasOwn(body, 'chapterId') || hasOwn(body, 'position'); if (!needMove) { await models.ReportTemplateBlock.update(updateData, { where: { id: templateBlockId }, transaction, }); await transaction.commit(); ctx.status = 204; return; } updateData.chapterId = nextChapterId; updateData.position = TEMP_POSITION_BASE - 1; await models.ReportTemplateBlock.update(updateData, { where: { id: templateBlockId }, transaction, }); if (oldChapterId !== nextChapterId) { await reorderTemplateBlockSiblings(ctx, oldChapterId, transaction); } const targetPosition = hasOwn(body, 'position') ? body.position : (oldChapterId === nextChapterId ? block.position : undefined); await reorderTemplateBlockSiblingsWithInsert( ctx, nextChapterId, templateBlockId, targetPosition, transaction ); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '更新模板内容块失败' }; } }; module.exports.deleteTemplateBlock = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); const templateBlockId = toInt(ctx.params.templateBlockId); if (!templateId || !templateBlockId) throw '缺少参数'; const block = await ensureTemplateBlock(ctx, templateId, templateBlockId, transaction); await models.ReportTemplateBlock.destroy({ where: { id: templateBlockId, templateId }, transaction, }); await reorderTemplateBlockSiblings(ctx, block.chapterId, transaction); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '删除模板内容块失败' }; } }; module.exports.deleteTemplate = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); if (!templateId) throw '缺少参数: templateId'; await ensureTemplate(ctx, templateId, transaction); await models.ReportTemplateBlock.destroy({ where: { templateId }, transaction, }); await models.ReportTemplateChapter.destroy({ where: { templateId }, transaction, }); await models.ReportTemplate.destroy({ where: { id: templateId }, transaction, }); await transaction.commit(); ctx.status = 204; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '删除模板失败' }; } }; module.exports.saveInstanceAsTemplate = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const instanceId = toInt(ctx.params.instanceId); if (!instanceId) throw '缺少参数: instanceId'; const instance = await ensureInstance(ctx, instanceId, transaction); const body = ctx.request.body || {}; const createdBy = ensureCreatedBy(body); const name = String(body.name || '').trim(); const departmentId = hasOwn(body, 'departmentId') ? normalizeNullableInt(body.departmentId, 'departmentId') : normalizeNullableInt(instance.departmentId, 'departmentId'); if (!name) throw '缺少参数: name'; const schemaVersion = await resolveSchemaVersion(ctx, instance.schemaVersionId, transaction); const createdTemplate = await models.ReportTemplate.create({ name, description: body.description || null, version: String(body.version || '1.0.0'), schemaVersionId: schemaVersion.id, dataSourceId: toInt(instance.dataSourceId), type: String(body.type || instance.type || 'general'), createdBy, departmentId, isActive: hasOwn(body, 'isActive') ? Boolean(body.isActive) : true, }, { transaction, returning: true, }); await cloneInstanceToTemplate(ctx, instanceId, createdTemplate.id, transaction); const chapters = await getTemplateChapterTree(ctx, createdTemplate.id, transaction); await transaction.commit(); ctx.body = { ...createdTemplate.toJSON(), chapters, }; ctx.status = 200; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '报表另存模板失败' }; } }; module.exports.createInstanceFromTemplate = async (ctx, next) => { const transaction = await ctx.app.fs.dc.orm.transaction(); try { const { models } = ctx.app.fs.dc; const templateId = toInt(ctx.params.templateId); if (!templateId) throw '缺少参数: templateId'; const template = await ensureTemplate(ctx, templateId, transaction); const body = ctx.request.body || {}; const createdBy = ensureCreatedBy(body); const name = String(body.name || '').trim(); const departmentId = hasOwn(body, 'departmentId') ? normalizeNullableInt(body.departmentId, 'departmentId') : normalizeNullableInt(template.departmentId, 'departmentId'); if (!name) throw '缺少参数: name'; const templateDataSourceId = toInt(template.dataSourceId); if (!templateDataSourceId) throw '模板未绑定数据源'; await ensureDataSource(ctx, templateDataSourceId, transaction); const blockConfigOverrideMap = hasOwn(body, 'blockConfigOverrides') ? await normalizeTemplateBlockConfigOverrides( ctx, template.id, body.blockConfigOverrides, transaction ) : new Map(); const schemaVersion = await resolveSchemaVersion(ctx, body.schemaVersionId || template.schemaVersionId, transaction); const createdInstance = await models.ReportInstance.create({ templateId: template.id, schemaVersionId: schemaVersion.id, name, type: String(body.type || template.type || 'general'), dataSourceId: templateDataSourceId, createdBy, departmentId, }, { transaction, returning: true, }); await cloneTemplateToInstance( ctx, template.id, createdInstance.id, blockConfigOverrideMap, transaction, { resolveBlockPayload: async (templateBlock, payload) => { return resolveBlockPayload(ctx, { blockType: templateBlock?.blockType, dataSourceId: templateDataSourceId, config: payload?.config || {}, contentSnapshot: payload?.contentSnapshot || null, transaction, }); }, finalizeCreatedBlock: async (templateBlock, createdPayload, helpers) => { return finalizeResolvedBlockPayload(ctx, { blockType: templateBlock?.blockType, config: createdPayload?.config || {}, contentSnapshot: createdPayload?.contentSnapshot || null, blockIdMap: helpers?.blockIdMap, getCreatedBlockStateByInstanceId: helpers?.getCreatedStateByInstanceId, }); }, } ); const chapters = await getInstanceChapterTree(ctx, createdInstance.id, transaction); await transaction.commit(); ctx.body = { ...createdInstance.toJSON(), chapters, }; ctx.status = 200; } catch (error) { await transaction.rollback(); ctx.logger.log(error); ctx.status = 400; ctx.body = { message: typeof error === 'string' ? error : '模板创建实例失败' }; } };