合并
This commit is contained in:
@@ -217,6 +217,24 @@ class Finalreview extends Base
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach ($aLists as $key => $value) {
|
||||
$rr = Db::name("article_reviewer_final")->where("reviewer_id",$value['user_id'])->order("id desc")->limit(1)->select();
|
||||
if(isset($rr[0])){
|
||||
$aLists[$key]['last_time'] = $rr[0]['invited_time'];
|
||||
}else{
|
||||
$aLists[$key]['last_time'] = 0;
|
||||
}
|
||||
$info = Db::name("user_reviewer_info")->field("field,field_ai")->where("reviewer_id",$value['user_id'])->find();
|
||||
if($info){
|
||||
$aLists[$key]['field'] = $info['field'];
|
||||
$aLists[$key]['field_ai'] = $info['field_ai'];
|
||||
}else{
|
||||
$aLists[$key]['field'] = '';
|
||||
$aLists[$key]['field_ai'] = '';
|
||||
}
|
||||
}
|
||||
return json_encode(['status' => 1,'msg' => 'success','data' => ['total' => $iCount,'lists' => $aLists]]);
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,193 @@ class Order extends base{
|
||||
}
|
||||
}
|
||||
|
||||
/**提交改价申请
|
||||
* @return void
|
||||
*/
|
||||
public function applyPrice(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"article_id"=>"require",
|
||||
"fee"=>"require",
|
||||
"remark"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
|
||||
// 查询文章、期刊和编辑者信息
|
||||
$article_info = $this->article_obj->where("article_id",$data['article_id'])->find();
|
||||
$journal_info = $this->journal_obj->where("journal_id",$article_info['journal_id'])->find();
|
||||
$editor_info = $this->user_obj->where("user_id",$journal_info['editor_id'])->find();
|
||||
|
||||
// 验证编辑者权限
|
||||
if($journal_info['editor_id']!=$editor_info['user_id']){
|
||||
return jsonError("You are not the editor of this journal");
|
||||
}
|
||||
|
||||
$check = Db::name("article_price_apply")->where("article_id",$data['article_id'])->where("state",0)->find();
|
||||
if($check){
|
||||
return jsonError("There is an unfinished price change application for this article");
|
||||
}
|
||||
|
||||
$insert['article_id'] = $data['article_id'];
|
||||
$insert['old_fee'] = $article_info['fee'];
|
||||
$insert['fee'] = $data['fee'];
|
||||
$insert['remark'] = $data['remark'];
|
||||
$insert['ctime'] = time();
|
||||
Db::name("article_price_apply")->insert($insert);
|
||||
|
||||
|
||||
//发送邮件
|
||||
$tt = '您有新的价格审批!<a href="https://submission.tmrjournals.com/journalFeeApproval">link</a>';
|
||||
|
||||
sendEmail("18812616272@qq.com", "tmr-文章价格申请", "tmr-文章价格申请", $tt, $journal_info['email'], $journal_info['epassword']);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return jsonSuccess();
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function getApplyList(){
|
||||
$data = $this->request->post();
|
||||
$data['pageIndex'] = isset($data['pageIndex'])?$data['pageIndex']:0;
|
||||
$data['pageSize'] = isset($data['pageSize'])?$data['pageSize']:10;
|
||||
$start_page = ($data['pageIndex']-1)*$data['pageSize'];
|
||||
$list = Db::name("article_price_apply")->where("state", $data['state'] ?? 0)->limit($start_page,$data['pageSize'])->select();
|
||||
foreach ($list as $k=>$v){
|
||||
$list[$k]['article_info'] = $this->article_obj->field("article_id,journal_id,accept_sn,title")->where("article_id",$v['article_id'])->find();
|
||||
$list[$k]['journal_info'] = $this->journal_obj->field("journal_id,title")->where("journal_id",$list[$k]['article_info']['journal_id'])->find();
|
||||
$list[$k]['history'] = Db::name("article_price_apply")->where("article_id",$list[$k]['article_info']['article_id'])->select();
|
||||
}
|
||||
$re['list'] = $list;
|
||||
$re['total'] = Db::name("article_price_apply")->where("state", $data['state'] ?? 0)->count();
|
||||
|
||||
return jsonSuccess($re);
|
||||
}
|
||||
|
||||
public function getApplyArticleList(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"state"=>"require",
|
||||
"pageIndex"=>"require",
|
||||
"pageSize"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
$start_page = ($data['pageIndex']-1)*$data['pageSize'];
|
||||
$sql = "SELECT a.article_id,a.title,a.accept_sn,j.title journal_title FROM t_article a
|
||||
INNER JOIN (
|
||||
SELECT MAX(id) AS max_id, article_id
|
||||
FROM t_article_price_apply
|
||||
GROUP BY article_id
|
||||
) t ON a.article_id = t.article_id
|
||||
INNER JOIN t_article_price_apply apa ON apa.id = t.max_id
|
||||
LEFT JOIN t_journal j ON a.journal_id = j.journal_id
|
||||
WHERE apa.state = ".$data['state']."
|
||||
LIMIT ".$start_page.",".$data['pageSize'];
|
||||
|
||||
$row = Db::query($sql);
|
||||
|
||||
foreach ($row as $k=>$v){
|
||||
$row[$k]['history'] = Db::name("article_price_apply")->where("article_id",$v['article_id'])->select();
|
||||
}
|
||||
|
||||
|
||||
$countSql = "SELECT COUNT(*) AS total FROM t_article a
|
||||
INNER JOIN (
|
||||
SELECT MAX(id) AS max_id, article_id
|
||||
FROM t_article_price_apply
|
||||
GROUP BY article_id
|
||||
) t ON a.article_id = t.article_id
|
||||
INNER JOIN t_article_price_apply apa ON apa.id = t.max_id
|
||||
WHERE apa.state = ".$data['state'];
|
||||
$total = Db::query($countSql);
|
||||
$total = intval($total[0]['total'] ?? 0);
|
||||
$re['list'] = $row;
|
||||
$re['count'] = $total;
|
||||
|
||||
return jsonSuccess($re);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getApplyCount(){
|
||||
$res = Db::name("article_price_apply")->field("state,count(*) as count")->group("state")->select();
|
||||
|
||||
return jsonSuccess($res);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function acceptPriceApply(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"id"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
|
||||
$apply_info = Db::name("article_price_apply")->where("id",$data['id'])->find();
|
||||
$article_update['fee']=$apply_info['fee'];
|
||||
$article_update['fee_remark']=$apply_info['remark'];
|
||||
|
||||
if(intval($apply_info['fee'])==0) {
|
||||
$article_update['is_buy']=1;
|
||||
}
|
||||
$this->article_obj->where("article_id",$apply_info['article_id'])->update($article_update);
|
||||
|
||||
Db::name("article_price_apply")->where("id",$data['id'])->update(['state'=>1]);
|
||||
|
||||
$order_info = $this->order_obj->where("article_id",$apply_info['article_id'])->whereIn("state",[0,1])->find();
|
||||
if($order_info){
|
||||
if(intval($apply_info['fee'])==0){
|
||||
$this->order_obj->where("order_id",$order_info['order_id'])->update(['state'=>2]);
|
||||
}else{
|
||||
$update1['order_fee']=$apply_info['fee'];
|
||||
$this->order_obj->where("order_id",$order_info['order_id'])->update($update1);
|
||||
}
|
||||
}
|
||||
|
||||
return jsonSuccess();
|
||||
}
|
||||
|
||||
public function getArticlePriceApplyList(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"article_id"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
|
||||
$list = Db::name("article_price_apply")->where("article_id",$data['article_id'])->select();
|
||||
|
||||
return jsonSuccess($list);
|
||||
}
|
||||
|
||||
|
||||
public function rejectPriceApply(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"id"=>"require",
|
||||
"remark"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
// $apply_info = Db::name("article_price_apply")->where("id",$data['id'])->find();
|
||||
Db::name("article_price_apply")->where("id",$data['id'])->update(['state'=>2,"remark"=>trim($data['remark'])]);
|
||||
return jsonSuccess();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 修改文章价格
|
||||
*
|
||||
|
||||
@@ -11,6 +11,7 @@ use think\Queue;
|
||||
use think\Validate;
|
||||
use think\log;
|
||||
use app\common\ArticleSymbolNormalizer;
|
||||
use app\common\ReferenceDispatchService;
|
||||
|
||||
/**
|
||||
* @title 公共管理相关
|
||||
@@ -1931,7 +1932,7 @@ class Production extends Base
|
||||
}
|
||||
|
||||
$this->referToDoi($data['p_article_id']);
|
||||
$this->doiTofrag($data['p_article_id']);
|
||||
(new ReferenceDispatchService())->dispatchRefersByType($data['p_article_id']);
|
||||
return jsonSuccess([]);
|
||||
}
|
||||
|
||||
@@ -1998,19 +1999,7 @@ class Production extends Base
|
||||
|
||||
public function doiTofrag($p_article_id)
|
||||
{
|
||||
$p_info = $this->production_article_obj->where('p_article_id', $p_article_id)->find();
|
||||
$refers = $this->production_article_refer_obj->where('p_article_id', $p_info['p_article_id'])->where('state', 0)->select();
|
||||
foreach ($refers as $v) {
|
||||
if ($v['refer_doi'] == '') {
|
||||
$this->production_article_refer_obj->where('p_refer_id', $v['p_refer_id'])->update(['refer_frag' => $v['refer_content']]);
|
||||
} else {
|
||||
|
||||
//修改队列兼容对接OPENAI接口 chengxiaoling 20251128 start
|
||||
// Queue::push('app\api\job\ts@fire1', $v, 'ts');
|
||||
Queue::push('app\api\job\ArticleReferDetailQueue@fire', $v, 'ArticleReferDetailQueue');
|
||||
//修改队列兼容对接OPENAI接口 chengxiaoling 20251128 end
|
||||
}
|
||||
}
|
||||
(new ReferenceDispatchService())->dispatchRefersByType($p_article_id);
|
||||
return jsonSuccess([]);
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,10 @@ class Ucenter extends Base{
|
||||
if($check){
|
||||
return jsonError("Your application for Editorial Board is processing. Please do not repeat it.");
|
||||
}
|
||||
|
||||
$check_has = $this->user_to_yboard_obj->where('user_id',$data['user_id'])->where('state',0)->find();
|
||||
if ($check_has){
|
||||
return jsonError("You have already applied for Editorial Board.");
|
||||
}
|
||||
//判断是否上传CV
|
||||
$aWhere = ['user_id' => $data['user_id'],'state' => 0];
|
||||
$aCv = $this->user_cv_obj->field('user_cv_id')->where($aWhere)->find();
|
||||
|
||||
@@ -273,26 +273,83 @@ class CrossrefService
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取标题
|
||||
* 提取标题(英文优先)
|
||||
*
|
||||
* CrossRef 的 title / original-title 可能包含多个语言版本(中文期刊/双语文章常见)。
|
||||
* 这里优先返回不含中日韩字符的版本,全部为中文时才退回第一个。
|
||||
*/
|
||||
public function getTitle($aDoiInfo = [])
|
||||
{
|
||||
return $aDoiInfo['title'][0] ?? '';
|
||||
$candidates = [];
|
||||
if (!empty($aDoiInfo['title']) && is_array($aDoiInfo['title'])) {
|
||||
$candidates = array_merge($candidates, $aDoiInfo['title']);
|
||||
}
|
||||
if (!empty($aDoiInfo['original-title']) && is_array($aDoiInfo['original-title'])) {
|
||||
$candidates = array_merge($candidates, $aDoiInfo['original-title']);
|
||||
}
|
||||
return $this->pickPreferredLatin($candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取期刊/出版社相关信息
|
||||
* 提取期刊/出版社相关信息(期刊名英文优先)
|
||||
*/
|
||||
public function getPublisher($aDoiInfo = [])
|
||||
{
|
||||
$containerTitles = (!empty($aDoiInfo['container-title']) && is_array($aDoiInfo['container-title']))
|
||||
? $aDoiInfo['container-title'] : [];
|
||||
$shortTitles = (!empty($aDoiInfo['short-container-title']) && is_array($aDoiInfo['short-container-title']))
|
||||
? $aDoiInfo['short-container-title'] : [];
|
||||
|
||||
return [
|
||||
'title' => isset($aDoiInfo['container-title'][0]) ? $aDoiInfo['container-title'][0] : '',
|
||||
'short_title' => isset($aDoiInfo['short-container-title'][0]) ? $aDoiInfo['short-container-title'][0] : '',
|
||||
'title' => $this->pickPreferredLatin($containerTitles),
|
||||
'short_title' => $this->pickPreferredLatin($shortTitles),
|
||||
'ISSN' => $aDoiInfo['ISSN'] ?? [],
|
||||
'publisher' => $aDoiInfo['publisher'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否包含中日韩(CJK)字符
|
||||
*/
|
||||
public function hasCjk($str)
|
||||
{
|
||||
$str = (string)$str;
|
||||
if ($str === '') {
|
||||
return false;
|
||||
}
|
||||
return preg_match('/[\x{4e00}-\x{9fff}\x{3040}-\x{30ff}\x{ac00}-\x{d7af}\x{3400}-\x{4dbf}]/u', $str) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多个候选字符串中优先挑选「不含 CJK 字符」的一个;
|
||||
* 全部为空则返回空串,全部含 CJK 则返回第一个非空值。
|
||||
*
|
||||
* @param array $candidates
|
||||
* @return string
|
||||
*/
|
||||
private function pickPreferredLatin($candidates)
|
||||
{
|
||||
if (!is_array($candidates) || empty($candidates)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$firstNonEmpty = '';
|
||||
foreach ($candidates as $item) {
|
||||
$s = trim((string)$item);
|
||||
if ($s === '') {
|
||||
continue;
|
||||
}
|
||||
if ($firstNonEmpty === '') {
|
||||
$firstNonEmpty = $s;
|
||||
}
|
||||
if (!$this->hasCjk($s)) {
|
||||
return $s;
|
||||
}
|
||||
}
|
||||
|
||||
return $firstNonEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 PubMed/NLM 反查期刊规范缩写(CrossRef 无缩写时的兜底)。
|
||||
* 任何异常都吞掉并返回空串,保证不影响主流程。
|
||||
|
||||
@@ -54,7 +54,7 @@ class ProductionArticleRefer
|
||||
return json_encode(['status' => 1,'msg' => 'Add to reference processing queue']);
|
||||
}
|
||||
/**
|
||||
* 处理参考文献
|
||||
* 处理参考文献(单个)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -71,10 +71,17 @@ class ProductionArticleRefer
|
||||
}
|
||||
//查询未处理过的数据
|
||||
$aWhere = ['p_refer_id' => $iPReferId,'p_article_id' => $iPArticleId,'state' => 0];
|
||||
$aRefer = Db::name('production_article_refer')->field('refer_doi,refer_content')->where($aWhere)->find();
|
||||
$aRefer = Db::name('production_article_refer')->field('refer_doi,refer_content,refer_type')->where($aWhere)->find();
|
||||
if(empty($aRefer)){
|
||||
return json_encode(array('status' => 2,'msg' => 'No reference records found'.json_encode($aParam)));
|
||||
}
|
||||
|
||||
// 非期刊类型已在分流阶段处理,队列不再走期刊解析
|
||||
$referType = strtolower(trim((string)($aRefer['refer_type'] ?? 'journal')));
|
||||
if (in_array($referType, ['book', 'other'], true)) {
|
||||
return json_encode(['status' => 1, 'msg' => 'Skipped non-journal reference']);
|
||||
}
|
||||
|
||||
if(empty($aRefer['refer_doi'])){
|
||||
return json_encode(['status' => 4,'msg' => 'Reference DOI is empty'.json_encode($aParam)]);
|
||||
}
|
||||
@@ -89,40 +96,48 @@ class ProductionArticleRefer
|
||||
]);
|
||||
$summary = $svc->fetchWorkSummary($doiNorm);
|
||||
if ($summary !== null && !empty($summary['doi'])) {
|
||||
$update_a = [];
|
||||
$title = trim((string)($summary['title'] ?? ''));
|
||||
$jouraRaw = trim((string)($summary['joura'] ?? ''));
|
||||
// 姓全写 + 名首字母,超过 3 个作者取前 3 个 + et al
|
||||
$authorCitation = $svc->getAuthorsCitation($summary['raw'] ?? [], 3);
|
||||
$dateno = trim((string)($summary['dateno'] ?? ''));
|
||||
$doilink = trim((string)($summary['doilink'] ?? ''));
|
||||
$update_a['title'] = $title;
|
||||
$update_a['author'] = $authorCitation !== '' ? $authorCitation . '.' : '';
|
||||
$update_a['joura'] = $jouraRaw;
|
||||
$update_a['dateno'] = $dateno;
|
||||
$update_a['refer_type'] = "journal";
|
||||
$update_a['is_ja'] = 1;
|
||||
$update_a['doilink'] = $doilink;
|
||||
$update_a['cs'] = 1;
|
||||
$update_a['update_time'] = time();
|
||||
$update_a['is_deal'] = 1;
|
||||
|
||||
try {
|
||||
(new ReferenceReferAuthorService())->syncFromWorkSummary(
|
||||
$iPReferId,
|
||||
$iPArticleId,
|
||||
$doiNorm,
|
||||
$summary
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
\think\Log::error(
|
||||
'ProductionArticleRefer sync refer authors failed p_refer_id='
|
||||
. $iPReferId . ' ' . $e->getMessage()
|
||||
);
|
||||
// 英文优先兜底:若 CrossRef 结果的标题/期刊/作者仍含中日韩字符,
|
||||
// 说明该 DOI 元数据是中文,放弃 CrossRef 路径,改走下方 citation.doi.org(lang=en-US)
|
||||
$hasCjk = $svc->hasCjk($title) || $svc->hasCjk($jouraRaw) || $svc->hasCjk($authorCitation);
|
||||
if (!$hasCjk) {
|
||||
$update_a = [];
|
||||
$dateno = trim((string)($summary['dateno'] ?? ''));
|
||||
$doilink = trim((string)($summary['doilink'] ?? ''));
|
||||
$update_a['title'] = $title;
|
||||
$update_a['author'] = $authorCitation !== '' ? $authorCitation . '.' : '';
|
||||
$update_a['joura'] = $jouraRaw;
|
||||
$update_a['dateno'] = $dateno;
|
||||
// CrossRef 的 type 最权威,据此确定参考文献类型,未命中回退 journal
|
||||
$crossrefType = isset($summary['raw']['type']) ? $summary['raw']['type'] : '';
|
||||
$mappedType = (new ReferenceTypeClassifier())->mapCrossrefType($crossrefType);
|
||||
$update_a['refer_type'] = $mappedType !== '' ? $mappedType : "journal";
|
||||
$update_a['is_ja'] = 1;
|
||||
$update_a['doilink'] = $doilink;
|
||||
$update_a['cs'] = 1;
|
||||
$update_a['update_time'] = time();
|
||||
$update_a['is_deal'] = 1;
|
||||
|
||||
try {
|
||||
(new ReferenceReferAuthorService())->syncFromWorkSummary(
|
||||
$iPReferId,
|
||||
$iPArticleId,
|
||||
$doiNorm,
|
||||
$summary
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
\think\Log::error(
|
||||
'ProductionArticleRefer sync refer authors failed p_refer_id='
|
||||
. $iPReferId . ' ' . $e->getMessage()
|
||||
);
|
||||
}
|
||||
Db::name('production_article_refer')->where(['p_refer_id' => $iPReferId])->limit(1)->update($update_a);
|
||||
return json_encode(['status' => 1,'msg' => 'Update successful']);
|
||||
}
|
||||
|
||||
Db::name('production_article_refer')->where(['p_refer_id' => $iPReferId])->limit(1)->update($update_a);
|
||||
return json_encode(['status' => 1,'msg' => 'Update successful']);
|
||||
}
|
||||
|
||||
//结束---用crossref接口的方式处理数据
|
||||
@@ -135,7 +150,13 @@ class ProductionArticleRefer
|
||||
$res = myGet($url);
|
||||
$frag = trim(substr($res, strpos($res, '.') + 1));
|
||||
if(empty($frag)){
|
||||
$aUpdate = ['refer_frag' => $aRefer['refer_content'],'refer_type' => 'other','is_deal' => 1,'update_time' => time()];
|
||||
// 依据作者原文识别类型;仅在识别出具体非期刊类型时覆盖,否则保持 other
|
||||
$referType = 'other';
|
||||
$typeInfo = (new ReferenceTypeClassifier())->classify((string)$aRefer['refer_content']);
|
||||
if (in_array($typeInfo['type'], ['book','conference','thesis','web'], true)) {
|
||||
$referType = $typeInfo['type'];
|
||||
}
|
||||
$aUpdate = ['refer_frag' => $aRefer['refer_content'],'refer_type' => $referType,'is_deal' => 1,'update_time' => time()];
|
||||
$aWhere = ['p_refer_id' => $iPReferId];
|
||||
$result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate);
|
||||
//写入通过AI获取参考文献详情队列
|
||||
@@ -148,6 +169,11 @@ class ProductionArticleRefer
|
||||
if (mb_substr_count($frag, '.') != 3){
|
||||
$f = $frag . " Available at: " . PHP_EOL . "https://doi.org/" . $aRefer['refer_doi'];
|
||||
$update['refer_type'] = "other";
|
||||
// 依据作者原文识别类型;仅在识别出具体非期刊类型时覆盖
|
||||
$typeInfo = (new ReferenceTypeClassifier())->classify((string)$aRefer['refer_content']);
|
||||
if (in_array($typeInfo['type'], ['book','conference','thesis','web'], true)) {
|
||||
$update['refer_type'] = $typeInfo['type'];
|
||||
}
|
||||
$update['refer_frag'] = $f;
|
||||
$update['cs'] = 1;
|
||||
//写入通过AI获取参考文献详情队列
|
||||
@@ -164,6 +190,11 @@ class ProductionArticleRefer
|
||||
if ($joura == trim($bj[0])) {
|
||||
}
|
||||
$update['refer_type'] = "journal";
|
||||
// 依据作者原文的强特征识别更精确的类型(禁用 LLM,仅规则强命中才覆盖)
|
||||
$ruleType = (new ReferenceTypeClassifier(['use_llm' => false]))->classifyByRule((string)$aRefer['refer_content']);
|
||||
if ($ruleType['confidence'] >= 0.8 && !in_array($ruleType['type'], ['journal','other'], true)) {
|
||||
$update['refer_type'] = $ruleType['type'];
|
||||
}
|
||||
$update['is_ja'] = $joura == trim($bj[0]) ? 0 : 1;
|
||||
$update['dateno'] = str_replace(' ', '', str_replace('-', '–', trim($bj[1])));
|
||||
//新增处理 期卷页码 20251127 start
|
||||
|
||||
251
application/common/ReferenceDispatchService.php
Normal file
251
application/common/ReferenceDispatchService.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
use think\Db;
|
||||
use think\Env;
|
||||
use think\Queue;
|
||||
|
||||
/**
|
||||
* 参考文献分流处理:freshRefers 入口之后,按 journal / book / other 三路处理。
|
||||
*
|
||||
* - journal:有 DOI 入 ArticleReferDetailQueue;无 DOI 保留原文
|
||||
* - book:填充结构化字段 author/title/joura/dateno/isbn(与 Preaccept、References 约定一致)
|
||||
* - other:refer_frag = refer_content,不做结构化
|
||||
*/
|
||||
class ReferenceDispatchService
|
||||
{
|
||||
/** @var ReferenceTypeClassifier */
|
||||
private $classifier;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->classifier = new ReferenceTypeClassifier(['use_llm' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对某篇生产文章的全部参考文献按类型分流处理
|
||||
*/
|
||||
public function dispatchRefersByType($pArticleId)
|
||||
{
|
||||
$pArticleId = intval($pArticleId);
|
||||
if ($pArticleId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$refers = Db::name('production_article_refer')
|
||||
->where('p_article_id', $pArticleId)
|
||||
->where('state', 0)
|
||||
->order('index asc, p_refer_id asc')
|
||||
->select();
|
||||
|
||||
if (empty($refers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$crossref = new CrossrefService([
|
||||
'mailto' => trim((string)Env::get('crossref_mailto', '')),
|
||||
]);
|
||||
|
||||
foreach ($refers as $refer) {
|
||||
$summary = null;
|
||||
$crossrefType = '';
|
||||
if (trim((string)$refer['refer_doi']) !== '') {
|
||||
$doiNorm = $this->normalizeDoi($refer['refer_doi']);
|
||||
if ($doiNorm !== '') {
|
||||
$summary = $crossref->fetchWorkSummary($doiNorm);
|
||||
if ($summary && !empty($summary['raw']['type'])) {
|
||||
$crossrefType = (string)$summary['raw']['type'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$typeInfo = $this->classifier->classify((string)$refer['refer_content'], $crossrefType);
|
||||
$dispatchType = $this->classifier->normalizeDispatchType($typeInfo['type']);
|
||||
|
||||
Db::name('production_article_refer')->where('p_refer_id', $refer['p_refer_id'])->update([
|
||||
'refer_type' => $dispatchType,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
$refer['refer_type'] = $dispatchType;
|
||||
|
||||
switch ($dispatchType) {
|
||||
case ReferenceTypeClassifier::TYPE_BOOK:
|
||||
$this->processBookRefer($refer, $summary, $crossref);
|
||||
break;
|
||||
case ReferenceTypeClassifier::TYPE_OTHER:
|
||||
$this->processOtherRefer($refer);
|
||||
break;
|
||||
default:
|
||||
$this->processJournalRefer($refer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processJournalRefer(array $refer)
|
||||
{
|
||||
$pReferId = intval($refer['p_refer_id']);
|
||||
if (trim((string)$refer['refer_doi']) === '') {
|
||||
Db::name('production_article_refer')->where('p_refer_id', $pReferId)->update([
|
||||
'refer_frag' => $refer['refer_content'],
|
||||
'refer_type' => ReferenceTypeClassifier::TYPE_JOURNAL,
|
||||
'is_deal' => 1,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Queue::push('app\api\job\ArticleReferDetailQueue@fire', $refer, 'ArticleReferDetailQueue');
|
||||
}
|
||||
|
||||
private function processOtherRefer(array $refer)
|
||||
{
|
||||
Db::name('production_article_refer')->where('p_refer_id', intval($refer['p_refer_id']))->update([
|
||||
'refer_frag' => $refer['refer_content'],
|
||||
'refer_type' => ReferenceTypeClassifier::TYPE_OTHER,
|
||||
'cs' => 0,
|
||||
'is_deal' => 1,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* book:结构化字段,DOI 仅用于补数据
|
||||
*/
|
||||
private function processBookRefer(array $refer, $summary, CrossrefService $crossref)
|
||||
{
|
||||
$pReferId = intval($refer['p_refer_id']);
|
||||
$content = (string)$refer['refer_content'];
|
||||
$update = [
|
||||
'refer_type' => ReferenceTypeClassifier::TYPE_BOOK,
|
||||
'is_deal' => 1,
|
||||
'update_time' => time(),
|
||||
];
|
||||
|
||||
if (is_array($summary) && !empty($summary['raw'])) {
|
||||
$raw = $summary['raw'];
|
||||
$authorCitation = $crossref->getAuthorsCitation($raw, 3);
|
||||
$update['author'] = $authorCitation !== '' ? rtrim($authorCitation, '.') . '.' : '';
|
||||
$update['title'] = trim((string)($summary['title'] ?? ''));
|
||||
$update['joura'] = $this->extractBookPublisher($raw, $summary);
|
||||
$update['dateno'] = $this->extractBookDateno($raw);
|
||||
$isbn = $this->extractIsbnFromRaw($raw);
|
||||
if ($isbn === '' && !empty($refer['refer_doi'])) {
|
||||
$doi = $this->normalizeDoi($refer['refer_doi']);
|
||||
$isbn = $doi !== '' ? 'https://doi.org/' . $doi : '';
|
||||
}
|
||||
$update['isbn'] = $isbn;
|
||||
$update['is_ja'] = 1;
|
||||
} else {
|
||||
$parsed = $this->parseBookFromContent($content);
|
||||
$update = array_merge($update, $parsed);
|
||||
}
|
||||
|
||||
$hasCore = trim((string)($update['author'] ?? '')) !== ''
|
||||
&& trim((string)($update['title'] ?? '')) !== '';
|
||||
$update['cs'] = $hasCore ? 1 : 0;
|
||||
if (!$hasCore) {
|
||||
$update['refer_frag'] = $content;
|
||||
}
|
||||
|
||||
Db::name('production_article_refer')->where('p_refer_id', $pReferId)->update($update);
|
||||
}
|
||||
|
||||
private function extractBookPublisher(array $raw, array $summary)
|
||||
{
|
||||
$publisher = trim((string)($raw['publisher'] ?? ''));
|
||||
if ($publisher !== '') {
|
||||
return $publisher;
|
||||
}
|
||||
$pub = $summary['publisher'] ?? [];
|
||||
if (!empty($pub['publisher'])) {
|
||||
return trim((string)$pub['publisher']);
|
||||
}
|
||||
if (!empty($pub['title'])) {
|
||||
return trim((string)$pub['title']);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function extractBookDateno(array $raw)
|
||||
{
|
||||
if (!empty($raw['published']['date-parts'][0][0])) {
|
||||
return (string)$raw['published']['date-parts'][0][0];
|
||||
}
|
||||
if (!empty($raw['issued']['date-parts'][0][0])) {
|
||||
return (string)$raw['issued']['date-parts'][0][0];
|
||||
}
|
||||
if (!empty($raw['created']['date-parts'][0][0])) {
|
||||
return (string)$raw['created']['date-parts'][0][0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function extractIsbnFromRaw(array $raw)
|
||||
{
|
||||
if (empty($raw['ISBN']) || !is_array($raw['ISBN'])) {
|
||||
return '';
|
||||
}
|
||||
foreach ($raw['ISBN'] as $isbn) {
|
||||
$isbn = trim((string)$isbn);
|
||||
if ($isbn !== '') {
|
||||
return $isbn;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 无 Crossref 时从原文尽量抽取 book 结构化字段
|
||||
*/
|
||||
private function parseBookFromContent($content)
|
||||
{
|
||||
$content = trim((string)$content);
|
||||
$out = [
|
||||
'author' => '',
|
||||
'title' => '',
|
||||
'joura' => '',
|
||||
'dateno' => '',
|
||||
'isbn' => '',
|
||||
'is_ja' => 1,
|
||||
];
|
||||
|
||||
if ($content === '') {
|
||||
return $out;
|
||||
}
|
||||
|
||||
if (preg_match('/\bISBN[:\s]*([\d\-Xx\s]+)/i', $content, $m)) {
|
||||
$out['isbn'] = preg_replace('/\s+/', '-', trim($m[1]));
|
||||
}
|
||||
|
||||
if (preg_match('/\b(19|20)\d{2}\b/', $content, $m)) {
|
||||
$out['dateno'] = $m[0];
|
||||
}
|
||||
|
||||
// Place: Publisher; Year → joura 取 Publisher
|
||||
if (preg_match('/:\s*([^;]+);\s*(19|20)\d{2}/', $content, $m)) {
|
||||
$out['joura'] = trim($m[1]);
|
||||
} elseif (preg_match('/\b([A-Z][A-Za-z .&]+(?:Press|Publishing|Publisher|Books?))\b/i', $content, $m)) {
|
||||
$out['joura'] = trim($m[1]);
|
||||
}
|
||||
|
||||
// 作者. 标题. ... 简单拆分
|
||||
$parts = preg_split('/\.\s+/', $content, 3);
|
||||
if (is_array($parts) && count($parts) >= 2) {
|
||||
$out['author'] = trim($parts[0]);
|
||||
if (substr($out['author'], -1) !== '.') {
|
||||
$out['author'] .= '.';
|
||||
}
|
||||
$out['title'] = trim(rtrim($parts[1], '.'));
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function normalizeDoi($doi)
|
||||
{
|
||||
$doi = preg_replace('#^https?://(dx\.)?doi\.org/#i', '', trim((string)$doi));
|
||||
return trim($doi, " \t\n\r\0\x0B/");
|
||||
}
|
||||
}
|
||||
236
application/common/ReferenceTypeClassifier.php
Normal file
236
application/common/ReferenceTypeClassifier.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
use app\common\service\LLMService;
|
||||
|
||||
/**
|
||||
* 参考文献类型识别器
|
||||
*
|
||||
* 识别顺序(分层,从权威到兜底):
|
||||
* 1. CrossRef 的 type 字段(若有 DOI 且命中)——最权威
|
||||
* 2. 英文规则启发式(基于作者原文特征词/结构)
|
||||
* 3. 大模型兜底(规则判为 other 或低置信度时)
|
||||
* 4. 仍无法判定 → other
|
||||
*
|
||||
* 支持类型:journal / book / conference / thesis / web / other
|
||||
*/
|
||||
class ReferenceTypeClassifier
|
||||
{
|
||||
const TYPE_JOURNAL = 'journal';
|
||||
const TYPE_BOOK = 'book';
|
||||
const TYPE_CONFERENCE = 'conference';
|
||||
const TYPE_THESIS = 'thesis';
|
||||
const TYPE_WEB = 'web';
|
||||
const TYPE_OTHER = 'other';
|
||||
|
||||
/** 是否允许使用大模型兜底 */
|
||||
private $useLlm = true;
|
||||
|
||||
public function __construct($config = [])
|
||||
{
|
||||
if (is_array($config) && isset($config['use_llm'])) {
|
||||
$this->useLlm = (bool)$config['use_llm'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:根据作者原文(可选 CrossRef type)识别类型
|
||||
*
|
||||
* @param string $referContent 作者提供的原始参考文献字符串
|
||||
* @param string $crossrefType CrossRef 返回的 type(可选)
|
||||
* @return array ['type' => string, 'confidence' => float, 'source' => string]
|
||||
*/
|
||||
public function classify($referContent, $crossrefType = '')
|
||||
{
|
||||
$text = trim((string)$referContent);
|
||||
|
||||
// 1. CrossRef type 优先
|
||||
$byCrossref = $this->mapCrossrefType($crossrefType);
|
||||
if ($byCrossref !== '') {
|
||||
return ['type' => $byCrossref, 'confidence' => 0.95, 'source' => 'crossref'];
|
||||
}
|
||||
|
||||
if ($text === '') {
|
||||
return ['type' => self::TYPE_OTHER, 'confidence' => 0.0, 'source' => 'fallback'];
|
||||
}
|
||||
|
||||
// 2. 规则启发式
|
||||
$byRule = $this->classifyByRule($text);
|
||||
if ($byRule['type'] !== self::TYPE_OTHER && $byRule['confidence'] >= 0.8) {
|
||||
return ['type' => $byRule['type'], 'confidence' => $byRule['confidence'], 'source' => 'rule'];
|
||||
}
|
||||
|
||||
// 3. 大模型兜底
|
||||
if ($this->useLlm) {
|
||||
$byLlm = $this->classifyByLlm($text);
|
||||
if ($byLlm !== '') {
|
||||
return ['type' => $byLlm, 'confidence' => 0.7, 'source' => 'llm'];
|
||||
}
|
||||
}
|
||||
|
||||
// 规则给出的弱结果(若有)优先于纯兜底
|
||||
if ($byRule['type'] !== self::TYPE_OTHER) {
|
||||
return ['type' => $byRule['type'], 'confidence' => $byRule['confidence'], 'source' => 'rule'];
|
||||
}
|
||||
|
||||
// 4. 兜底
|
||||
return ['type' => self::TYPE_OTHER, 'confidence' => 0.0, 'source' => 'fallback'];
|
||||
}
|
||||
|
||||
/**
|
||||
* CrossRef type → 内部枚举映射;未命中返回空串
|
||||
*/
|
||||
/**
|
||||
* 将细分类归并为排版用的三类:journal / book / other
|
||||
*/
|
||||
public function normalizeDispatchType($type)
|
||||
{
|
||||
$type = strtolower(trim((string)$type));
|
||||
if ($type === self::TYPE_BOOK) {
|
||||
return self::TYPE_BOOK;
|
||||
}
|
||||
if ($type === self::TYPE_JOURNAL) {
|
||||
return self::TYPE_JOURNAL;
|
||||
}
|
||||
return self::TYPE_OTHER;
|
||||
}
|
||||
|
||||
public function mapCrossrefType($crossrefType)
|
||||
{
|
||||
$t = strtolower(trim((string)$crossrefType));
|
||||
if ($t === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$map = [
|
||||
// 期刊
|
||||
'journal-article' => self::TYPE_JOURNAL,
|
||||
'journal' => self::TYPE_JOURNAL,
|
||||
'journal-volume' => self::TYPE_JOURNAL,
|
||||
'journal-issue' => self::TYPE_JOURNAL,
|
||||
// 图书
|
||||
'book' => self::TYPE_BOOK,
|
||||
'book-chapter' => self::TYPE_BOOK,
|
||||
'book-part' => self::TYPE_BOOK,
|
||||
'book-section' => self::TYPE_BOOK,
|
||||
'book-set' => self::TYPE_BOOK,
|
||||
'book-series' => self::TYPE_BOOK,
|
||||
'book-track' => self::TYPE_BOOK,
|
||||
'reference-book' => self::TYPE_BOOK,
|
||||
'edited-book' => self::TYPE_BOOK,
|
||||
'monograph' => self::TYPE_BOOK,
|
||||
// 会议
|
||||
'proceedings-article' => self::TYPE_CONFERENCE,
|
||||
'proceedings' => self::TYPE_CONFERENCE,
|
||||
'proceedings-series' => self::TYPE_CONFERENCE,
|
||||
// 学位论文
|
||||
'dissertation' => self::TYPE_THESIS,
|
||||
// 在线/预印本
|
||||
'posted-content' => self::TYPE_WEB,
|
||||
];
|
||||
|
||||
return isset($map[$t]) ? $map[$t] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 英文规则启发式:按「最独特 → 最普通」顺序判定
|
||||
*
|
||||
* @return array ['type' => string, 'confidence' => float]
|
||||
*/
|
||||
public function classifyByRule($text)
|
||||
{
|
||||
$hasDoi = (bool)preg_match('/\bdoi:\s*10\./i', $text) || (bool)preg_match('#doi\.org/#i', $text);
|
||||
$hasUrl = (bool)preg_match('#https?://#i', $text);
|
||||
// 期刊卷期页结构,如 2020;382(8):727-733 或 2020;10:100
|
||||
$hasJournalVol = (bool)preg_match('/\b(19|20)\d{2}\s*[;:]\s*\d+\s*(\(\d+\))?\s*:\s*[A-Za-z]?\d+/', $text);
|
||||
|
||||
// 1) 学位论文
|
||||
if (preg_match('/\[(ph\.?d\.?|master(\'s)?|doctoral|masters)?\s*(thesis|dissertation)\]/i', $text)
|
||||
|| preg_match('/\b(ph\.?d\.?|master\'?s|doctoral|doctorate)\b[^.]{0,40}\b(thesis|dissertation)\b/i', $text)
|
||||
|| preg_match('/\b(thesis|dissertation)\b/i', $text)) {
|
||||
return ['type' => self::TYPE_THESIS, 'confidence' => 0.9];
|
||||
}
|
||||
|
||||
// 2) 会议论文
|
||||
if (preg_match('/\bproceedings\b/i', $text)
|
||||
|| preg_match('/\bin:\s*proc\b/i', $text)
|
||||
|| preg_match('/\b(conference|symposium|workshop|congress)\b/i', $text)
|
||||
|| preg_match('/\bannual meeting\b/i', $text)) {
|
||||
return ['type' => self::TYPE_CONFERENCE, 'confidence' => 0.85];
|
||||
}
|
||||
|
||||
// 3) 网页 / 在线资源
|
||||
if (preg_match('/\[(internet|online)\]/i', $text)
|
||||
|| preg_match('/\baccessed\b/i', $text)
|
||||
|| preg_match('/\bavailable\s+(from|at)\b/i', $text)
|
||||
|| preg_match('/\bcited\s+(19|20)\d{2}/i', $text)
|
||||
|| ($hasUrl && !$hasDoi && !$hasJournalVol)) {
|
||||
return ['type' => self::TYPE_WEB, 'confidence' => 0.8];
|
||||
}
|
||||
|
||||
// 4) 图书
|
||||
if (preg_match('/\b\d+(st|nd|rd|th)\s+ed(ition)?\.?/i', $text)
|
||||
|| preg_match('/\bisbn\b/i', $text)
|
||||
|| preg_match('/\b(press|publisher|publishing house)\b/i', $text)
|
||||
|| preg_match('/[A-Z][A-Za-z .]+:\s*[A-Z][A-Za-z .&]+;\s*(19|20)\d{2}/', $text)) {
|
||||
// 期刊卷期结构更强时不判为书
|
||||
if (!$hasJournalVol) {
|
||||
return ['type' => self::TYPE_BOOK, 'confidence' => 0.8];
|
||||
}
|
||||
}
|
||||
|
||||
// 5) 期刊
|
||||
if ($hasJournalVol || $hasDoi
|
||||
|| preg_match('/\bvol\.?\s*\d+/i', $text)
|
||||
|| preg_match('/\bpp?\.\s*\d+/i', $text)) {
|
||||
return ['type' => self::TYPE_JOURNAL, 'confidence' => $hasJournalVol ? 0.85 : 0.6];
|
||||
}
|
||||
|
||||
return ['type' => self::TYPE_OTHER, 'confidence' => 0.0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 大模型兜底:只返回枚举值之一,失败返回空串
|
||||
*/
|
||||
public function classifyByLlm($text)
|
||||
{
|
||||
try {
|
||||
$llm = new LLMService();
|
||||
$system = 'You are a bibliography classifier. Classify the reference into exactly one type. '
|
||||
. 'Allowed types: journal, book, conference, thesis, web, other. '
|
||||
. 'journal=journal article; book=book or book chapter; conference=conference/proceedings paper; '
|
||||
. 'thesis=dissertation/thesis; web=website or online resource; other=none of the above. '
|
||||
. 'Respond with ONLY a JSON object: {"type":"<one of the allowed types>"}. No explanation.';
|
||||
$user = "Reference:\n" . mb_substr($text, 0, 2000);
|
||||
|
||||
$content = $llm->requestChat([
|
||||
['role' => 'system', 'content' => $system],
|
||||
['role' => 'user', 'content' => $user],
|
||||
], 0);
|
||||
|
||||
if ($content === null || $content === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$parsed = $llm->parseJsonResponse($content);
|
||||
$type = '';
|
||||
if (is_array($parsed) && isset($parsed['type'])) {
|
||||
$type = strtolower(trim((string)$parsed['type']));
|
||||
} else {
|
||||
// 兜底:直接从文本里找枚举词
|
||||
if (preg_match('/\b(journal|book|conference|thesis|web|other)\b/i', $content, $m)) {
|
||||
$type = strtolower($m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
self::TYPE_JOURNAL, self::TYPE_BOOK, self::TYPE_CONFERENCE,
|
||||
self::TYPE_THESIS, self::TYPE_WEB, self::TYPE_OTHER,
|
||||
];
|
||||
return in_array($type, $allowed, true) ? $type : '';
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user