diff --git a/application/api/controller/Article.php b/application/api/controller/Article.php index dd23dc6d..6272c181 100644 --- a/application/api/controller/Article.php +++ b/application/api/controller/Article.php @@ -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()); } diff --git a/application/api/controller/Special.php b/application/api/controller/Special.php index 292ab664..744dc367 100644 --- a/application/api/controller/Special.php +++ b/application/api/controller/Special.php @@ -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']), diff --git a/application/command.php b/application/command.php index 7be87dc3..be8e47c8 100644 --- a/application/command.php +++ b/application/command.php @@ -12,4 +12,6 @@ return [ 'app\\command\\ReferenceCheckMqConsume', 'app\\command\\AiWritingRiskMqConsume', + 'app\\command\\RevisionCommentCheckRun', + 'app\\command\\RevisionCommentMqConsume', ]; diff --git a/application/common/mq/RabbitMqConfig.php b/application/common/mq/RabbitMqConfig.php index 64f8e97e..a2b1e631 100644 --- a/application/common/mq/RabbitMqConfig.php +++ b/application/common/mq/RabbitMqConfig.php @@ -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 : []; + } } diff --git a/application/common/service/LLMService.php b/application/common/service/LLMService.php index 3daca96a..50c2fa84 100644 --- a/application/common/service/LLMService.php +++ b/application/common/service/LLMService.php @@ -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; + } } /** diff --git a/application/extra/rabbitmq.php b/application/extra/rabbitmq.php index 5eb8c0ca..b29c1627 100644 --- a/application/extra/rabbitmq.php +++ b/application/extra/rabbitmq.php @@ -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', + ], ];