专刊 作者修回检测是否按照审稿人意见进行修改

This commit is contained in:
wyn
2026-08-03 11:01:33 +08:00
parent 48fdb3bf72
commit be1c0f359a
6 changed files with 200 additions and 23 deletions

View File

@@ -1184,8 +1184,9 @@ class Article extends Base
/**
* @title 判断作者是否已按审稿意见修改(逐条核对最终版)
* @description 拆分审稿意见条目,结合修回稿与回复信,用 LLM 逐条判定 addressed/partial/not可缓存
* @description 异步检测:立即返回检测中,后台执行 LLM请轮询 getRevisionCommentCheckResult
* @param name:article_id type:int require:1 desc:文章id
* @param name:type type:string require:0 desc:不传=综合(意见+回复信+修回稿)manuscript/response/comment 为单侧触发
* @param name:force type:int require:0 desc:1强制重跑忽略缓存
* @param name:mode type:string require:0 desc:full=逐条核对(默认) quick=仅时间门
*
@@ -1196,7 +1197,7 @@ class Article extends Base
{
$data = $this->request->post();
$rule = new Validate([
'article_id' => 'require|number'
'article_id' => 'require|number',
]);
if (!$rule->check($data)) {
return jsonError($rule->getError());
@@ -1208,13 +1209,18 @@ class Article extends Base
try {
$service = new \app\common\service\RevisionCommentMatchService();
$checkType = $service->normalizeCheckType($data['type'] ?? '');
if ($mode === 'quick') {
return jsonSuccess($service->quickCheck($articleId));
$quick = $service->quickCheck($articleId);
$quick['check_type'] = $checkType;
return jsonSuccess($quick);
}
$result = $service->check($articleId, [
'force' => $force,
'use_cache' => $force ? 0 : 1,
'type' => $checkType,
'async' => 1,
]);
$result['mode'] = 'full';
return jsonSuccess($result);
@@ -1224,8 +1230,11 @@ class Article extends Base
}
/**
* @title 获取最近一次逐条修回核对结果
* @title 获取/触发作者按审稿意见修改的逐条核对结果
* @description 有成功结果直接返回;排队/检测中返回 status=1无任务则投递 RabbitMQ。type 不传=综合检测(意见+回复信+修回稿)
* @param name:article_id type:int require:1 desc:文章id
* @param name:type type:string require:0 desc:不传=综合manuscript/response/comment
* @param name:force type:int require:0 desc:1强制重新检测忽略缓存
* @url /api/Article/getRevisionCommentCheckResult
* @method POST
*/
@@ -1233,18 +1242,148 @@ class Article extends Base
{
$data = $this->request->post();
$rule = new Validate([
'article_id' => 'require|number'
'article_id' => 'require|number',
]);
if (!$rule->check($data)) {
return jsonError($rule->getError());
}
$articleId = intval($data['article_id']);
$force = !empty($data['force']) ? 1 : 0;
try {
$service = new \app\common\service\RevisionCommentMatchService();
$checkType = $service->normalizeCheckType($data['type'] ?? '');
if (!$force) {
// 未传 force始终返回该文章该类型「最新一条」检测记录的进度/结果
$latest = $service->getLatestJob($articleId, $checkType, null);
if (!empty($latest)) {
$latestStatus = intval($latest['status'] ?? 0);
if ($latestStatus === \app\common\service\RevisionCommentMatchService::STATUS_SUCCESS) {
$detail = $service->getCheckDetail(intval($latest['id']));
$detail['mode'] = 'full';
return jsonSuccess($detail);
}
$meta = json_decode((string)($latest['result_json'] ?? ''), true);
if (!is_array($meta)) {
$meta = [];
}
if ($latestStatus === \app\common\service\RevisionCommentMatchService::STATUS_FAIL) {
return jsonSuccess([
'article_id' => $articleId,
'check_id' => intval($latest['id']),
'check_no' => (string)($latest['check_no'] ?? ''),
'check_type' => $checkType,
'status' => \app\common\service\RevisionCommentMatchService::STATUS_FAIL,
'status_text' => '检测失败',
'message' => $meta['message'] ?? ($latest['error_msg'] ?? '检测失败'),
'error_msg' => $latest['error_msg'] ?? '',
'mode' => 'full',
]);
}
return jsonSuccess([
'article_id' => $articleId,
'check_id' => intval($latest['id']),
'check_no' => (string)($latest['check_no'] ?? ''),
'check_type' => $checkType,
'check_type_label' => $meta['check_type_label'] ?? '',
'status' => \app\common\service\RevisionCommentMatchService::STATUS_RUNNING,
'status_text' => '检测中',
'message' => $meta['message'] ?? '检测进行中,请稍后轮询',
'mode' => 'full',
]);
}
}
$result = $service->check($articleId, [
'force' => $force ? 1 : 0,
'use_cache' => $force ? 0 : 1,
'type' => $checkType,
'async' => 1,
]);
$result['mode'] = 'full';
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* @title 审稿意见修改检测可视化报告页
* @description 渲染检测报告页面;页面内可触发检测并实时查看逐条结果
* @url /api/Article/revisionCommentCheckReport
* @method GET
*/
public function revisionCommentCheckReport()
{
$apiGetResult = '/index.php/api/Article/getRevisionCommentCheckResult';
$apiGetDetail = '/index.php/api/Article/getRevisionCommentCheckDetail';
$this->assign([
'api_get_result' => $apiGetResult,
'api_get_detail' => $apiGetDetail,
'init_article_id' => intval($this->request->param('article_id', 0)),
'init_check_id' => intval($this->request->param('check_id', 0)),
'init_type' => (string)$this->request->param('type', 'all'),
]);
return $this->fetch('article/revision_comment_check_report');
}
/**
* @title 审稿意见修改检测记录列表
* @description 仅返回某篇文章最新一条检测记录摘要(含明细条数);详情用 getRevisionCommentCheckDetail
* @param name:article_id type:int require:1 desc:文章id
* @param name:type type:string require:0 desc:按检测类型过滤,不传=全部
* @param name:page type:int require:0 desc:页码默认1
* @param name:page_size type:int require:0 desc:每页条数默认20
* @url /api/Article/listRevisionCommentCheck
* @method POST
*/
public function listRevisionCommentCheck()
{
$data = $this->request->post();
$rule = new Validate([
'article_id' => 'require|number',
]);
if (!$rule->check($data)) {
return jsonError($rule->getError());
}
try {
$result = (new \app\common\service\RevisionCommentMatchService())
->getLatestResult(intval($data['article_id']));
if (empty($result)) {
return jsonError('暂无检测结果,请先调用 checkAuthorRevisedByReview');
}
return jsonSuccess($result);
$service = new \app\common\service\RevisionCommentMatchService();
$list = $service->listChecks(
intval($data['article_id']),
intval($data['page'] ?? 1),
intval($data['page_size'] ?? ($data['pageSize'] ?? 20)),
(string)($data['type'] ?? '')
);
return jsonSuccess($list);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* @title 审稿意见修改检测记录详情(含每条结果)
* @description 按 check_id 返回完整检测结果items 来自明细表,逐条含意见文本、质量评估、是否落实、证据与理由
* @param name:check_id type:int require:1 desc:检测记录id
* @url /api/Article/getRevisionCommentCheckDetail
* @method POST
*/
public function getRevisionCommentCheckDetail()
{
$data = $this->request->post();
$rule = new Validate([
'check_id' => 'require|number',
]);
if (!$rule->check($data)) {
return jsonError($rule->getError());
}
try {
$service = new \app\common\service\RevisionCommentMatchService();
$detail = $service->getCheckDetail(intval($data['check_id']));
$detail['mode'] = 'full';
return jsonSuccess($detail);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}

View File

@@ -1021,11 +1021,18 @@ class Special extends Controller
if (!$rule->check($data)) {
return jsonError($rule->getError());
}
//获取分页相关参
$iSize = empty($data['size']) ? 15 : $data['size'];//每页显示条数
$iPage = empty($data['page']) ? 1 : $data['page'];// 当前页码
// 分页参数pageIndex=页码pageSize=每页条
$iPage = isset($data['pageIndex']) ? intval($data['pageIndex']) : 1;
$iSize = isset($data['pageSize']) ? intval($data['pageSize']) : 15;
if ($iPage < 1) {
$iPage = 1;
}
if ($iSize < 1) {
$iSize = 15;
}
$journal_id = $data['journal_id'];
$base_url = Env::get('journal.base_url');
$journal_image_url = Env::get('journal.journal_image_url');
$state = isset($data['state'])?$data['state']:-1;
$gwData = [
'journal_id' => $journal_id,
@@ -1039,9 +1046,12 @@ class Special extends Controller
$specials = isset($res['specials']) ? $res['specials'] : (isset($res['data']['specials']) ? $res['data']['specials'] : []);
foreach ($specials as $k => $v) {
unset($specials[$k]['abstract']);
$specials[$k]['icon'] = $specials[$k]['icon']?$journal_image_url."specialIcon/".$specials[$k]['icon']:"";
}
$re['specials'] = $specials;
$re['count'] = $res['data']['count'];
$re['count'] = isset($res['data']['count']) ? intval($res['data']['count']) : 0;
$re['pageIndex'] = $iPage;
$re['pageSize'] = $iSize;
return jsonSuccess($re);
}
/**
@@ -1109,7 +1119,7 @@ class Special extends Controller
'journal_id' => 'require',
'title' => 'require',
'abstract' => 'require',
'intro' => 'require',
// 'intro' => 'require',
'keywords' => 'require',
'deadline' => 'require',
]);
@@ -1138,7 +1148,7 @@ class Special extends Controller
'journal_id' => $data['journal_id'],
'title' => $data['title'],
// 专刊封面(由前端先调 up_icon_file 拿到路径后传入)
'icon' => isset($data['icon']) ? trim((string)$data['icon']) : '',
'specialIcon' => isset($data['specialIcon']) ? trim((string)$data['specialIcon']) : '',
'abstract' => $data['abstract'],
'intro' => $data['intro'],
'keywords' => $data['keywords'],
@@ -1192,7 +1202,8 @@ class Special extends Controller
$remoteData['interests'] = isset($user['interests']) ? trim($user['interests']) : '';
$remoteData['website'] = trim(!empty($user['website']) ? $user['website'] : (isset($user['g_website']) ? $user['g_website'] : ''));
$remoteData['orcid'] = trim(isset($user['orcid']) ? $user['orcid'] : '');
$remoteData['specialIcon'] = $specialIcon;
// 客座编辑头像
$remoteData['icon'] = $specialIcon;
}
$raw = myPost($base_url . "/api/Special/addSpecial", $remoteData);
@@ -1507,7 +1518,8 @@ class Special extends Controller
'journal_special_id' => intval($data['journal_special_id']),
'title' => trim((string)$data['title']),
'intro' => trim((string)$data['intro']),
'icon' => isset($data['icon']) ? trim((string)$data['icon']) : '',
// 专刊封面
'icon' => isset($data['specialIcon']) ? trim((string)$data['specialIcon']) : '',
'abstract' => trim((string)$data['abstract']),
'keywords' => trim((string)$data['keywords']),
'deadline' => trim((string)$data['deadline']),
@@ -1536,7 +1548,7 @@ class Special extends Controller
$ruleFields = [
'journal_special_id' => 'require|number',
'title' => 'require',
'intro' => 'require',
// 'intro' => 'require',
'abstract' => 'require',
'keywords' => 'require',
'deadline' => 'require',
@@ -1560,7 +1572,8 @@ class Special extends Controller
'journal_special_id' => $specialId,
'title' => trim((string)$data['title']),
'intro' => trim((string)$data['intro']),
'icon' => isset($data['icon']) ? trim((string)$data['icon']) : '',
// 专刊封面
'icon' => isset($data['specialIcon']) ? trim((string)$data['specialIcon']) : '',
'abstract' => trim((string)$data['abstract']),
'keywords' => trim((string)$data['keywords']),
'deadline' => trim((string)$data['deadline']),

View File

@@ -12,4 +12,6 @@
return [
'app\\command\\ReferenceCheckMqConsume',
'app\\command\\AiWritingRiskMqConsume',
'app\\command\\RevisionCommentCheckRun',
'app\\command\\RevisionCommentMqConsume',
];

View File

@@ -27,4 +27,10 @@ class RabbitMqConfig
$cfg = self::get('ai_writing_risk', []);
return is_array($cfg) ? $cfg : [];
}
public static function revisionComment()
{
$cfg = self::get('revision_comment', []);
return is_array($cfg) ? $cfg : [];
}
}

View File

@@ -119,9 +119,10 @@ class LLMService
*
* @param array $messages OpenAI messages
* @param float $temperature
* @param int|null $timeoutSec 可选覆盖超时秒数
* @return string|null 助手回复正文
*/
public function requestChat(array $messages, $temperature = 0)
public function requestChat(array $messages, $temperature = 0, $timeoutSec = null)
{
if ($this->url === '' || $this->model === '') {
\think\Log::warning('LLM requestChat: url or model not configured');
@@ -132,7 +133,15 @@ class LLMService
'temperature' => $temperature,
'messages' => $messages,
];
return $this->postChat($payload);
$oldTimeout = $this->timeout;
if ($timeoutSec !== null && intval($timeoutSec) > 0) {
$this->timeout = max(30, intval($timeoutSec));
}
try {
return $this->postChat($payload);
} finally {
$this->timeout = $oldTimeout;
}
}
/**

View File

@@ -20,4 +20,12 @@ return [
'dlq' => 'ai_writing_risk.task.dlq',
'route_key' => 'task.start',
],
// 作者按审稿意见修改检测串行消费prefetch=1失败 ack 不重试
'revision_comment' => [
'exchange' => 'revision_comment',
'queue' => 'revision_comment.task',
'dlq' => 'revision_comment.task.dlq',
'route_key' => 'check.start',
],
];