request->post() : $aParam; //必填值验证 $iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id']; if(empty($iPReferId)){ return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']); } $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; $aRefer = Db::name('production_article_refer')->where($aWhere)->find(); if(empty($aRefer)){ return json_encode(['status' => 4,'msg' => 'Reference is empty']); } //获取文章信息 $aParam['p_article_id'] = $aRefer['p_article_id']; $aArticle = $this->getArticle($aParam); $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; if($iStatus != 1){ return json_encode($aArticle); } $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; if(empty($aArticle)){ return json_encode(['status' => 3,'msg' => 'The article does not exist']); } //获取参考文献信息作者名.文章题目.期刊名缩写.年卷页.Available at: //https://doi.org/xxxxx //作者 $sData = $aRefer['refer_frag']; if($aRefer['refer_type'] == 'journal'){ if(!empty($aRefer['doilink'])){ $sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.'); if(!empty($sAuthor)){ $aAuthor = explode(',', $sAuthor); if(count($aAuthor) > 3){ $sAuthor = implode(',', array_slice($aAuthor, 0,3)); $sAuthor .= ', et al'; } if(count($aAuthor) <= 3 ){ $sAuthor = implode(',', $aAuthor); } } //文章标题 $sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.'); //期刊名缩写 $sJoura = empty($aRefer['joura']) ? '' : trim(trim($aRefer['joura']),'.'); //年卷页 $sDateno = empty($aRefer['dateno']) ? '' : trim(trim($aRefer['dateno']),'.'); //DOI $sDoilink = empty($aRefer['doilink']) ? '' : trim($aRefer['doilink']); if(!empty($sDoilink)){ $sDoilink = strpos($sDoilink ,"http")===false ? "https://doi.org/".$sDoilink : $sDoilink; $sDoilink = str_replace('http://doi.org/', 'https://doi.org/', $sDoilink); } $sReferDoi = empty($aRefer['refer_doi']) ? '' : trim($aRefer['refer_doi']); if(!empty($sReferDoi)){ $sReferDoi = strpos($sReferDoi ,"http")===false ? "https://doi.org/".$sReferDoi : $sReferDoi; $sReferDoi = str_replace('http://doi.org/', 'https://doi.org/', $sReferDoi); } $sDoilink = empty($sDoilink) ? $sReferDoi : $sDoilink; $sData = $sAuthor.'.'.$sTitle.'.'.$sJoura.'.'.$sDateno.".Available at:\n".$sDoilink; } } if($aRefer['refer_type'] == 'book'){ $sAuthor = empty($aRefer['author']) ? '' : trim(trim($aRefer['author']),'.'); if(!empty($sAuthor)){ $aAuthor = explode(',', $sAuthor); if(count($aAuthor) > 3){ $sAuthor = implode(',', array_slice($aAuthor, 0,3)); $sAuthor .= ', et al'; } if(count($aAuthor) <= 3 ){ $sAuthor = implode(',', $aAuthor); } } //文章标题 $sTitle = empty($aRefer['title']) ? '' : trim(trim($aRefer['title']),'.'); //期刊名缩写 $sJoura = empty($aRefer['joura']) ? '' : trim(trim($aRefer['joura']),'.'); //年卷页 $sDateno = empty($aRefer['dateno']) ? '' : trim(trim($aRefer['dateno']),'.'); //DOI $sDoilink = empty($aRefer['isbn']) ? '' : trim($aRefer['isbn']); $sData = $sAuthor.'.'.$sTitle.'.'.$sJoura.'.'.$sDateno.".Available at:\n".$sDoilink; } $aRefer['deal_content'] = $sData; return json_encode(['status' => 1,'msg' => 'success','data' => $aRefer]); } /** * 参考文献鉴别:正文引用上下文 + PubMed/Crossref + 大模型向量相似度 * 参数:p_refer_id(必填) * 环境变量(可选):citation_chat_url、citation_chat_model、citation_chat_api_key、citation_chat_timeout、crossref_mailto、pubmed_email */ public function checkCitationRelevance($aParam = []) { $aParam = empty($aParam) ? $this->request->post() : $aParam; $pReferId = intval(isset($aParam['p_refer_id']) ? $aParam['p_refer_id'] : 0); if (!$pReferId) { return jsonError('p_refer_id is required'); } $refer = Db::name('production_article_refer') ->where('p_refer_id', $pReferId) ->where('state', 0) ->find(); if (empty($refer)) { return jsonError('Reference not found'); } $aArticle = $this->getArticle(['p_article_id' => $refer['p_article_id']]); $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; if ($iStatus != 1) { return json_encode($aArticle); } $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; if (empty($aArticle['article_id'])) { return jsonError('Article not found'); } $articleId = intval($aArticle['article_id']); $mains = Db::name('article_main') ->where('article_id', $articleId) ->whereIn('state', [0, 2]) ->order('sort asc') ->select(); if (empty($mains)) { return jsonError('article_main is empty'); } $citationMark = intval($refer['index']) + 1; $context = $this->extractCitationContextFromMains($mains, $citationMark); if ($context === '') { return jsonError('Citation context not found in article_main for mark [' . $citationMark . ']'); } $apiKey = trim((string)Env::get('citation_chat_api_key', '')); // if ($apiKey === '') { // return jsonError('Please set env citation_chat_api_key for embedding via chat'); // } $config = [ 'chat_url' => trim((string)Env::get('citation_chat_url', 'http://chat.taimed.cn/v1/chat/completions')), 'chat_model' => trim((string)Env::get('citation_chat_model', 'DeepSeek-Coder-V2-Instruct')), 'timeout' => max(60, intval(Env::get('citation_chat_timeout', 180))), 'embedding_dim' => max(32, intval(Env::get('citation_embedding_dim', 256))), 'embedding_headers' => [ 'Authorization: Bearer ' . $apiKey, ], ]; $pubmed = new PubmedService([ 'email' => trim((string)Env::get('pubmed_email', '')), 'tool' => trim((string)Env::get('pubmed_tool', 'tmrjournals')), ]); $crossref = new CrossrefService([ 'mailto' => trim((string)Env::get('crossref_mailto', '')), ]); $svc = new CitationRelevanceService($pubmed, $crossref, $config); $qc = $svc->checkOne($context, $refer, []); return jsonSuccess([ 'p_refer_id' => $pReferId, 'citation_mark' => $citationMark, 'refer_index' => intval($refer['index']), 'context' => $context, 'problem_flag' => $qc['problem_flag'] ?? '', 'problem_reason' => $qc['problem_reason'] ?? '', 'relevance_flag' => $qc['relevance_flag'] ?? '', 'relevance_score'=> $qc['relevance_score'] ?? 0, 'reason' => $qc['reason'] ?? '', 'pubmed' => $qc['pubmed'] ?? [], ]); } /** * 提交参考文献鉴别到队列(异步) * 参数:p_refer_id */ public function checkCitationRelevanceQueue($aParam = []) { $aParam = empty($aParam) ? $this->request->post() : $aParam; $pReferId = intval(isset($aParam['p_refer_id']) ? $aParam['p_refer_id'] : 0); if (!$pReferId) { return jsonError('p_refer_id is required'); } $redisKey = 'queue_job:app\api\job\CitationRelevanceQueue:' . $pReferId; $queueRedis = QueueRedis::getInstance(); $status = $queueRedis->getJobStatus($redisKey); // 若已完成,直接返回已完成状态,前端可立刻去拉结果 if ($status === 'completed') { return jsonSuccess([ 'queue_key' => $redisKey, 'status' => 'completed', ]); } // 若正在处理,返回 processing if ($status === 'processing') { return jsonSuccess([ 'queue_key' => $redisKey, 'status' => 'processing', ]); } // 推送新任务到队列 $queueId = Queue::push('app\api\job\CitationRelevanceQueue@fire', [ 'p_refer_id' => $pReferId, ], 'CitationRelevanceQueue'); if (!$queueId) { return jsonError('queue push failed'); } return jsonSuccess([ 'queue_key' => $redisKey, 'status' => 'queued', 'queue_id' => $queueId, ]); } /** * 轮询获取参考文献鉴别结果 * 参数:p_refer_id */ public function getCitationRelevanceResult($aParam = []) { $aParam = empty($aParam) ? $this->request->post() : $aParam; $pReferId = intval(isset($aParam['p_refer_id']) ? $aParam['p_refer_id'] : 0); if (!$pReferId) { return jsonError('p_refer_id is required'); } $redisKey = 'queue_job:app\api\job\CitationRelevanceQueue:' . $pReferId; $queueRedis = QueueRedis::getInstance(); $status = $queueRedis->getJobStatus($redisKey); if ($status === null || $status === false) { return jsonSuccess([ 'status' => 'not_found', ]); } if ($status === 'processing') { return jsonSuccess([ 'status' => 'processing', ]); } if ($status === 'failed') { $raw = $queueRedis->getRedisValue($redisKey . ':result'); $data = []; if (is_string($raw) && $raw !== '') { $decoded = json_decode($raw, true); if (is_array($decoded)) { $data = $decoded; } } return jsonSuccess([ 'status' => 'failed', 'data' => $data, ]); } // completed:从 Redis 取出完整结果返回 $raw = $queueRedis->getRedisValue($redisKey . ':result'); if (!is_string($raw) || $raw === '') { return jsonSuccess([ 'status' => 'completed', 'data' => [], ]); } $decoded = json_decode($raw, true); if (!is_array($decoded)) { $decoded = ['status' => 1, 'msg' => 'success', 'data' => []]; } return jsonSuccess([ 'status' => 'completed', 'data' => $decoded, ]); } /** * 从 t_article_main 拼接正文,按 [n] 定位句子并取前后各 1 句作为上下文 */ private function extractCitationContextFromMains(array $mains, int $citationMark): string { if ($citationMark <= 0) { return ''; } $chunks = []; foreach ($mains as $row) { $text = isset($row['content']) ? (string)$row['content'] : ''; if ($text === '') { continue; } $text = preg_replace('/<\s*\/?\s*blue[^>]*>/i', '', $text); $text = strip_tags($text); $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace('/\s+/u', ' ', trim($text)); if ($text !== '') { $chunks[] = $text; } } $fullText = implode("\n", $chunks); if ($fullText === '') { return ''; } $sentences = $this->splitEnglishSentences($fullText); $pattern = '/\[' . preg_quote((string)$citationMark, '/') . '\]/'; foreach ($sentences as $si => $sent) { if (!preg_match($pattern, $sent)) { continue; } $start = max(0, $si - 1); $end = min(count($sentences) - 1, $si + 1); $ctx = implode(' ', array_slice($sentences, $start, $end - $start + 1)); return trim(preg_replace('/\s+/u', ' ', $ctx)); } return ''; } private function splitEnglishSentences(string $text): array { $text = trim($text); if ($text === '') { return []; } $text = preg_replace('/\s+/u', ' ', $text); $parts = preg_split('/(?<=[\.\?\!])\s+/', $text); $out = []; foreach ($parts as $p) { $p = trim((string)$p); if ($p !== '') { $out[] = $p; } } return $out; } /** * 根据 t_production_article_refer 构建:正文引用序号([n] 中的 n)=> p_refer_id * 规则与 Production::convertReferencesToLatex 一致:正文序号 = index + 1 * * @param int $pArticleId production 侧 p_article_id * @return array 例如 [1 => 101, 2 => 102] */ private function buildCitationNumberToPReferIdMap(int $pArticleId): array { if ($pArticleId <= 0) { return []; } $rows = Db::name('production_article_refer') ->where('p_article_id', $pArticleId) ->where('state', 0) ->field('p_refer_id,index') ->order('index asc') ->select(); $map = []; foreach ($rows as $row) { $n = intval($row['index']) + 1; if ($n > 0 && !empty($row['p_refer_id'])) { $map[$n] = intval($row['p_refer_id']); } } return $map; } /** * 解析括号内引用串(如 1,2 / 3-5 / 1,3-5),展开为正文引用序号列表(保留顺序,不去重) * * @param string $referencePart 不含 [] 的内层,已规范化为英文逗号与普通连字符 * @return int[] */ private function expandCitationBracketInner(string $referencePart): array { $referencePart = trim($referencePart); if ($referencePart === '') { return []; } // 与 Base::blueIntegerChange 一致:范围可用 ASCII -、en dash –、em dash —、minus − 等 $referencePart = str_replace( [',', '–', '—', '−', '‐', '‑'], [',', '-', '-', '-', '-', '-'], $referencePart ); $out = []; $segments = preg_split('/\s*,\s*/', $referencePart); foreach ($segments as $seg) { $seg = trim((string)$seg); if ($seg === '') { continue; } $seg = str_replace(['–', '—', '−', '‐', '‑'], '-', $seg); if (preg_match('/^(\d+)\s*-\s*(\d+)$/', $seg, $m)) { $a = intval($m[1]); $b = intval($m[2]); if ($a > $b) { $t = $a; $a = $b; $b = $t; } for ($i = $a; $i <= $b; $i++) { $out[] = $i; } } else { $out[] = intval($seg); } } return $out; } /** * 纯文本/HTML 片段:[n](需传入已构建的 map) */ private function applyBlueCitationsToMycite(string $content, array $map): string { if ($map === [] || $content === '') { return $content; } return preg_replace_callback( '/(?:<\s*blue[^>]*>)?\[([^\]]+)\](?:<\/\s*blue\s*>)?/iu', function (array $matches) use ($map): string { $inner = trim((string)$matches[1]); if ($inner === '') { return $matches[0]; } $innerNorm = str_replace( [',', '–', '—', '−', '‐', '‑'], [',', '-', '-', '-', '-', '-'], $inner ); $innerNorm = preg_replace('/\s+/u', ' ', trim($innerNorm)); if (!preg_match('/^[\d\s,\-]+$/u', $innerNorm)) { return $matches[0]; } $nums = $this->expandCitationBracketInner($innerNorm); if ($nums === []) { return $matches[0]; } $ids = []; foreach ($nums as $n) { if ($n <= 0) { continue; } if (empty($map[$n])) { return $matches[0]; } $ids[] = (string)intval($map[$n]); } if ($ids === []) { return $matches[0]; } return ''; }, $content ); } /** * table_data 存 JSON:递归替换各字符串字段内的引用;非法 JSON 时退回整串替换。 * html_data 不在此处理(由业务单独维护)。 */ private function rewriteTableDataJsonCitationsToMycite(string $tableDataJson, int $pArticleId): string { $tableDataJson = trim((string)$tableDataJson); if ($tableDataJson === '') { return ''; } $map = $this->buildCitationNumberToPReferIdMap($pArticleId); if ($map === []) { return $tableDataJson; } $decoded = $this->decodeTableDataJsonToArray($tableDataJson); if ($decoded === null) { return $this->applyBlueCitationsToMycite($tableDataJson, $map); } $walked = $this->rewriteBlueCitationsInJsonNode($decoded, $map); $flags = JSON_UNESCAPED_UNICODE; if (defined('JSON_UNESCAPED_SLASHES')) { $flags |= JSON_UNESCAPED_SLASHES; } return json_encode($walked, $flags); } /** * @param mixed $node * @return mixed */ private function rewriteBlueCitationsInJsonNode($node, array $map) { if (is_string($node)) { return $this->applyBlueCitationsToMycite($node, $map); } if (is_array($node)) { $out = []; foreach ($node as $k => $v) { $out[$k] = $this->rewriteBlueCitationsInJsonNode($v, $map); } return $out; } return $node; } /** * 将正文 HTML 中的 [n](及 [1,2]、[2-4] 等)替换为 * 找不到对应参考文献时保留原 […],避免丢内容。 * * @param string $content article_main.content 等 HTML 片段 * @param int $pArticleId t_production_article_refer.p_article_id */ public function rewriteMainContentCitationsToMycite(string $content, int $pArticleId) { $map = $this->buildCitationNumberToPReferIdMap($pArticleId); if ($map === []) { return $content; } return $this->applyBlueCitationsToMycite($content, $map); } /** * 接口:将 content 中的 blue 引用替换为 mycite(需传 p_article_id) */ public function convertMainCitationsToMycite() { $data = $this->request->post(); $rule = new Validate([ "am_id"=>"require", "article_id"=>"require" ]); if(!$rule->check($data)){ return jsonError($rule->getError()); } $main_info = $this->article_main_obj->where("am_id",$data['am_id'])->find(); $p_info = $this->production_article_obj->where("article_id",$data['article_id'])->where("state",0)->find(); if(!$p_info||!$main_info){ return jsonError('production_article_id not found'); } $pArticleId = $p_info['p_article_id']; if (intval($main_info['type'] ?? 0) === 2) { $amtId = intval($main_info['amt_id'] ?? 0); if ($amtId <= 0) { return jsonError('amt_id is empty for table main row'); } $tbl = Db::name('article_main_table') ->where('amt_id', $amtId) ->where('article_id', $data['article_id']) ->where('state', 0) ->find(); if (empty($tbl)) { return jsonError('article_main_table not found'); } $out = []; $td = (string)($tbl['table_data'] ?? ''); $out['table_data'] = $td === '' ? '' : $this->rewriteTableDataJsonCitationsToMycite($td, $pArticleId); foreach (['title', 'note'] as $f) { $raw = (string)($tbl[$f] ?? ''); $out[$f] = $raw === '' ? '' : $this->rewriteMainContentCitationsToMycite($raw, $pArticleId); } return jsonSuccess([ 'target' => 'article_main_table', 'amt_id' => $amtId, 'fields' => $out, ]); } $content = $main_info['content']; $out = $this->rewriteMainContentCitationsToMycite($content, $pArticleId); return jsonSuccess(['content' => $out]); } /** * 批量处理并回写: * - type=0:t_article_main.content * - type=2:t_article_main_table(table_data 为 JSON 递归替换;title/note 为纯文本;不修改 html_data) * * 参数: * - article_id (必填) * - type (可选):不传则处理正文+表格(type in 0,2);传具体数字则只处理该 type * - dry_run (可选):1=只预览不落库 */ public function convertArticleMainCitationsToMycite() { $aParam = $this->request->post(); $rule = new Validate([ "article_id"=>"require" ]); if(!$rule->check($aParam)){ return jsonError($rule->getError()); } $dryRun = intval($aParam['dry_run'] ?? 0) === 1; $p_info = $this->production_article_obj->where('article_id', $aParam['article_id'])->where('state', 0)->find(); if (empty($p_info)) { return jsonError('production_article not found'); } $pArticleId = $p_info['p_article_id']; $query = Db::name('article_main') ->where('article_id', $aParam['article_id']) ->whereIn('state', [0, 2]) ->order('sort asc'); if (isset($aParam['type']) && $aParam['type'] !== '' && $aParam['type'] !== null) { $query->where('type', intval($aParam['type'])); } else { // 默认同时处理正文段落与表格占位行(避免原先默认 type=0 漏掉 type=2) $query->whereIn('type', [0, 2]); } $mains = $query->field('am_id,content,type,sort,amt_id')->select(); if (empty($mains)) { return jsonError('article_main is empty'); } $changed = 0; $preview = []; Db::startTrans(); try { foreach ($mains as $row) { $amId = intval($row['am_id']); $mainType = intval($row['type'] ?? 0); if ($mainType === 2) { $amtId = intval($row['amt_id'] ?? 0); if ($amtId <= 0) { continue; } $tbl = Db::name('article_main_table') ->where('amt_id', $amtId) ->where('article_id', $aParam['article_id']) ->where('state', 0) ->find(); if (empty($tbl)) { continue; } $updateTbl = []; $fieldPreview = []; $oldTd = (string)($tbl['table_data'] ?? ''); if ($oldTd !== '') { $newTd = $this->rewriteTableDataJsonCitationsToMycite($oldTd, $pArticleId); if ($newTd !== $oldTd) { $updateTbl['table_data'] = $newTd; if (count($fieldPreview) < 4) { $fieldPreview['table_data'] = [ 'before' => $oldTd, 'after' => $newTd, ]; } } } foreach (['title', 'note'] as $f) { $old = (string)($tbl[$f] ?? ''); if ($old === '') { continue; } $new = $this->rewriteMainContentCitationsToMycite($old, $pArticleId); if ($new !== $old) { $updateTbl[$f] = $new; if (count($fieldPreview) < 4) { $fieldPreview[$f] = [ 'before' => $old, 'after' => $new, ]; } } } if ($updateTbl === []) { continue; } $changed++; if (count($preview) < 3) { $preview[] = [ 'am_id' => $amId, 'amt_id' => $amtId, 'type' => 2, 'sort' => intval($row['sort'] ?? 0), 'target' => 'article_main_table', 'fields' => $fieldPreview, ]; } if (!$dryRun) { Db::name('article_main_table') ->where('amt_id', $amtId) ->limit(1) ->update($updateTbl); } continue; } // 正文等:写回 article_main.content $old = (string)($row['content'] ?? ''); if ($old === '') { continue; } $new = $this->rewriteMainContentCitationsToMycite($old, $pArticleId); if ($new === $old) { continue; } $changed++; if (count($preview) < 3) { $preview[] = [ 'am_id' => $amId, 'type' => $mainType, 'sort' => intval($row['sort'] ?? 0), 'target' => 'article_main', 'before' => $old, 'after' => $new, ]; } if (!$dryRun) { Db::name('article_main') ->where('am_id', $amId) ->limit(1) ->update([ 'content' => $new, ]); } } if ($dryRun) { Db::rollback(); } else { Db::commit(); } } catch (\Exception $e) { Db::rollback(); return jsonError('convert failed: ' . $e->getMessage()); } $this->markUnusedReferencesForArticle(intval($aParam['article_id'])); return jsonSuccess([ 'article_id' => $aParam['article_id'], 'p_article_id' => $pArticleId, 'dry_run' => $dryRun ? 1 : 0, 'total' => count($mains), 'changed' => $changed, 'preview' => $preview, ]); } /** * 批量更新 production_article_refer * * 参数: * - list(必填):数组,每项至少含 p_refer_id,其余为可更新字段 * - p_article_id(可选):若传则校验每条记录均属该生产文章,防止误改 * * 可更新字段(白名单):author,title,joura,dateno,doilink,doi,refer_doi,refer_content,refer_frag, * refer_type,isbn,index,is_change,is_ai_check,cs,is_ja,article_id */ public function batchUpdateRefer($aParam = []) { $aParam = empty($aParam) ? $this->request->post() : $aParam; $list = isset($aParam['list']) ? $aParam['list'] : (isset($aParam['refer_list']) ? $aParam['refer_list'] : null); if (is_string($list)) { $list = json_decode($list, true); } if (!is_array($list) || $list === []) { return jsonError('list is required and must be a non-empty array'); } $pArticleIdCheck = isset($aParam['p_article_id']) ? intval($aParam['p_article_id']) : 0; $allowed = [ 'author', 'title', 'joura', 'dateno', 'doilink', 'doi', 'refer_doi', 'refer_content', 'refer_frag', 'refer_type', 'isbn', 'index', 'is_change', 'is_ai_check', 'cs', 'is_ja', 'article_id', ]; $ok = 0; $failed = []; Db::startTrans(); try { foreach ($list as $idx => $row) { if (!is_array($row)) { $failed[] = ['index' => $idx, 'msg' => 'item must be object']; continue; } $pReferId = intval(isset($row['p_refer_id']) ? $row['p_refer_id'] : 0); if ($pReferId <= 0) { $failed[] = ['index' => $idx, 'msg' => 'p_refer_id is required']; continue; } $where = ['p_refer_id' => $pReferId, 'state' => 0]; $exist = Db::name('production_article_refer')->where($where)->find(); if (empty($exist)) { $failed[] = ['p_refer_id' => $pReferId, 'msg' => 'reference not found or state!=0']; continue; } if ($pArticleIdCheck > 0 && intval($exist['p_article_id']) !== $pArticleIdCheck) { $failed[] = ['p_refer_id' => $pReferId, 'msg' => 'p_article_id mismatch']; continue; } $update = []; foreach ($allowed as $field) { if (array_key_exists($field, $row)) { $update[$field] = $row[$field]; } } if ($update === []) { $failed[] = ['p_refer_id' => $pReferId, 'msg' => 'no updatable fields']; continue; } $update['update_time'] = time(); if (!isset($update['is_change'])) { $update['is_change'] = 1; } $result = Db::name('production_article_refer')->where($where)->limit(1)->update($update); if ($result === false) { $failed[] = ['p_refer_id' => $pReferId, 'msg' => 'update failed']; continue; } $ok++; } Db::commit(); } catch (\Exception $e) { Db::rollback(); return jsonError('batch update failed: ' . $e->getMessage()); } return jsonSuccess([ 'updated' => $ok, 'failed' => $failed, 'total' => count($list), ]); } /** * 修改参考文献的信息 * @param p_refer_id 主键ID */ public function modify($aParam = []){ //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; //必填值验证 $iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id']; if(empty($iPReferId)){ return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']); } $sContent = empty($aParam['content']) ? '' : $aParam['content']; if(empty($sContent)){ return json_encode(['status' => 2,'msg' => 'Please enter the modification content']); } if(!is_string($sContent)){ return json_encode(['status' => 2,'msg' => 'The content format is incorrect']); } //获取参考文献信息 $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; $aRefer = Db::name('production_article_refer')->where($aWhere)->find(); if(empty($aRefer)){ return json_encode(['status' => 4,'msg' => 'Reference is empty']); } //获取文章信息 $aParam['p_article_id'] = $aRefer['p_article_id']; $aArticle = $this->getArticle($aParam); $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; if($iStatus != 1){ return json_encode($aArticle); } $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; if(empty($aArticle)){ return json_encode(['status' => 3,'msg' => 'The article does not exist']); } //数据处理 $aContent = json_decode($this->dealContent(['content' => $sContent]),true); $aUpdate = empty($aContent['data']) ? [] : $aContent['data']; if(empty($aUpdate)){ return json_encode(['status' => 5,'msg' => 'The content format is incorrect']); } $aUpdate['refer_content'] = $sContent; $aUpdate['is_change'] = 1; $aUpdate['update_time'] = time(); //更新数据 $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; $result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate); if($result === false){ return json_encode(['status' => 6,'msg' => 'Update failed']); } return json_encode(['status' => 1,'msg' => 'success']); } /** * 处理参考文献的信息 * @param p_refer_id 主键ID */ public function dealContent($aParam = []){ //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; //必填验证 $sContent = empty($aParam['content']) ? '' : $aParam['content']; if(empty($sContent)){ return json_encode(['status' => 2,'msg' => 'Please enter the modification content']); } if(!is_string($sContent)){ return json_encode(['status' => 2,'msg' => 'The content format is incorrect']); } $sContent = str_replace(['?','?'], '.', $sContent); $aContent = explode('.', $sContent); $aUpdate = []; if(count($aContent) > 1){ $aField = [0 => 'author',1 => 'title', 2 => 'joura',3 => 'dateno']; $aStart = array_slice($aContent, 0,4); foreach ($aStart as $key => $value) { if(empty($value)){ continue; } $aUpdate[$aField[$key]] = trim(trim($value),'.'); } $sDoi = empty(array_slice($aContent, 4)) ? '' : implode('.', array_slice($aContent, 4)); // 匹配http/https开头的URL正则 $urlPattern = '/https?:\/\/[^\s<>"]+|http?:\/\/[^\s<>"]+/i'; // 执行匹配(preg_match_all返回所有结果) preg_match_all($urlPattern, $sDoi, $matches); if(!empty($matches[0])){ $sDoi = implode(',', array_unique($matches[0])); } if(empty($sDoi)){ return json_encode(['status' => 4,'msg' => 'Reference DOI is empty']); } $sDoi = trim(trim($sDoi),':'); $sDoi = strpos($sDoi ,"http")===false ? "https://doi.org/".$sDoi : $sDoi; $sDoi = str_replace('http://doi.org/', 'https://doi.org/', $sDoi); $aUpdate['doilink'] = $sDoi; //$doiPattern = '/10\.\d{4,9}\/[^\s\/?#&=]+/i'; $doiPattern = '/\b10\.\d+(?:\.\d+)*\/[^\s?#&=]+/i'; if (preg_match($doiPattern, $sDoi, $matches)) { $aUpdate['doi'] = $matches[0]; $aUpdate['doilink'] = 'https://doi.org/'.''.$aUpdate['doi']; }else{ $aUpdate['doi'] = $sDoi; } if(!empty($aUpdate['author'])){ $aUpdate['author'] = trim(trim($aUpdate['author'])).'.'; } } return json_encode(['status' => 1,'msg' => 'success','data' => $aUpdate]); } /** * 获取文章信息 */ private function getArticle($aParam = []){ //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; //获取生产文章信息 $iPArticleId = empty($aParam['p_article_id']) ? 0 : $aParam['p_article_id']; if(empty($iPArticleId)){ return ['status' => 2,'msg' => 'Please select the article to query']; } $aWhere = ['p_article_id' => $iPArticleId,'state' => ['in',[0,2]]]; $aProductionArticle = Db::name('production_article')->field('article_id')->where($aWhere)->find(); $iArticleId = empty($aProductionArticle['article_id']) ? 0 : $aProductionArticle['article_id']; if(empty($iArticleId)) { return ['status' => 2,'msg' => 'No articles found']; } //查询条件 $aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]]; $aArticle = Db::name('article')->field('article_id')->where($aWhere)->find(); if(empty($aArticle)){ return ['status' => 3,'msg' => 'The article does not exist or has not entered the editorial reference status']; } $aArticle['p_article_id'] = $iPArticleId; return ['status' => 1,'msg' => 'success','data' => $aArticle]; } /** * AI检测 */ public function checkByAi($aParam = []){ //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; //获取文章信息 $aArticle = $this->getArticle($aParam); $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; if($iStatus != 1){ return json_encode($aArticle); } $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; if(empty($aArticle)){ return json_encode(['status' => 3,'msg' => 'The article does not exist']); } //查询参考文献信息 $aWhere = ['p_article_id' => $aArticle['p_article_id'],'state' => 0,'doilink' => '']; $aRefer = Db::name('production_article_refer')->field('p_refer_id,p_article_id,refer_type,refer_content,doilink,refer_doi')->where($aWhere)->select(); if(empty($aRefer)){ return json_encode(['status' => 4,'msg' => 'No reference information found']); } //数据处理 foreach ($aRefer as $key => $value) { if(empty($value['refer_doi'])){ continue; } if($value['refer_doi'] == 'Not Available'){ continue; } if($value['refer_type'] == 'journal' && !empty($value['doilink'])){ continue; } if($value['refer_type'] == 'book' && !empty($value['isbn'])){ continue; } //写入获取参考文献详情队列 \think\Queue::push('app\api\job\AiCheckReferByDoi@fire',$value,'AiCheckReferByDoi'); } return json_encode(['status' => 1,'msg' => 'Successfully joined the AI inspection DOI queue']); } /** * 获取结果 */ public function getCheckByAiResult($aParam = []){ //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; //必填值验证 $iPReferId = empty($aParam['p_refer_id']) ? '' : $aParam['p_refer_id']; if(empty($iPReferId)){ return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']); } //获取参考文献信息 $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; $aRefer = Db::name('production_article_refer')->field('p_refer_id,p_article_id,refer_type,refer_content,doilink,refer_doi,state,dateno')->where($aWhere)->find(); if(empty($aRefer)){ return json_encode(['status' => 4,'msg' => 'Reference is empty'.json_encode($aParam)]); } if(empty($aRefer['refer_doi'])){ return json_encode(['status' => 4,'msg' => 'Reference DOI is empty'.json_encode($aParam)]); } if($aRefer['refer_type'] == 'journal' && !empty($aRefer['doilink'])){ $aDateno = empty($aRefer['dateno']) ? [] : explode(':', $aRefer['dateno']); if(count($aDateno) > 1){ return json_encode(['status' => 4,'msg' => 'No need to parse again-journal'.json_encode($aParam)]); } } if($aRefer['refer_type'] == 'book' && !empty($aRefer['isbn'])){ return json_encode(['status' => 4,'msg' => 'No need to parse again-book'.json_encode($aParam)]); } //获取文章信息 $aParam['p_article_id'] = $aRefer['p_article_id']; $aArticle = $this->getArticle($aParam); $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status']; if($iStatus != 1){ return json_encode($aArticle); } $aArticle = empty($aArticle['data']) ? [] : $aArticle['data']; if(empty($aArticle)){ return json_encode(['status' => 3,'msg' => 'The article does not exist']); } //请求AI获取结果 $aResult = $this->curlOpenAIByDoi(['doi' => $aRefer['refer_doi']]); $iStatus = empty($aResult['status']) ? 0 : $aResult['status']; $sMsg = empty($aResult['msg']) ? 'The DOI number AI did not find any relevant information' : $aResult['msg']; if($iStatus != 1){ return json_encode(['status' => 4,'msg' => $sMsg]); } $aData = empty($aResult['data']) ? [] : $aResult['data']; if(empty($aData)){ return json_encode(['status' => 5,'msg' => 'AI obtains empty data']); } //写入日志 $aLog = []; $aLog['content'] = json_encode($aResult); $aLog['update_time'] = time(); $aLog['p_refer_id'] = $iPReferId; $iLogId = Db::name('production_article_refer_ai')->insertGetId($aLog); $iIsAiCheck = empty($aData['is_ai_check']) ? 2 : $aData['is_ai_check']; if($iIsAiCheck != 1){//AI未检测到信息 return json_encode(['status' => 6,'msg' => 'AI did not find any information'.json_encode($aParam)]); } //数据处理入库 $aField = ['author','title','joura','dateno','doilink']; foreach ($aField as $key => $value) { if(empty($aData[$value])){ continue; } if($value == 'author'){ $aUpdate['author'] = implode(',', $aData['author']); // $aUpdate['author'] = str_replace('et al.', '', $aUpdate['author']); }else{ $aUpdate[$value] = $aData[$value]; } } if(empty($aUpdate)){ return json_encode(['status' => 6,'msg' => 'Update data to empty'.json_encode($aData)]); } if($aRefer['refer_type'] == 'other'){ $aUpdate['refer_type'] = 'journal'; } if($aRefer['refer_type'] == 'book' && !empty($aUpdate['doilink'])){ $aUpdate['refer_type'] = $aUpdate['doilink']; unset($aUpdate['doilink']); } $aLog = $aUpdate; $aUpdate['is_change'] = 1; $aUpdate['is_ai_check'] = 1; $aUpdate['cs'] = 1; $aUpdate['update_time'] = time(); Db::startTrans(); //更新数据 $aWhere = ['p_refer_id' => $iPReferId,'state' => 0]; $result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate); if($result === false){ return json_encode(['status' => 6,'msg' => 'Update failed']); } //更新日志 if(!empty($iLogId)){ $aWhere = ['id' => $iLogId]; if(isset($aLog['refer_type'])){ unset($aLog['refer_type']); } $result = Db::name('production_article_refer_ai')->where($aWhere)->limit(1)->update($aLog); } Db::commit(); return json_encode(['status' => 1,'msg' => 'success']); } /** * 对接OPENAI */ private function curlOpenAIByDoi($aParam = []){ //获取DOI $sDoi = empty($aParam['doi']) ? '' : $aParam['doi']; if(empty($sDoi)){ return ['status' => 2,'msg' => 'Reference doi is empty']; } //系统角色 $sSysMessagePrompt = '请完成以下任务: 1. 根据提供的DOI号,查询该文献的AMA引用格式; 2. 按照以下规则调整AMA引用格式: - 第三个作者名字后添加 et al.; - DOI前加上"Available at: "; - DOI信息格式调整为"https://doi.org/+真实DOI"(替换真实DOI为文献实际DOI). 3. 严格按照以下JSON结构返回结果,仅返回JSON数据,不要额外文字,包含字段:doilink(url格式)、title(标题)、author(作者数组)、joura(出版社名称)、dateno(年;卷(期):起始页-终止页),is_ai_check(默认1) 4. 若未查询到信息,字段is_ai_check为2,相关字段为null。'; //用户角色 $sUserPrompt = '我提供的DOI是:'.$sDoi; $aMessage = [ ['role' => 'system', 'content' => $sSysMessagePrompt], ['role' => 'user', 'content' => $sUserPrompt], ]; //请求OPENAI接口 $sModel = empty($aParam['model']) ? 'gpt-4.1' : $aParam['model'];//模型 $sApiUrl = $this->sApiUrl;//'http://chat.taimed.cn/v1/chat/completions';// $aParam = ['model' => $sModel,'url' => $sApiUrl,'temperature' => 0,'messages' => $aMessage,'api_key' => $this->sApiKey]; $oOpenAi = new \app\common\OpenAi; $aResult = json_decode($oOpenAi->curlOpenAI($aParam),true); return $aResult; } /** * 作者修改完成发邮件 */ public function finishSendEmail(){ //获取参数 $aParam = empty($aParam) ? $this->request->post() : $aParam; //文章ID $iArticleId = empty($aParam['article_id']) ? '' : $aParam['article_id']; if(empty($iArticleId)){ return json_encode(array('status' => 2,'msg' => 'Please select an article')); } //查询条件 $aWhere = ['article_id' => $iArticleId,'state' => ['in',[5,6]]]; $aArticle = Db::name('article')->field('article_id,journal_id,accept_sn')->where($aWhere)->find(); if(empty($aArticle)){ return json_encode(['status' => 3,'msg' => 'The article does not exist or has not entered the editorial reference status']); } $aWhere = ['article_id' => $iArticleId,'state' => 0]; $aProductionArticle = Db::name('production_article')->field('p_article_id')->where($aWhere)->find(); if(empty($aProductionArticle)) { return ['status' => 2,'msg' => 'The article has not entered the production stage']; } //查询是否有参考文献 $aWhere = ['p_article_id' => $aProductionArticle['p_article_id'],'state' => 0]; $aRefer = Db::name('production_article_refer')->field('article_id')->where($aWhere)->find(); if(empty($aRefer)) { return ['status' => 2,'msg' => 'No reference information found, please be patient and wait for the editor to upload']; } //查询期刊信息 if(empty($aArticle['journal_id'])){ return json_encode(array('status' => 4,'msg' => 'The article is not associated with a journal' )); } $aWhere = ['state' => 0,'journal_id' => $aArticle['journal_id']]; $aJournal = Db::name('journal')->where($aWhere)->find(); if(empty($aJournal)){ return json_encode(array('status' => 5,'msg' => 'No journal information found' )); } //查询编辑邮箱 $iUserId = empty($aJournal['editor_id']) ? '' : $aJournal['editor_id']; if(empty($iUserId)){ return json_encode(array('status' => 6,'msg' => 'The journal to which the article belongs has not designated a responsible editor' )); } $aWhere = ['user_id' => $iUserId,'state' => 0,'email' => ['<>','']]; $aUser = Db::name('user')->field('user_id,email,realname,account')->where($aWhere)->find(); if(empty($aUser)){ return json_encode(['status' => 7,'msg' => "Edit email as empty"]); } //处理发邮件 //邮件模版 $aEmailConfig = [ 'email_subject' => '{journal_title}-{accept_sn}', 'email_content' => ' Dear Editor,

