From a9409de4f955524c80116df0a9597680126003a0 Mon Sep 17 00:00:00 2001 From: wangjinlei <751475802@qq.com> Date: Wed, 8 Jul 2026 11:15:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E6=9B=B4=E6=96=B0,1=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=8F=82=E8=80=83=E6=96=87=E7=8C=AE=E7=9A=84?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=A2=9E=E5=8A=A0=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=EF=BC=8C=EF=BC=8C2=E5=AE=98=E7=BD=91=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Order.php | 119 ++++++++++ application/common/ProductionArticleRefer.php | 23 +- .../common/ReferenceTypeClassifier.php | 221 ++++++++++++++++++ 3 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 application/common/ReferenceTypeClassifier.php diff --git a/application/api/controller/Order.php b/application/api/controller/Order.php index 90f80736..1313eca2 100644 --- a/application/api/controller/Order.php +++ b/application/api/controller/Order.php @@ -172,6 +172,125 @@ 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); + + return jsonSuccess(); + + } + + + public function getApplyList(){ + $list = Db::name("article_price_apply")->where("state",0)->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; + + return jsonSuccess($re); + } + + + + 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(); + } + + /** * 修改文章价格 * diff --git a/application/common/ProductionArticleRefer.php b/application/common/ProductionArticleRefer.php index 6d7dfb02..7455d77b 100644 --- a/application/common/ProductionArticleRefer.php +++ b/application/common/ProductionArticleRefer.php @@ -105,7 +105,10 @@ class ProductionArticleRefer $update_a['author'] = $authorCitation !== '' ? $authorCitation . '.' : ''; $update_a['joura'] = $jouraRaw; $update_a['dateno'] = $dateno; - $update_a['refer_type'] = "journal"; + // 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; @@ -126,7 +129,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获取参考文献详情队列 @@ -139,6 +148,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获取参考文献详情队列 @@ -155,6 +169,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 diff --git a/application/common/ReferenceTypeClassifier.php b/application/common/ReferenceTypeClassifier.php new file mode 100644 index 00000000..b4bc3242 --- /dev/null +++ b/application/common/ReferenceTypeClassifier.php @@ -0,0 +1,221 @@ +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 → 内部枚举映射;未命中返回空串 + */ + 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":""}. 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 ''; + } + } +}