'use strict'; const plagiarismCheckService = require('../utils/plagiarismCheckService'); const { recordAiQueryLog } = require('../utils/aiUsage'); const toInt = (value, defaultValue = null) => { const parsed = Number.parseInt(String(value), 10); return Number.isFinite(parsed) ? parsed : defaultValue; }; const getErrorMessage = (error, fallback) => typeof error === 'string' ? error : (error?.message || fallback); const normalizeTaskUserId = (ctx) => { const body = ctx.request.body || {}; return toInt(body.userId || body.externalUserId || ctx.state?.externalUserId || ctx.state?.user?.id); }; module.exports.createTask = async (ctx) => { try { const data = await plagiarismCheckService.createTask(ctx); try { await recordAiQueryLog(ctx, { userId: normalizeTaskUserId(ctx), ipAddress: String(ctx.request.body?.curIp || '').trim() || null, feature: '文档查重', }); } catch (error) { ctx.logger?.log?.(error); } ctx.status = 200; ctx.body = { message: '提交成功', data, }; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '提交查重任务失败') }; } }; module.exports.getTaskDetail = async (ctx) => { try { const { models } = ctx.app.fs.dc; const taskId = toInt(ctx.params.taskId); if (!taskId) throw new Error('缺少参数: taskId'); const task = await models.PlagiarismCheckTasks.findByPk(taskId, { include: [ { model: models.PlagiarismCheckFiles, required: false, include: [ { model: models.PlagiarismCheckDocTypes, required: false, }, { model: models.PlagiarismCheckFileSummaries, required: false, }, ], }, { model: models.PlagiarismCheckFileSummaries, required: false, }, ], order: [[models.PlagiarismCheckFiles, 'sortOrder', 'ASC']], }); if (!task) throw new Error('查重任务不存在'); ctx.status = 200; ctx.body = task; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '获取查重任务失败') }; } }; module.exports.getFileBlocks = async (ctx) => { try { const { models } = ctx.app.fs.dc; const taskId = toInt(ctx.params.taskId); const fileId = toInt(ctx.params.fileId); if (!taskId) throw new Error('缺少参数: taskId'); if (!fileId) throw new Error('缺少参数: fileId'); const page = Math.max(toInt(ctx.request.query.page, 1), 1); const pageSize = Math.min(Math.max(toInt(ctx.request.query.pageSize, 200), 1), 1000); const file = await models.PlagiarismCheckFiles.findOne({ where: { id: fileId, taskId }, raw: true, }); if (!file) throw new Error('文件不存在或不属于当前任务'); const result = await models.PlagiarismCheckFileBlocks.findAndCountAll({ where: { taskId, fileId }, order: [['blockIndex', 'ASC']], offset: (page - 1) * pageSize, limit: pageSize, }); ctx.status = 200; ctx.body = { count: result.count, rows: result.rows.map(plagiarismCheckService.buildEditableBlockResponse), page, pageSize, }; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '获取解析块失败') }; } }; module.exports.updateFileBlock = async (ctx) => { try { const taskId = toInt(ctx.params.taskId); const fileId = toInt(ctx.params.fileId); const blockId = toInt(ctx.params.blockId); if (!taskId) throw new Error('缺少参数: taskId'); if (!fileId) throw new Error('缺少参数: fileId'); if (!blockId) throw new Error('缺少参数: blockId'); const body = ctx.request.body || {}; const result = await plagiarismCheckService.updateBlockContent(ctx, { taskId, fileId, blockId, htmlContent: body.htmlContent, plainText: body.plainText, editSource: body.editSource, }); ctx.status = 200; ctx.body = result; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '保存解析块失败') }; } }; module.exports.deduplicateFileBlock = async (ctx) => { try { const taskId = toInt(ctx.params.taskId); const fileId = toInt(ctx.params.fileId); const blockId = toInt(ctx.params.blockId); if (!taskId) throw new Error('缺少参数: taskId'); if (!fileId) throw new Error('缺少参数: fileId'); if (!blockId) throw new Error('缺少参数: blockId'); const body = ctx.request.body || {}; const result = await plagiarismCheckService.generateDeduplicatedBlock(ctx, { taskId, fileId, blockId, htmlContent: body.htmlContent, plainText: body.plainText, prompt: body.prompt, }); ctx.status = 200; ctx.body = result; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '块内容降重失败') }; } }; module.exports.deleteFileBlock = async (ctx) => { try { const taskId = toInt(ctx.params.taskId); const fileId = toInt(ctx.params.fileId); const blockId = toInt(ctx.params.blockId); if (!taskId) throw new Error('缺少参数: taskId'); if (!fileId) throw new Error('缺少参数: fileId'); if (!blockId) throw new Error('缺少参数: blockId'); await plagiarismCheckService.deleteFileBlock(ctx, { taskId, fileId, blockId, }); ctx.status = 204; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '删除解析块失败') }; } }; module.exports.exportTaskFileDocx = async (ctx) => { try { const taskId = toInt(ctx.params.taskId); const fileId = toInt(ctx.params.fileId); if (!taskId) throw new Error('缺少参数: taskId'); if (!fileId) throw new Error('缺少参数: fileId'); const result = await plagiarismCheckService.exportTaskFileDocx(ctx, { taskId, fileId, }); ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(result.fileName)}"`); ctx.body = result.buffer; ctx.status = 200; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '导出文档失败') }; } }; module.exports.updateTaskThreshold = async (ctx) => { try { const taskId = toInt(ctx.params.taskId); if (!taskId) throw new Error('缺少参数: taskId'); const body = ctx.request.body || {}; const data = await plagiarismCheckService.updateTaskThreshold(ctx, { taskId, threshold: body.threshold, }); ctx.status = 200; ctx.body = { message: '阈值更新成功', data, }; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '更新查重阈值失败') }; } }; module.exports.getTaskMatches = async (ctx) => { try { const { models, ORM: { Op } } = ctx.app.fs.dc; const taskId = toInt(ctx.params.taskId); if (!taskId) throw new Error('缺少参数: taskId'); const page = Math.max(toInt(ctx.request.query.page, 1), 1); const pageSize = Math.min(Math.max(toInt(ctx.request.query.pageSize, 200), 1), 1000); const where = { taskId }; const fileId = toInt(ctx.request.query.fileId); const leftFileId = toInt(ctx.request.query.leftFileId); const rightFileId = toInt(ctx.request.query.rightFileId); const matchType = String(ctx.request.query.matchType || '').trim(); if (fileId) where[Op.or] = [{ leftFileId: fileId }, { rightFileId: fileId }]; if (leftFileId) where.leftFileId = leftFileId; if (rightFileId) where.rightFileId = rightFileId; if (matchType) { where.matchType = matchType === 'red' ? 'same_type' : (matchType === 'yellow' ? 'cross_type' : matchType); } const result = await models.PlagiarismCheckMatches.findAndCountAll({ where, order: [['id', 'ASC']], offset: (page - 1) * pageSize, limit: pageSize, }); ctx.status = 200; ctx.body = { count: result.count, rows: result.rows, page, pageSize, }; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '获取查重结果失败') }; } }; module.exports.getTaskResult = async (ctx) => { try { const { models } = ctx.app.fs.dc; const taskId = toInt(ctx.params.taskId); if (!taskId) throw new Error('缺少参数: taskId'); const task = await models.PlagiarismCheckTasks.findByPk(taskId, { raw: true }); if (!task) throw new Error('查重任务不存在'); const files = await models.PlagiarismCheckFiles.findAll({ where: { taskId }, order: [['sortOrder', 'ASC'], ['id', 'ASC']], raw: true, }); const summaries = await models.PlagiarismCheckFileSummaries.findAll({ where: { taskId }, order: [['fileId', 'ASC']], raw: true, }); const matches = await models.PlagiarismCheckMatches.findAll({ where: { taskId }, order: [['id', 'ASC']], raw: true, }); const fileIndexMap = new Map(files.map((file, index) => [Number(file.id), index])); const mockMatches = matches.map((match, index) => ({ id: `m-${index}`, dbId: match.id, leftFileId: match.leftFileId, rightFileId: match.rightFileId, leftBlockId: match.leftBlockId, rightBlockId: match.rightBlockId, leftIdx: fileIndexMap.get(Number(match.leftFileId)) ?? 0, rightIdx: fileIndexMap.get(Number(match.rightFileId)) ?? 0, leftPara: match.leftBlockIndex, rightPara: match.rightBlockIndex, len: match.matchLength, matchType: match.color, matchText: match.matchText, leftStart: match.leftStart, leftEnd: match.leftEnd, rightStart: match.rightStart, rightEnd: match.rightEnd, leftRawStart: match.leftRawStart, leftRawEnd: match.leftRawEnd, rightRawStart: match.rightRawStart, rightRawEnd: match.rightRawEnd, })); ctx.status = 200; ctx.body = { task, files, summaries, matches: mockMatches, }; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '获取查重汇总失败') }; } }; module.exports.rerunTaskCheck = async (ctx) => { try { const taskId = toInt(ctx.params.taskId); if (!taskId) throw new Error('缺少参数: taskId'); const result = await plagiarismCheckService.runPlagiarismCheck(ctx, taskId); ctx.status = 200; ctx.body = { message: '查重完成', data: result, }; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '重新查重失败') }; } }; module.exports.getDocTypes = async (ctx) => { try { const { models } = ctx.app.fs.dc; const includeDisabled = String(ctx.request.query.includeDisabled || '') === 'true'; const where = includeDisabled ? {} : { enabled: true }; const rows = await models.PlagiarismCheckDocTypes.findAll({ where, order: [['sortOrder', 'ASC'], ['id', 'ASC']], }); ctx.status = 200; ctx.body = rows; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '获取文档类型失败') }; } }; module.exports.createDocType = async (ctx) => { try { const { models } = ctx.app.fs.dc; const body = ctx.request.body || {}; const typeCode = String(body.typeCode || '').trim(); const typeName = String(body.typeName || '').trim(); if (!typeCode) throw new Error('缺少参数: typeCode'); if (!typeName) throw new Error('缺少参数: typeName'); const created = await models.PlagiarismCheckDocTypes.create({ typeCode, typeName, sortOrder: toInt(body.sortOrder, 0), enabled: body.enabled === undefined ? true : Boolean(body.enabled), updatedAt: new Date(), }, { returning: true }); ctx.status = 200; ctx.body = created; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '创建文档类型失败') }; } }; module.exports.updateDocType = async (ctx) => { try { const { models } = ctx.app.fs.dc; const docTypeId = toInt(ctx.params.docTypeId); if (!docTypeId) throw new Error('缺少参数: docTypeId'); const body = ctx.request.body || {}; const payload = { updatedAt: new Date() }; if (body.typeCode !== undefined) payload.typeCode = String(body.typeCode || '').trim(); if (body.typeName !== undefined) payload.typeName = String(body.typeName || '').trim(); if (body.sortOrder !== undefined) payload.sortOrder = toInt(body.sortOrder, 0); if (body.enabled !== undefined) payload.enabled = Boolean(body.enabled); if (payload.typeCode === '') throw new Error('typeCode 不能为空'); if (payload.typeName === '') throw new Error('typeName 不能为空'); const [count] = await models.PlagiarismCheckDocTypes.update(payload, { where: { id: docTypeId }, }); if (!count) throw new Error('文档类型不存在'); ctx.status = 204; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '更新文档类型失败') }; } }; module.exports.deleteDocType = async (ctx) => { try { const { models } = ctx.app.fs.dc; const docTypeId = toInt(ctx.params.docTypeId); if (!docTypeId) throw new Error('缺少参数: docTypeId'); const usingCount = await models.PlagiarismCheckFiles.count({ where: { docTypeId }, }); if (usingCount > 0) { await models.PlagiarismCheckDocTypes.update({ enabled: false, updatedAt: new Date(), }, { where: { id: docTypeId } }); ctx.status = 204; return; } await models.PlagiarismCheckDocTypes.destroy({ where: { id: docTypeId } }); ctx.status = 204; } catch (error) { ctx.logger?.log?.(error); ctx.status = 400; ctx.body = { message: getErrorMessage(error, '删除文档类型失败') }; } };