The authors have revised the formats of all references, please check.
Sn:{accept_sn}

Sincerely,
Editorial Office
Subscribe to this journal
{journal_title}
Email: {journal_email}
Website: {website}' ]; //邮件内容 $aSearch = [ '{accept_sn}' => empty($aArticle['accept_sn']) ? '' : $aArticle['accept_sn'],//accept_sn '{journal_title}' => empty($aJournal['title']) ? '' : $aJournal['title'],//期刊名 '{journal_issn}' => empty($aJournal['issn']) ? '' : $aJournal['issn'], '{journal_email}' => empty($aJournal['email']) ? '' : $aJournal['email'], '{website}' => empty($aJournal['website']) ? '' : $aJournal['website'], ]; //发邮件 //邮件标题 $email = $aUser['email']; $title = str_replace(array_keys($aSearch), array_values($aSearch),$aEmailConfig['email_subject']); //邮件内容变量替换 $content = str_replace(array_keys($aSearch), array_values($aSearch), $aEmailConfig['email_content']); $pre = \think\Env::get('emailtemplete.pre'); $net = \think\Env::get('emailtemplete.net'); $net1 = str_replace("{{email}}",trim($email),$net); $content=$pre.$content.$net1; //发送邮件 $memail = empty($aJournal['email']) ? '' : $aJournal['email']; $mpassword = empty($aJournal['epassword']) ? '' : $aJournal['epassword']; //期刊标题 $from_name = empty($aJournal['title']) ? '' : $aJournal['title']; //邮件队列组装参数 $aResult = sendEmail($email,$title,$from_name,$content,$memail,$mpassword); $iStatus = empty($aResult['status']) ? 1 : $aResult['status']; $iIsSuccess = 2; $sMsg = empty($aResult['data']) ? '失败' : $aResult['data']; if($iStatus == 1){ return json_encode(['status' => 1,'msg' => 'success']); } return json_encode(['status' => 8,'msg' => 'fail']); } }