自动查重

This commit is contained in:
wangjinlei
2026-05-20 11:58:10 +08:00
parent 53e6ddbd9e
commit cfa3f791f4
11 changed files with 938 additions and 58 deletions

View File

@@ -2817,7 +2817,28 @@ class EmailClient extends Base
break;
case 1: // 主编(预留,本期不实现)
break;
case 4: // 作者(预留)
Db::name("article_author")->alias('aa')
->join('t_user u', 'u.email = aa.email', 'inner')
->join("t_article a","a.article_id = aa.article_id","left")
->join('t_user_reviewer_info uri', 'uri.reviewer_id = u.user_id', 'left')
->where('a.journal_id', $journalId)
->where('u.email', '<>', '')
->where('u.unsubscribed', 0);
break;
case 6: //获取往期的青年编委2025年以前的,中国人
$now = strtotime('2025-01-01');
$query = Db::name('user_to_yboard')->alias('y')
->join('t_user u', 'u.user_id = y.user_id', 'inner')
->join('t_user_reviewer_info uri', 'uri.reviewer_id = u.user_id', 'left')
->where('y.journal_id', $journalId)
->where('y.state', 0)
->where('y.start_date', '<=', $now)
->where('uri.country', 'China')
->where('u.email', '<>', '')
->where('u.unsubscribed', 0);
break;//
default:
return [];
}

View File

@@ -12,7 +12,7 @@ use think\Validate;
* 论文查重Turnitin / Crossref Similarity Check控制器。
*
* 触发方式:纯手工(编辑后台点"查重"按钮)。
* 报告策略:在线 viewer URL 临时签名 + PDF 永久落盘 runtime/plagiarism/
* 报告策略:PDF 在 poll 完成时落盘;在线 viewer URL 通过 getReportUrl 按需生成(临时签名)
*
* 主要接口:
* POST submit 触发查重
@@ -37,12 +37,14 @@ class Plagiarism extends Base
* article_id 必填
* file_url 选填;不传则按 article_id 在 t_article_file 找 manuscirpt
* editor_id 选填;触发人 user_id前端拿不到也可以传 0
* check_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('check_type', 'full'));
if ($articleId <= 0) {
return jsonError('article_id required');
@@ -53,8 +55,12 @@ class Plagiarism extends Base
$localPath = $fileUrl !== ''
? $svc->resolveFileUrlToLocal($fileUrl)
: $svc->locateArticleManuscript($articleId);
$checkId = $svc->submit($articleId, $localPath, $editorId, 'manual');
return jsonSuccess(['check_id' => $checkId]);
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());
}
@@ -257,10 +263,14 @@ class Plagiarism extends Base
'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']),
@@ -268,6 +278,15 @@ class Plagiarism extends Base
];
}
private function checkTypeLabel($checkType)
{
$t = strtolower(trim((string) $checkType));
if ($t === 'body_only' || $t === 'body') {
return '正文查重';
}
return '全文查重';
}
private function stateLabel($state)
{
$map = [

View File

@@ -0,0 +1,92 @@
<?php
namespace app\api\controller;
use think\Db;
use think\Validate;
use app\common\UserFieldAiService;
/**
* 用户主领域 AI 总结(写入 t_user_reviewer_info.field_ai
*
* POST startChain 启动链式队列(扫描全部符合条件的用户)
* POST processOne 同步处理单个 user_id调试
* GET preview 预览某用户是否 eligible 及上下文摘要
*/
class UserFieldAi extends Base
{
/**
* 启动链式处理。需 worker: php think queue:work --queue UserFieldAi
*/
public function startChain()
{
$force = intval($this->request->param('force', 0)) === 1;
$delay = max(0, intval($this->request->param('delay', 1)));
$svc = new UserFieldAiService();
$started = $svc->startChain($force, $delay);
return jsonSuccess([
'started' => $started,
'queue' => UserFieldAiService::QUEUE_NAME,
'force' => $force,
'msg' => $started ? 'chain enqueued' : 'no pending users',
]);
}
/**
* 同步处理单个用户(不调队列)。
*/
public function processOne()
{
$userId = intval($this->request->param('user_id', 0));
$force = intval($this->request->param('force', 0)) === 1;
if ($userId <= 0) {
return jsonError('user_id required');
}
$svc = new UserFieldAiService();
$result = $svc->processUser($userId, $force);
if (empty($result['ok'])) {
return jsonError(isset($result['error']) ? $result['error'] : 'failed');
}
return jsonSuccess($result);
}
/**
* 预览:是否满足条件、当前 field_ai 状态。
*/
public function preview()
{
$userId = intval($this->request->param('user_id', 0));
if ($userId <= 0) {
return jsonError('user_id required');
}
$svc = new UserFieldAiService();
$svc->ensureReviewerInfoRow($userId);
$uri = Db::name('user_reviewer_info')->where('reviewer_id', $userId)->find();
return jsonSuccess([
'user_id' => $userId,
'has_articles' => $svc->hasSubmittedArticles($userId),
'profile_complete' => $svc->isReviewerProfileComplete($uri),
'eligible' => $svc->isEligible($userId, $uri),
'field_ai' => $uri ? (string) $uri['field_ai'] : '',
'field_ai_status' => $uri ? intval($uri['field_ai_status']) : 0,
'field_ai_utime' => $uri ? intval($uri['field_ai_utime']) : 0,
'field_ai_status_text' => $this->statusLabel($uri ? intval($uri['field_ai_status']) : 0),
]);
}
private function statusLabel($status)
{
$map = [
UserFieldAiService::STATUS_PENDING => 'pending',
UserFieldAiService::STATUS_DONE => 'done',
UserFieldAiService::STATUS_INSUFFICIENT => 'insufficient',
UserFieldAiService::STATUS_FAILED => 'failed',
];
return isset($map[$status]) ? $map[$status] : 'unknown';
}
}

View File

@@ -23,16 +23,16 @@ class PlagiarismPoll
public function fire(Job $job, $data)
{
// $checkId = isset($data['check_id']) ? intval($data['check_id']) : 0;
// $attempt = isset($data['attempt']) ? intval($data['attempt']) : 1;
//
// if ($checkId <= 0) {
// $job->delete();
// return;
// }
$checkId = isset($data['check_id']) ? intval($data['check_id']) : 0;
$attempt = isset($data['attempt']) ? intval($data['attempt']) : 1;
if ($checkId <= 0) {
$job->delete();
return;
}
$svc = new PlagiarismService();
$svc->log("PlagiarismPoll job is running");
// $svc->runPollStatus($checkId, $attempt);
$svc->runPollStatus($checkId, $attempt);
$job->delete();
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace app\api\job;
use think\queue\Job;
use app\common\UserFieldAiService;
/**
* 链式任务:为单个用户生成 field_ai完成后自动入队下一位用户。
*
* data:
* - user_id 当前处理的用户
* - queue 队列名(默认 UserFieldAi
* - force 1=强制重算
*
* Worker: php think queue:work --queue UserFieldAi
*/
class UserFieldAiFill
{
public function fire(Job $job, $data)
{
$userId = isset($data['user_id']) ? intval($data['user_id']) : 0;
$queue = isset($data['queue']) ? (string) $data['queue'] : UserFieldAiService::QUEUE_NAME;
$force = !empty($data['force']);
$svc = new UserFieldAiService();
if ($userId > 0) {
$svc->processUser($userId, $force);
}
$job->delete();
$delay = max(0, (int) (isset($data['delay']) ? $data['delay'] : 1));
$svc->enqueueNextFieldAi($delay, $queue, $userId, $force);
}
}