218 lines
7.2 KiB
PHP
218 lines
7.2 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use think\Db;
|
||
use think\Response;
|
||
use app\common\PlagiarismService;
|
||
|
||
/**
|
||
* 论文查重(Turnitin / Crossref Similarity Check)控制器。
|
||
*
|
||
* 触发方式:纯手工(编辑后台点"查重"按钮)。
|
||
* 报告策略:在线 viewer URL 临时签名 + PDF 永久落盘 runtime/plagiarism/。
|
||
*
|
||
* 主要接口:
|
||
* POST submit 触发查重
|
||
* GET getStatus 轮询单条查重状态(前端 ajax)
|
||
* GET getList 列出某 article 的全部查重记录
|
||
* GET getReportUrl 获取/刷新在线查看 URL
|
||
* GET downloadReport 下载本地 PDF
|
||
* POST retry 重新触发(创建新行)
|
||
* GET features 探活(开发调试用)
|
||
*/
|
||
class Plagiarism extends Base
|
||
{
|
||
public function __construct(\think\Request $request = null)
|
||
{
|
||
parent::__construct($request);
|
||
}
|
||
|
||
/**
|
||
* 触发查重
|
||
*
|
||
* 入参:
|
||
* article_id 必填
|
||
* file_url 选填;不传则按 article_id 在 t_article_file 找 manuscirpt
|
||
* editor_id 选填;触发人 user_id(前端拿不到也可以传 0)
|
||
*/
|
||
public function submit()
|
||
{
|
||
$articleId = intval($this->request->param('article_id', 0));
|
||
$fileUrl = trim($this->request->param('file_url', ''));
|
||
$editorId = intval($this->request->param('editor_id', 0));
|
||
|
||
if ($articleId <= 0) {
|
||
return jsonError('article_id required');
|
||
}
|
||
|
||
try {
|
||
$svc = new PlagiarismService();
|
||
$localPath = $fileUrl !== ''
|
||
? $svc->resolveFileUrlToLocal($fileUrl)
|
||
: $svc->locateArticleManuscript($articleId);
|
||
|
||
$checkId = $svc->submit($articleId, $localPath, $editorId, 'manual');
|
||
return jsonSuccess(['check_id' => $checkId]);
|
||
} catch (\Throwable $e) {
|
||
return jsonError($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重试 = 提交一次新查重(保留历史)
|
||
*/
|
||
public function retry()
|
||
{
|
||
return $this->submit();
|
||
}
|
||
|
||
/**
|
||
* 取单条查重状态
|
||
*/
|
||
public function getStatus()
|
||
{
|
||
$checkId = intval($this->request->param('check_id', 0));
|
||
if ($checkId <= 0) {
|
||
return jsonError('check_id required');
|
||
}
|
||
$row = Db::name('plagiarism_check')->where('check_id', $checkId)->find();
|
||
if (!$row) {
|
||
return jsonError('not found');
|
||
}
|
||
return jsonSuccess($this->formatRow($row));
|
||
}
|
||
|
||
/**
|
||
* 列出某 article 的全部查重记录(按时间倒序)
|
||
*/
|
||
public function getList()
|
||
{
|
||
$articleId = intval($this->request->param('article_id', 0));
|
||
if ($articleId <= 0) {
|
||
return jsonError('article_id required');
|
||
}
|
||
$rows = Db::name('plagiarism_check')
|
||
->where('article_id', $articleId)
|
||
->order('check_id desc')
|
||
->select();
|
||
$out = [];
|
||
foreach ($rows as $r) {
|
||
$out[] = $this->formatRow($r);
|
||
}
|
||
return jsonSuccess(['list' => $out]);
|
||
}
|
||
|
||
/**
|
||
* 取在线查看 URL;过期则自动刷新
|
||
*/
|
||
public function getReportUrl()
|
||
{
|
||
$checkId = intval($this->request->param('check_id', 0));
|
||
if ($checkId <= 0) {
|
||
return jsonError('check_id required');
|
||
}
|
||
try {
|
||
$row = Db::name('plagiarism_check')->where('check_id', $checkId)->find();
|
||
if (!$row) {
|
||
return jsonError('not found');
|
||
}
|
||
if ($row['state'] != 3) {
|
||
return jsonError('check not completed yet, state=' . $row['state']);
|
||
}
|
||
$needRefresh = empty($row['view_only_url'])
|
||
|| intval($row['view_only_url_expire']) < time() + 60;
|
||
|
||
if ($needRefresh) {
|
||
$svc = new PlagiarismService();
|
||
$info = $svc->refreshViewerUrlFor($checkId);
|
||
return jsonSuccess([
|
||
'view_only_url' => $info['url'],
|
||
'expire' => $info['expire'],
|
||
]);
|
||
}
|
||
return jsonSuccess([
|
||
'view_only_url' => $row['view_only_url'],
|
||
'expire' => intval($row['view_only_url_expire']),
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
return jsonError($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 直接吐 PDF 二进制流给浏览器下载
|
||
*/
|
||
public function downloadReport()
|
||
{
|
||
$checkId = intval($this->request->param('check_id', 0));
|
||
if ($checkId <= 0) {
|
||
return jsonError('check_id required');
|
||
}
|
||
$row = Db::name('plagiarism_check')->where('check_id', $checkId)->find();
|
||
if (!$row || empty($row['pdf_local_path'])) {
|
||
return jsonError('report not available');
|
||
}
|
||
$rootDir = ROOT_PATH ?: dirname(dirname(dirname(__DIR__)));
|
||
$abs = rtrim($rootDir, '/\\') . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $row['pdf_local_path']);
|
||
if (!is_file($abs)) {
|
||
return jsonError('pdf file missing on disk: ' . $row['pdf_local_path']);
|
||
}
|
||
$filename = sprintf('plagiarism_check_%d_article_%d.pdf', $row['check_id'], $row['article_id']);
|
||
return Response::create(file_get_contents($abs), 'html', 200, [
|
||
'Content-Type' => 'application/pdf',
|
||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||
'Content-Length' => (string)filesize($abs),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Turnitin 探活(开发调试用)
|
||
*/
|
||
public function features()
|
||
{
|
||
try {
|
||
$tii = new \app\common\TurnitinService();
|
||
return jsonSuccess($tii->featuresEnabled());
|
||
} catch (\Throwable $e) {
|
||
return jsonError($e->getMessage());
|
||
}
|
||
}
|
||
|
||
// ---------- 内部 ----------
|
||
|
||
private function formatRow($r)
|
||
{
|
||
return [
|
||
'check_id' => intval($r['check_id']),
|
||
'article_id' => intval($r['article_id']),
|
||
'journal_id' => intval($r['journal_id']),
|
||
'state' => intval($r['state']),
|
||
'state_label' => $this->stateLabel($r['state']),
|
||
'similarity_score' => floatval($r['similarity_score']),
|
||
'tii_report_status' => (string)$r['tii_report_status'],
|
||
'has_pdf' => !empty($r['pdf_local_path']),
|
||
'has_viewer_url' => !empty($r['view_only_url']) && intval($r['view_only_url_expire']) > time(),
|
||
'attempts' => intval($r['attempts']),
|
||
'error_msg' => (string)$r['error_msg'],
|
||
'source_file_name' => (string)$r['source_file_name'],
|
||
'trigger_source' => (string)$r['trigger_source'],
|
||
'triggered_by' => intval($r['triggered_by']),
|
||
'ctime' => intval($r['ctime']),
|
||
'utime' => intval($r['utime']),
|
||
];
|
||
}
|
||
|
||
private function stateLabel($state)
|
||
{
|
||
$map = [
|
||
0 => '待上传',
|
||
1 => '上传中',
|
||
2 => '比对中',
|
||
3 => '完成',
|
||
4 => '失败',
|
||
];
|
||
return isset($map[$state]) ? $map[$state] : 'unknown';
|
||
}
|
||
}
|