Files
tougao/application/api/controller/References.php
2026-06-02 09:57:51 +08:00

1605 lines
63 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;
use app\common\ReferenceCheckService;
/**
* @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 [];
}
// 与 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 片段:<blue>[n]</blue> → <mycite>(需传入已构建的 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 '<mycite data-id="' . implode(',', $ids) . '"></mycite>';
},
$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 中的 <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 $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=0t_article_main.content
* - type=2t_article_main_tabletable_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 = []){
return jsonError("service is stop!");
//获取参数
$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']);
}
/**
* 参考文献第一次校对
* @return \think\response\Json
*/
public function allReferenceCheckAI(){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//必填值验证
$iPArticleId = empty($aParam['p_article_id']) ? '' : $aParam['p_article_id'];
if(empty($iPArticleId)){
return json_encode(array('status' => 2,'msg' => 'Please select an article' ));
}
//查询文章p_article_id 与 article_id 都要带,下游服务方法两者都用)
$aWhere = ['p_article_id' => $iPArticleId,'state' => ['in',[0,2]]];
$aProductionArticle = Db::name('production_article')->field('p_article_id,article_id')->where($aWhere)->find();
if(empty($aProductionArticle)){
return json_encode(array('status' => 3,'msg' => 'No articles found' ));
}
if($this->checkReferStatus($iPArticleId)==0){
return jsonError('请修正完文献内容再进行校对。');
}
//已存在校对记录则禁止重复执行第一次校对,提示走重置接口
$iExisting = Db::name('article_reference_check_result')
->where('p_article_id', $iPArticleId)
->count();
if(intval($iExisting) > 0){
return jsonError('该文章已存在校对记录,请使用"重置校对"接口重新校对。');
}
try {
$svc = new ReferenceCheckService();
$result = $svc->enqueueByPArticle($aProductionArticle);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* 文献校对重置:删除该文章已有的全部校对明细,并重新入队整篇校对
* POST/GET: article_id必填
* @url /api/Article/referenceCheckReset
*/
public function referenceCheckResetAI()
{
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
//必填值验证
$iPArticleId = empty($aParam['p_article_id']) ? '' : $aParam['p_article_id'];
if(empty($iPArticleId)){
return json_encode(array('status' => 2,'msg' => 'Please select an article' ));
}
//查询文章p_article_id 与 article_id 都要带,下游服务方法两者都用)
$aWhere = ['p_article_id' => $iPArticleId,'state' => ['in',[0,2]]];
$aProductionArticle = Db::name('production_article')->field('p_article_id,article_id')->where($aWhere)->find();
if(empty($aProductionArticle)){
return json_encode(array('status' => 3,'msg' => 'No articles found' ));
}
if($this->checkReferStatus($iPArticleId)==0){
return jsonError('请修正完文献内容再进行校对。');
}
$iArticleId = empty($aProductionArticle['article_id']) ? 0 : $aProductionArticle['article_id'];
if(empty($iArticleId)){
return json_encode(array('status' => 4,'msg' => 'Unbound article' ));
}
try {
$result = (new ReferenceCheckService())->resetAndRecheckByArticle($aProductionArticle);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* 清空某篇文章下的全部参考文献校对记录(不重新入队)
*
* 与 referenceCheckResetAI 的区别reset 是「清空 + 重新校对」,
* 这里只做「清空」一步,校对状态回到未校对,等待用户手动再触发。
*
* POST/GET: p_article_id必填
*/
public function referenceCheckClearAI()
{
$aParam = $this->request->post();
if (empty($aParam)) {
$aParam = $this->request->param();
}
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
if ($iPArticleId <= 0) {
return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
}
// 校验文章存在与其它校对接口口径一致state in [0,2]
$aProductionArticle = Db::name('production_article')
->field('p_article_id,article_id')
->where(['p_article_id' => $iPArticleId, 'state' => ['in', [0, 2]]])
->find();
if (empty($aProductionArticle)) {
return json_encode(array('status' => 3, 'msg' => 'No articles found'));
}
try {
$deleted = (new ReferenceCheckService())->clearArticleChecksByPArticleId($iPArticleId);
return jsonSuccess([
'p_article_id' => $iPArticleId,
'deleted' => intval($deleted),
]);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* 按 p_article_id 查整篇引用校对进度(按 reference_no 分组聚合)
*
* POST/GET: p_article_id必填
*
* 返回 list 中每项含reference_no、p_refer_id、progress_status数值
* total、pending、done、failed、pass、is_pass、last_updated_at、records
*
* 分组状态字段 progress_status 数值含义(生命周期顺序):
* 0 = 待校验 1 = 校对中 2 = 校对完成 3 = 校对失败
*
* records[i].status 与分组同一套数值含义(但 record 不会出现 1=校对中):
* 0 = 待校验 2 = 校对完成 3 = 校对失败
*
* summary 用字符串键pending / checking / completed / failed
*/
public function referenceCheckProgressAI()
{
$aParam = $this->request->post();
if (empty($aParam)) {
$aParam = $this->request->param();
}
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
if ($iPArticleId <= 0) {
return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
}
try {
$result = (new ReferenceCheckService())->getProgressByPArticleId($iPArticleId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* 按 p_article_id 查整篇文章引用校对总状态(用于前端按钮分流)
*
* POST/GET: p_article_id必填
*
* 计数维度是「参考文献」(按 reference_no 分组),不是单条校对明细行。
* 例50 条参考文献、底层 111 条校对明细时total = 50。
*
* 返回 status 数值含义(整篇):
* 0 = 未校对(一条记录都没有)
* 1 = 校对中(至少 1 条参考文献仍有未跑完的明细)
* 2 = 校对完成(所有参考文献全部明细已结束)
*
* 返回字段p_article_id、status、total、pending、done、failed、progress_percent
* total —— 参考文献条数
* pending —— 该条参考文献仍有未跑完明细的数量(含"部分跑完"
* done —— 该条参考文献所有明细都 status=2(完成) 的数量
* failed —— 该条参考文献全部跑完且至少 1 条 status=3(失败) 的数量
* pending + done + failed = totalprogress_percent = (done+failed)/total
*
* 分组明细请走 referenceCheckProgressAI。
*/
public function referenceCheckArticleStatusAI()
{
$aParam = $this->request->post();
if (empty($aParam)) {
$aParam = $this->request->param();
}
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
if ($iPArticleId <= 0) {
return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
}
try {
$result = (new ReferenceCheckService())->getArticleProgressStatusByPArticleId($iPArticleId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* 多篇文章并行校对时,查询指定文章前面还有几篇在排队
*
* POST/GET: p_article_id必填
*
* 例:当前 5 篇文章正在校对,该文排在第 3 → ahead=2, position=3, running_total=5。
* 返回running_total、ahead、position、in_queue、status整篇校对状态 0/1/2
*/
public function referenceCheckPendingCountAI()
{
$aParam = $this->request->post();
if (empty($aParam)) {
$aParam = $this->request->param();
}
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
if ($iPArticleId <= 0) {
return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
}
try {
$result = (new ReferenceCheckService())->getArticleCheckQueuePositionByPArticleId($iPArticleId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* 某条参考文献下「校对失败」的明细重新校对(异步)
*
* POST/GET: p_refer_id必填
* p_article_id可选
*
* 仅重跑 status=3校对失败的记录不改动 refer_text只重置结果字段后入 RabbitMQ 批次队列。
* 返回p_refer_id、p_article_id、reset、queued、check_ids、queue
*/
public function referenceCheckRecheckFailedAI()
{
$aParam = $this->request->post();
if (empty($aParam)) {
$aParam = $this->request->param();
}
$iPReferId = empty($aParam['p_refer_id']) ? 0 : intval($aParam['p_refer_id']);
if ($iPReferId <= 0) {
return json_encode(array('status' => 2, 'msg' => 'Please select a reference'));
}
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
try {
$result = (new ReferenceCheckService())->enqueueRecheckFailedByPReferId($iPReferId, $iPArticleId);
return jsonSuccess([]);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
/**
* 按 p_refer_id 查单条参考文献的校对明细与进度
*
* POST/GET: p_refer_id必填
*
* 分组进度progress_status(0待/1中/2完成/3失败)、pending、done、failed、pass、
* is_pass、progress_percent、last_updated_at
* list 每项check_id、am_id、status、confidence、reason、is_match、is_pass
*/
public function referenceCheckDetailsAI()
{
$aParam = $this->request->post();
if (empty($aParam)) {
$aParam = $this->request->param();
}
$iPReferId = empty($aParam['p_refer_id']) ? 0 : intval($aParam['p_refer_id']);
if ($iPReferId <= 0) {
return json_encode(array('status' => 2, 'msg' => 'Please select a reference'));
}
try {
$result = (new ReferenceCheckService())->getCheckDetailsByPReferId($iPReferId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
}
public function checkReferStatus($p_article_id){
$list = $this->production_article_refer_obj->where('p_article_id', $p_article_id)->where('state', 0)->select();
if (!$list) {
return jsonError('references error');
}
$frag = 1;
foreach ($list as $v) {
if ($v['cs'] == 0) {
$frag = 0;
break;
}
}
return $frag;
}
}