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('check_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]); } /** * 取在线查看 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); 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']), ]); } return jsonSuccess([ 'view_only_url' => $row['view_only_url'], 'expire' => intval($row['view_only_url_expire']), 'has_pdf' => !empty($row['pdf_local_path']), ]); } 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'; } }