Files
tougao/application/api/controller/Plagiarism.php
2026-06-02 17:05:23 +08:00

343 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\api\controller;
use app\common\TurnitinService;
use think\Db;
use think\Response;
use app\common\PlagiarismService;
use think\Validate;
/**
* 论文查重Turnitin / Crossref Similarity Check控制器。
*
* 触发方式:纯手工(编辑后台点"查重"按钮)。
* 报告策略PDF 在 poll 完成时落盘;在线 viewer URL 通过 getReportUrl 按需生成(临时签名)。
*
* 主要接口:
* 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
* type 选填full默认全文| body_only正文| both各提交一条
*/
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));
$checkType = trim($this->request->param('type', 'full'));
if ($articleId <= 0) {
return jsonError('article_id required');
}
try {
$svc = new PlagiarismService();
$localPath = $fileUrl !== ''
? $svc->resolveFileUrlToLocal($fileUrl)
: $svc->locateArticleManuscript($articleId);
if (strtolower($checkType) === 'both') {
$ids = $svc->submitBoth($articleId, $localPath, $editorId, 'manual');
return jsonSuccess($ids);
}
$checkId = $svc->submit($articleId, $localPath, $editorId, 'manual', $checkType);
return jsonSuccess(['check_id' => $checkId, 'check_type' => strtolower($checkType) ?: 'full']);
} catch (\Throwable $e) {
return jsonError($e->getMessage());
}
}
/**
* 调试与线上一致走队列链upload → wait ingest → trigger → poll需 worker 消费 plagiarism 队列。
*/
public function testccone()
{
$svc = new PlagiarismService();
$checkId = 9;
$filePath = '/home/wwwroot/api.tmrjournals.com/public/manuscirpt/20260509/6832a56e8ace38fe99df390ab5221deb.docx';
$svc->runUploadOnly($checkId, $filePath);
}
public function testcconegetstatus(){
$data = $this->request->post();
$rule = new Validate([
"id"=>"require"
]);
if(!$rule->check($data)){
return jsonError($rule->getError());
}
$tii = new TurnitinService();
$res = $tii->parseSubmissionIngestState($data['id']);
return jsonSuccess($res);
}
public function testcconewait(){
$data = $this->request->post();
$rule = new Validate([
"checkId"=>"require"
]);
if(!$rule->check($data)){
return jsonError($rule->getError());
}
$svc = new PlagiarismService();
$res = $svc->runIngestPollStep($data['checkId']);
return jsonSuccess($res);
}
public function testcconesimilar(){
$data = $this->request->post();
$rule = new Validate([
"checkId"=>"require"
]);
if(!$rule->check($data)){
return jsonError($rule->getError());
}
$svc = new PlagiarismService();
$res = $svc->runTriggerSimilarityOnly($data['checkId']);
return jsonSuccess($res);
}
public function testcconelast(){
$data = $this->request->post();
$rule = new Validate([
"checkId"=>"require"
]);
if(!$rule->check($data)){
return jsonError($rule->getError());
}
$svc = new PlagiarismService();
$re = $svc->runPollStatus($data['checkId']);
return jsonSuccess($re);
}
/**
* 重试 = 提交一次新查重(保留历史)
*/
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');
}
$out = $this->formatRow($row);
if (!empty($row['raw_response'])) {
$raw = json_decode($row['raw_response'], true);
if (is_array($raw)) {
$out['similarity_meta'] = \app\common\TurnitinService::parseSimilarityReportMeta($raw);
}
}
$out['report_view_hint'] = 'PDF 多为 Match Overview 汇总样式;按来源库(Internet/Publication/Crossref)分类请用 getReportUrl 打开在线报告并切到 All Sources';
return jsonSuccess($out);
}
/**
* 列出某 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]);
}
/**
* 取在线查看 URLTurnitin 一次性会话链接,关闭报告页后勿复用旧 URL
*
* 入参:
* check_id 必填
* editor_id 选填,当前打开报告的编辑 user_id与 viewer_user_id 对应,避免 session 认证失败)
* reuse 选填1=在未过期时复用库内缓存;默认 0每次调用重新向 Turnitin 申请
*/
public function getReportUrl()
{
$checkId = intval($this->request->param('check_id', 0));
$editorId = intval($this->request->param('editor_id', 0));
$reuse = intval($this->request->param('reuse', 0)) === 1;
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']);
}
$viewerContext = [];
if ($editorId > 0) {
$viewerContext['editor_id'] = $editorId;
}
$needRefresh = !$reuse
|| empty($row['view_only_url'])
|| intval($row['view_only_url_expire']) < time() + 60;
$usageHint = '每次打开请先调用本接口获取新链接;勿收藏或再次打开旧链接。请在新标签页打开,并允许 Turnitin 域名 Cookie。';
if ($needRefresh) {
$svc = new PlagiarismService();
$info = $svc->refreshViewerUrlFor($checkId, $viewerContext);
if ($info['url'] === '') {
return jsonError('Turnitin returned empty viewer_url');
}
return jsonSuccess([
'view_only_url' => $info['url'],
'expire' => $info['expire'],
'has_pdf' => !empty($info['local_pdf']),
'viewer_user_id' => $info['viewer_user_id'],
'refreshed' => true,
'usage_hint' => $usageHint,
]);
}
return jsonSuccess([
'view_only_url' => $row['view_only_url'],
'expire' => intval($row['view_only_url_expire']),
'has_pdf' => !empty($row['pdf_local_path']),
'refreshed' => false,
'usage_hint' => $usageHint,
]);
} catch (\Throwable $e) {
if (!empty($row['pdf_local_path'])) {
return jsonSuccess([
'view_only_url' => '',
'expire' => 0,
'has_pdf' => true,
'viewer_error' => $e->getMessage(),
'hint' => '在线报告暂不可用,请使用 downloadReport 下载 PDF',
]);
}
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']),
'local_pdf_url' => $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'],
'check_type' => (string)($r['check_type'] ?? 'full'),
'check_type_label' => $this->checkTypeLabel($r['check_type'] ?? 'full'),
'derived_file_path'=> (string)($r['derived_file_path'] ?? ''),
'trigger_source' => (string)$r['trigger_source'],
'triggered_by' => intval($r['triggered_by']),
'ctime' => intval($r['ctime']),
'utime' => intval($r['utime']),
];
}
private function checkTypeLabel($checkType)
{
$t = strtolower(trim((string) $checkType));
if ($t === 'body_only' || $t === 'body') {
return '正文查重';
}
return '全文查重';
}
private function stateLabel($state)
{
$map = [
0 => '待上传',
1 => '上传中',
2 => '比对中',
3 => '完成',
4 => '失败',
];
return isset($map[$state]) ? $map[$state] : 'unknown';
}
}