Files
tougao/application/api/controller/References.php
wangjinlei b0f0d6461f 1
2026-04-10 11:19:20 +08:00

1151 lines
46 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\api\controller;
use app\api\controller\Base;
use app\common\CitationRelevanceService;
use app\common\CrossrefService;
use app\common\QueueRedis;
use app\common\PubmedService;
use think\Validate;
use think\Db;
use think\Env;
use think\Queue;
/**
* @title 参考文献
* @description 相关方法汇总
*/
class References extends Base
{
public function __construct(\think\Request $request = null) {
parent::__construct($request);
}
//OPENAI token
private $sApiKey = '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';
//OPENAI URL
private $sApiUrl = 'https://api.openai.com/v1/chat/completions';
/**
* 获取参考文献的信息
* @param p_refer_id 主键ID
*/
public function get($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')->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<int,int> 例如 [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 [];
}
$out = [];
$segments = preg_split('/\s*,\s*/', $referencePart);
foreach ($segments as $seg) {
$seg = trim((string)$seg);
if ($seg === '') {
continue;
}
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 中的 <blue>[n]</blue>(及 [1,2]、[2-4] 等)替换为 <mycite data-id="p_refer_id"></mycite>
* 找不到对应参考文献时保留原 <blue>[…]</blue>,避免丢内容。
*
* @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 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];
}
// 仅处理数字引用,避免误伤 [Fig 1] 等
$innerNorm = str_replace(['', '', '—'], [',', '-', '-'], $inner);
if (!preg_match('/^[\d\s,\-]+$/', $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])) {
// 有任意一个序号无法映射到 p_refer_id则保持原始片段不变避免丢引用信息
return $matches[0];
}
$ids[] = (string)intval($map[$n]);
}
if ($ids === []) {
return $matches[0];
}
return '<mycite data-id="' . implode(',', $ids) . '"></mycite>';
},
$content
);
}
/**
* 接口:将 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'];
$content = $main_info['content'];
$out = $this->rewriteMainContentCitationsToMycite($content, $pArticleId);
return jsonSuccess(['content' => $out]);
}
/**
* 批量处理并回写 t_article_main.content
* 将正文中的 <blue>[n]</blue> / [1,2] / [2-4] 改写为 <mycite data-id="..."></mycite>
*
* 参数:
* - p_article_id (必填)production 侧文章ID
* - type (可选):默认 0仅文本 main传空则处理所有 type
* - dry_run (可选)1=只预览不落库
*/
public function convertArticleMainCitationsToMycite()
{
// $aParam = empty($aParam) ? $this->request->post() : $aParam;
// $pArticleId = intval($aParam['p_article_id'] ?? 0);
// if ($pArticleId <= 0) {
// return jsonError('p_article_id is required');
// }
//
// // 通过 production_article -> article_id确保是当前系统存在的文章
// $aArticle = $this->getArticle(['p_article_id' => $pArticleId]);
// $iStatus = empty($aArticle['status']) ? 0 : $aArticle['status'];
// if ($iStatus != 1) {
// return json_encode($aArticle);
// }
// $aArticle = empty($aArticle['data']) ? [] : $aArticle['data'];
// $articleId = intval($aArticle['article_id'] ?? 0);
// if ($articleId <= 0) {
// return jsonError('Article not found');
// }
$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;
$type = $aParam['type'] ?? 0;
$p_info = $this->production_article_obj->where('article_id', $aParam['article_id'])->where('state', 0)->find();
$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 ($type !== '' && $type !== null) {
$query->where('type', intval($type));
}
$mains = $query->field('am_id,content,type,sort')->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']);
$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' => intval($row['type'] ?? 0),
'sort' => intval($row['sort'] ?? 0),
'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());
}
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数据不要额外文字,包含字段doilinkurl格式、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,<br><br>
The authors have revised the formats of all references, please check.<br>
Sn:{accept_sn}<br><br>
Sincerely,<br>Editorial Office<br>
<a href="https://www.tmrjournals.com/draw_up.html?issn={journal_issn}">Subscribe to this journal</a><br>{journal_title}<br>
Email: {journal_email}<br>
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']);
}
}