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.
442 lines
15 KiB
442 lines
15 KiB
'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, '删除文档类型失败') };
|
|
}
|
|
};
|
|
|