1052 lines
42 KiB
PHP
1052 lines
42 KiB
PHP
<?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 = [])
|
||
{
|
||
$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');
|
||
}
|
||
|
||
$dryRun = intval($aParam['dry_run'] ?? 0) === 1;
|
||
$type = isset($aParam['type']) ? $aParam['type'] : 0;
|
||
|
||
$query = Db::name('article_main')
|
||
->where('article_id', $articleId)
|
||
->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,
|
||
'update_time' => time(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
if ($dryRun) {
|
||
Db::rollback();
|
||
} else {
|
||
Db::commit();
|
||
}
|
||
} catch (\Exception $e) {
|
||
Db::rollback();
|
||
return jsonError('convert failed: ' . $e->getMessage());
|
||
}
|
||
|
||
return jsonSuccess([
|
||
'article_id' => $articleId,
|
||
'p_article_id' => $pArticleId,
|
||
'dry_run' => $dryRun ? 1 : 0,
|
||
'total' => count($mains),
|
||
'changed' => $changed,
|
||
'preview' => $preview,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 修改参考文献的信息
|
||
* @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,<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']);
|
||
}
|
||
}
|