大更新,1投稿系统参考文献的类型增加功能,,,2官网数据库功能
This commit is contained in:
43
.env
43
.env
@@ -184,4 +184,45 @@ net = '</p>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</html>'
|
||||
</html>'
|
||||
|
||||
; =====================================================================
|
||||
; 数据库收录推送凭证(由 application/extra/dbpush.php 读取)
|
||||
; 修改后无需改代码。留空则回退到 dbpush.php 中的默认值。
|
||||
; =====================================================================
|
||||
|
||||
[portico]
|
||||
host = ftp.portico.org
|
||||
port = 22
|
||||
user = TMR
|
||||
pass = h6EHD8
|
||||
|
||||
[scopus]
|
||||
host = sftp-opsbank2.elsevier.com
|
||||
port = 22
|
||||
user = pdf-e
|
||||
pass = '?b#W5=T4'
|
||||
base = /data/incoming/tmr/
|
||||
|
||||
[ebsco]
|
||||
host = ftp.epnet.com
|
||||
port = 21
|
||||
user = 56704
|
||||
pass = 'o3Mys"(S'
|
||||
|
||||
[cnki]
|
||||
host = FTP.cnki.net
|
||||
port = 21
|
||||
user = glo616cnki
|
||||
pass = glo616cnki
|
||||
|
||||
[dbpush]
|
||||
; 是否通过队列异步推送(true 需常驻 php think queue:work --queue dbpush)
|
||||
; 默认 false = 同步执行,行为与历史一致
|
||||
async = false
|
||||
; 失败自动重试次数(async=true 时生效)
|
||||
max_attempt = 3
|
||||
ts_base_url = http://ts.tmrjournals.com/
|
||||
article_pdf = http://journalapi.tmrjournals.com/public/articlePDF/
|
||||
article_sub = http://journalapi.tmrjournals.com/public/articleSUB/
|
||||
public_base = http://journalapi.tmrjournals.com/public/
|
||||
47
.env.example
Normal file
47
.env.example
Normal file
@@ -0,0 +1,47 @@
|
||||
; =====================================================================
|
||||
; 环境配置示例文件
|
||||
; 用法:复制为 .env 并填入真实值。.env 不应提交到版本库。
|
||||
; =====================================================================
|
||||
|
||||
; 邮件模板(HTML,多行,用单引号包裹)。实际值见线上 .env。
|
||||
; [emailtemplete]
|
||||
; pre = '<!doctype html> ... '
|
||||
; net = '</p> ... </html>'
|
||||
|
||||
; ---------------------------------------------------------------------
|
||||
; 数据库收录推送凭证(application/extra/dbpush.php 读取)
|
||||
; ---------------------------------------------------------------------
|
||||
|
||||
[portico]
|
||||
host = ftp.portico.org
|
||||
port = 22
|
||||
user = your_portico_user
|
||||
pass = your_portico_pass
|
||||
|
||||
[scopus]
|
||||
host = sftp-opsbank2.elsevier.com
|
||||
port = 22
|
||||
user = your_scopus_user
|
||||
pass = 'your_scopus_pass'
|
||||
base = /data/incoming/tmr/
|
||||
|
||||
[ebsco]
|
||||
host = ftp.epnet.com
|
||||
port = 21
|
||||
user = your_ebsco_user
|
||||
pass = 'your_ebsco_pass'
|
||||
|
||||
[cnki]
|
||||
host = FTP.cnki.net
|
||||
port = 21
|
||||
user = your_cnki_user
|
||||
pass = your_cnki_pass
|
||||
|
||||
[dbpush]
|
||||
; false=同步(默认);true=异步入队(需 php think queue:work --queue dbpush)
|
||||
async = false
|
||||
max_attempt = 3
|
||||
ts_base_url = http://ts.tmrjournals.com/
|
||||
article_pdf = http://journalapi.tmrjournals.com/public/articlePDF/
|
||||
article_sub = http://journalapi.tmrjournals.com/public/articleSUB/
|
||||
public_base = http://journalapi.tmrjournals.com/public/
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
/nbproject/private/
|
||||
/public/
|
||||
/.vscode/
|
||||
/.vscode/
|
||||
|
||||
# 环境配置:包含第三方数据库凭证,不入库
|
||||
/.env
|
||||
@@ -107,7 +107,7 @@ class Journal extends Controller
|
||||
}
|
||||
|
||||
//新增查询信息journalAbs chengxiaoling 20250928 start
|
||||
$abs = $this->journal_abs_obj->where('journal_id', $v['journal_id'])->where('state', 0)->order('sort')->select();
|
||||
$abs = $this->journal_abs_obj->where('journal_id', $v['journal_id'])->where('state', 0)->whereRaw('(is_show IS NULL OR is_show <> 0)')->order('sort')->select();
|
||||
$res[$k]['journalAbs'] = $abs;
|
||||
|
||||
//查询期刊编委
|
||||
@@ -161,7 +161,7 @@ class Journal extends Controller
|
||||
$journal_info = $this->journal_obj->where('journal_id', $data['journal_id'])->find();
|
||||
$rearr = $journal_info['relate'] == '' ? [] : explode(',', $journal_info['relate']);
|
||||
$relatelist = $this->journal_obj->where('journal_id', 'in', $rearr)->where('state', 0)->select();
|
||||
$absList = $this->journal_abs_obj->where('journal_id', $data['journal_id'])->where('state', 0)->order('sort')->select();
|
||||
$absList = $this->journal_abs_obj->where('journal_id', $data['journal_id'])->where('state', 0)->whereRaw('(is_show IS NULL OR is_show <> 0)')->order('sort')->select();
|
||||
$stageList = $this->journal_stage_obj->where('journal_id', $data['journal_id'])->where('is_publish', 1)->where('state', 0)->order('stage_year desc,stage_no desc')->select();
|
||||
|
||||
//获取期刊主编辑 chengxiaoling start 20250626
|
||||
@@ -498,7 +498,7 @@ class Journal extends Controller
|
||||
->select();
|
||||
|
||||
foreach ($list as $k => $v) {
|
||||
$abs = $this->journal_abs_obj->where('journal_id', $v['journal_id'])->where('state', 0)->order('sort')->limit(4)->select();
|
||||
$abs = $this->journal_abs_obj->where('journal_id', $v['journal_id'])->where('state', 0)->whereRaw('(is_show IS NULL OR is_show <> 0)')->order('sort')->limit(4)->select();
|
||||
$list[$k]['abs'] = $abs;
|
||||
}
|
||||
|
||||
@@ -555,7 +555,7 @@ class Journal extends Controller
|
||||
}
|
||||
//获取期刊主编辑 chengxiaoling end 20251210
|
||||
foreach ($list as $k => $v) {
|
||||
$abs = $this->journal_abs_obj->where('journal_id', $v['journal_id'])->where('state', 0)->order('sort')->limit(4)->select();
|
||||
$abs = $this->journal_abs_obj->where('journal_id', $v['journal_id'])->where('state', 0)->whereRaw('(is_show IS NULL OR is_show <> 0)')->order('sort')->limit(4)->select();
|
||||
$list[$k]['abs'] = $abs;
|
||||
//获取期刊主编辑 chengxiaoling start 20251210
|
||||
$sIssn = empty($v['issn']) ? '' : $v['issn'];
|
||||
|
||||
37
application/api/job/dbpush.php
Normal file
37
application/api/job/dbpush.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\job;
|
||||
|
||||
use think\queue\Job;
|
||||
use app\master\service\DbPushManager;
|
||||
|
||||
/**
|
||||
* 数据库收录推送 - 异步队列任务
|
||||
* 队列名:dbpush
|
||||
*/
|
||||
class dbpush
|
||||
{
|
||||
public function handle(Job $job, $data)
|
||||
{
|
||||
$manager = new DbPushManager();
|
||||
$ok = $manager->executeAndLog(
|
||||
$data['log_id'],
|
||||
$data['stage_id'],
|
||||
$data['pusher'],
|
||||
isset($data['options']) ? $data['options'] : []
|
||||
);
|
||||
|
||||
if ($ok) {
|
||||
$job->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
// 失败重试
|
||||
$max = (int) config('dbpush.max_attempt');
|
||||
if ($job->attempts() >= $max) {
|
||||
$job->delete();
|
||||
} else {
|
||||
$job->release(30); // 30 秒后重试
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,12 @@ class mysftp {
|
||||
|
||||
public function push(Job $job, $data){
|
||||
$job->delete();
|
||||
//链接sftp服务器
|
||||
$config['host'] = 'ftp.portico.org';
|
||||
$config['port'] = 22;
|
||||
$config['username'] = "TMR";
|
||||
$config['password'] = "h6EHD8";
|
||||
//链接sftp服务器(凭证走 dbpush 配置,历史值兜底)
|
||||
$cred = config('dbpush.credentials.portico');
|
||||
$config['host'] = isset($cred['host']) ? $cred['host'] : 'ftp.portico.org';
|
||||
$config['port'] = isset($cred['port']) ? $cred['port'] : 22;
|
||||
$config['username'] = isset($cred['user']) ? $cred['user'] : "TMR";
|
||||
$config['password'] = isset($cred['pass']) ? $cred['pass'] : "h6EHD8";
|
||||
$sftp_obj = new Sftp($config);
|
||||
$sftp_obj->upftp($data['local'],$data['remote']);
|
||||
}
|
||||
|
||||
@@ -316,16 +316,17 @@ function getAuthor($article) {
|
||||
|
||||
function pushSFTPForScopus($file,$pdf_file,$stage_info)
|
||||
{
|
||||
$host = 'sftp-opsbank2.elsevier.com';
|
||||
$port = 22; // SFTP 默认端口号
|
||||
$username = 'pdf-e';
|
||||
$password = '?b#W5=T4';
|
||||
$cred = config('dbpush.credentials.scopus');
|
||||
$host = isset($cred['host']) ? $cred['host'] : 'sftp-opsbank2.elsevier.com';
|
||||
$port = isset($cred['port']) ? $cred['port'] : 22; // SFTP 默认端口号
|
||||
$username = isset($cred['user']) ? $cred['user'] : 'pdf-e';
|
||||
$password = isset($cred['pass']) ? $cred['pass'] : '?b#W5=T4';
|
||||
|
||||
// 创建 SFTP 连接
|
||||
$sftp = new SFTP($host, $port);
|
||||
|
||||
if (!$sftp->login($username, $password)) {
|
||||
exit('SFTP 登录失败');
|
||||
throw new \Exception('Scopus SFTP 登录失败');
|
||||
}
|
||||
|
||||
// 列出远程服务器的文件
|
||||
@@ -336,7 +337,7 @@ function pushSFTPForScopus($file,$pdf_file,$stage_info)
|
||||
// if($stage_info !=null){
|
||||
// $bb = '/data/incoming/tmr/'.$stage_info['stage_year']."tmr-".$stage_info['stage_vol']."-".$stage_info['stage_no']."/";
|
||||
// }else{
|
||||
$bb = '/data/incoming/tmr/';
|
||||
$bb = isset($cred['base']) ? $cred['base'] : '/data/incoming/tmr/';
|
||||
// }
|
||||
$localFile = ROOT_PATH."public/articleXML/".$file;
|
||||
$remoteFile = $bb.$stage_info['stage_year']."v".$stage_info['stage_vol']."n".$stage_info['stage_no']."-".$file;
|
||||
@@ -349,13 +350,10 @@ function pushSFTPForScopus($file,$pdf_file,$stage_info)
|
||||
$remoteFilePdf = $bb.$stage_info['stage_year']."v".$stage_info['stage_vol']."n".$stage_info['stage_no']."-".str_replace(".xml",".pdf",$file);
|
||||
|
||||
$res1 = $sftp->put($remoteFilePdf,$localPdf,SFTP::SOURCE_LOCAL_FILE);
|
||||
if ($res&$res1) {
|
||||
echo "文件上传成功!";
|
||||
} else {
|
||||
// echo $sftp->getErrors();
|
||||
var_dump($sftp->getErrors());
|
||||
// return $sftp;
|
||||
if (!($res && $res1)) {
|
||||
throw new \Exception('Scopus SFTP 上传失败: ' . implode('; ', (array) $sftp->getErrors()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
134
application/extra/dbpush.php
Normal file
134
application/extra/dbpush.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
// =====================================================================
|
||||
// 数据库收录推送 - 集中配置
|
||||
// 说明:
|
||||
// 1) 所有第三方数据库的账号/密码优先从 .env 读取;读不到时回退到这里的
|
||||
// 默认值(即历史代码里写死的值),因此即使 .env 未配置也不会影响现有推送。
|
||||
// 2) default_channels 仅在 j_db_channel 数据表为空时生效,作为回退默认。
|
||||
// 3) 建议在生产环境的 .env 中填写真实凭证,并将 .env 从版本库移除
|
||||
// (见项目根目录 .env.example)。
|
||||
// =====================================================================
|
||||
|
||||
use think\Env;
|
||||
|
||||
return [
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 第三方数据库凭证(.env 优先,历史值兜底)
|
||||
// ---------------------------------------------------------------
|
||||
'credentials' => [
|
||||
// Portico:Excel + PDF 走 SFTP
|
||||
'portico' => [
|
||||
'host' => Env::get('portico.host', 'ftp.portico.org'),
|
||||
'port' => (int) Env::get('portico.port', 22),
|
||||
'user' => Env::get('portico.user', 'TMR'),
|
||||
'pass' => Env::get('portico.pass', 'h6EHD8'),
|
||||
],
|
||||
// Scopus / Elsevier:XML + PDF 走 SFTP
|
||||
'scopus' => [
|
||||
'host' => Env::get('scopus.host', 'sftp-opsbank2.elsevier.com'),
|
||||
'port' => (int) Env::get('scopus.port', 22),
|
||||
'user' => Env::get('scopus.user', 'pdf-e'),
|
||||
'pass' => Env::get('scopus.pass', '?b#W5=T4'),
|
||||
'base' => Env::get('scopus.base', '/data/incoming/tmr/'),
|
||||
],
|
||||
// EBSCO:XML + PDF 走 FTP
|
||||
'ebsco' => [
|
||||
'host' => Env::get('ebsco.host', 'ftp.epnet.com'),
|
||||
'port' => (int) Env::get('ebsco.port', 21),
|
||||
'user' => Env::get('ebsco.user', '56704'),
|
||||
'pass' => Env::get('ebsco.pass', 'o3Mys"(S'),
|
||||
],
|
||||
// CNKI 中国知网:Excel zip 走 FTP
|
||||
'cnki' => [
|
||||
'host' => Env::get('cnki.host', 'FTP.cnki.net'),
|
||||
'port' => (int) Env::get('cnki.port', 21),
|
||||
'user' => Env::get('cnki.user', 'glo616cnki'),
|
||||
'pass' => Env::get('cnki.pass', 'glo616cnki'),
|
||||
],
|
||||
],
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 通用数据库邮件送交的默认收件人(email_data 渠道使用)
|
||||
// ---------------------------------------------------------------
|
||||
'data_recipients' => [
|
||||
'jgatelicensing@informaticsglobal.com',
|
||||
'3097953993@qq.com',
|
||||
'751475802@qq.com',
|
||||
],
|
||||
|
||||
// 相关外部服务地址(保持与历史代码一致)
|
||||
'ts_base_url' => Env::get('dbpush.ts_base_url', 'http://ts.tmrjournals.com/'),
|
||||
'article_pdf' => Env::get('dbpush.article_pdf', 'http://journalapi.tmrjournals.com/public/articlePDF/'),
|
||||
'article_sub' => Env::get('dbpush.article_sub', 'http://journalapi.tmrjournals.com/public/articleSUB/'),
|
||||
'public_base' => Env::get('dbpush.public_base', 'http://journalapi.tmrjournals.com/public/'),
|
||||
|
||||
// 推送执行方式:
|
||||
// false = 同步执行(默认,行为与历史一致:Portico/邮件仍走既有 mail/mysftp 队列)
|
||||
// true = 各渠道整体异步入队,需额外常驻 `php think queue:work --queue dbpush`
|
||||
'async' => (bool) Env::get('dbpush.async', false),
|
||||
|
||||
// 失败自动重试次数(队列内)
|
||||
'max_attempt' => (int) Env::get('dbpush.max_attempt', 3),
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 默认渠道配置(仅当 j_db_channel 表为空时作为回退)
|
||||
// journal_ids 为空数组表示适用全部期刊
|
||||
// ---------------------------------------------------------------
|
||||
'default_channels' => [
|
||||
[
|
||||
'channel_key' => 'portico',
|
||||
'name' => 'Portico',
|
||||
'transport' => 'sftp',
|
||||
'pusher' => 'portico',
|
||||
'journal_ids' => [],
|
||||
'is_enable' => 1,
|
||||
'config' => [],
|
||||
],
|
||||
[
|
||||
'channel_key' => 'scopus',
|
||||
'name' => 'Scopus (Elsevier)',
|
||||
'transport' => 'sftp',
|
||||
'pusher' => 'scopus',
|
||||
'journal_ids' => [1, 11],
|
||||
'is_enable' => 1,
|
||||
'config' => [],
|
||||
],
|
||||
[
|
||||
'channel_key' => 'ebsco',
|
||||
'name' => 'EBSCO',
|
||||
'transport' => 'ftp',
|
||||
'pusher' => 'ebsco',
|
||||
'journal_ids' => [2, 8, 20, 15, 9, 10, 3, 1, 14, 11, 4, 18],
|
||||
'is_enable' => 1,
|
||||
'config' => [],
|
||||
],
|
||||
[
|
||||
'channel_key' => 'cnki',
|
||||
'name' => 'CNKI 中国知网',
|
||||
'transport' => 'ftp',
|
||||
'pusher' => 'cnki',
|
||||
'journal_ids' => [],
|
||||
'is_enable' => 0, // 历史代码中已注释,默认关闭
|
||||
'config' => [],
|
||||
],
|
||||
[
|
||||
'channel_key' => 'email_data',
|
||||
'name' => '通用数据库邮件送交',
|
||||
'transport' => 'email',
|
||||
'pusher' => 'email_data',
|
||||
'journal_ids' => [],
|
||||
'is_enable' => 1,
|
||||
'config' => [],
|
||||
],
|
||||
[
|
||||
'channel_key' => 'email_wanfang',
|
||||
'name' => '万方数据库',
|
||||
'transport' => 'email',
|
||||
'pusher' => 'email_data',
|
||||
'journal_ids' => [1],
|
||||
'is_enable' => 1,
|
||||
'config' => ['recipients' => ['958518573@qq.com', 'swyy2@wanfangdata.com.cn']],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -7,6 +7,13 @@ use think\Db;
|
||||
use think\Queue;
|
||||
use sftp\Sftp;
|
||||
use think\Validate;
|
||||
use app\master\service\DbPushManager;
|
||||
use app\master\service\DbPushLogger;
|
||||
use app\master\service\XmlBuilder;
|
||||
use app\master\service\push\PorticoPusher;
|
||||
use app\master\service\push\ScopusPusher;
|
||||
use app\master\service\push\EbscoPusher;
|
||||
use app\master\service\push\CnkiPusher;
|
||||
|
||||
/**
|
||||
* @title 数据库接口
|
||||
@@ -774,66 +781,159 @@ class Datebase extends Controller
|
||||
public function dataPush()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$stage_info = $this->journal_stage_obj->where("journal_stage_id", $data["journal_stage_id"])->find();
|
||||
$this->createEmailFile($data["journal_stage_id"]);
|
||||
|
||||
$journal_info = $this->journal_obj->where("journal_id", $stage_info["journal_id"])->find();
|
||||
$zipfile = ROOT_PATH . "public/dataFile/" . $data["journal_stage_id"] . '/' . $data["journal_stage_id"] . '.zip';
|
||||
$tt = "Dear Database,<br><br>";
|
||||
$tt .= "Please find the PDF files in attachment of published issue (Vol. " . $stage_info['stage_vol'] . ", No." . $stage_info['stage_no'] . ") of " . $journal_info['title'] . ".<br><br>";
|
||||
$tt .= "Yours Sincerely<br>";
|
||||
$tt .= "Dan Chen<br>";
|
||||
$tt .= "Manager<br>";
|
||||
$tt .= "TMR Publishing Group | Editorial Office | New Zealand<br>";
|
||||
$tt .= "Telephone: +64 02108293806<br>";
|
||||
$tt .= "Email: publisher@tmrjournals.com<br>";
|
||||
$tt .= "Website:www.tmrjournals.com";
|
||||
|
||||
$elist = [];
|
||||
$elist[] = "jgatelicensing@informaticsglobal.com";
|
||||
// $elist[] = "3601240974@qq.com";
|
||||
if ($journal_info["journal_id"] == 1) {
|
||||
$elist[] = "958518573@qq.com";
|
||||
$elist[] = "swyy2@wanfangdata.com.cn";//万方数据库
|
||||
$rule = new Validate([
|
||||
"journal_stage_id" => "require"
|
||||
]);
|
||||
if (!$rule->check($data)) {
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
$elist[] = "3097953993@qq.com";
|
||||
// $elist[] = "849192806@qq.com"; //皮皮
|
||||
$elist[] = "751475802@qq.com"; //我
|
||||
// $elist[] = "ELD@dia.govt.nz";
|
||||
|
||||
foreach ($elist as $v) {
|
||||
$cdata['email'] = $v;
|
||||
$cdata['title'] = "Data submitted-TMR Publishing Group";
|
||||
$cdata["fromname"] = "Data submitted-TMR Publishing Group";
|
||||
$cdata["content"] = $tt;
|
||||
$cdata["temail"] = $journal_info['email'];
|
||||
$cdata["tpassword"] = $journal_info['epassword'];
|
||||
$cdata["fj"] = $zipfile;
|
||||
Queue::push('app\api\job\mail@puchAndFJ', $cdata, "mail");
|
||||
// sendEmail_data($v, "Data submitted-TMR Publishing Group", "Data submitted-TMR Publishing Group", $tt, $journal_info['email'], $journal_info['epassword'],$zipfile);
|
||||
}
|
||||
//发送sftp
|
||||
$this->bf_db_push($data["journal_stage_id"]);
|
||||
// 发送ftp
|
||||
// $this->createFtpFileInfo($data["journal_stage_id"]);//cnki数据库
|
||||
|
||||
if($journal_info['journal_id']==1||$journal_info['journal_id']==11){
|
||||
$this->scopusPushXML($data["journal_stage_id"]);//scopus数据库
|
||||
}
|
||||
// if(in_array($journal_info["journal_id"],[2,8,20,15,9,10,3,1])){//ep数据库
|
||||
// $this->pushXmlToEp($data["journal_stage_id"]);
|
||||
// }
|
||||
|
||||
//EBSCO上传XML新增期刊ID[14,11,4,18,8,1,2] chengxiaoling 20250402 start
|
||||
if(in_array($journal_info["journal_id"],[2,8,20,15,9,10,3,1,14,11,4,18,8])){//ep数据库2,8,20,15,9,10,3,1 //EBSCO上传XML
|
||||
$this->pushXmlToEp($data["journal_stage_id"]);
|
||||
}
|
||||
//EBSCO上传XML新增期刊ID[14,11,4,18,8,1,2] chengxiaoling 20250402 end
|
||||
return jsonSuccess([]);
|
||||
// 配置驱动 + 全程留痕 + 可异步的统一推送(渠道由 j_db_channel / 默认配置决定)
|
||||
$operator = session('admin.realname') ?: (session('admin.username') ?: '');
|
||||
$manager = new DbPushManager();
|
||||
$summary = $manager->run($data["journal_stage_id"], $operator);
|
||||
return jsonSuccess(['summary' => $summary]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @title 获取某期推送情况
|
||||
* @description 查看指定分期在各数据库的推送记录/状态
|
||||
* @url /master/Datebase/getPushStatus
|
||||
* @method POST
|
||||
*
|
||||
* @param name:journal_stage_id type:int require:1 desc:分期id
|
||||
* @return list:推送日志array#
|
||||
*/
|
||||
public function getPushStatus()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"journal_stage_id" => "require"
|
||||
]);
|
||||
if (!$rule->check($data)) {
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
$logger = new DbPushLogger();
|
||||
$list = $logger->stageStatus($data['journal_stage_id']);
|
||||
$statusMap = [0 => '待推送/排队中', 1 => '成功', 2 => '失败'];
|
||||
foreach ($list as $k => $v) {
|
||||
$list[$k]['status_text'] = isset($statusMap[$v['status']]) ? $statusMap[$v['status']] : '未知';
|
||||
$list[$k]['ctime_text'] = $v['ctime'] ? date('Y-m-d H:i:s', $v['ctime']) : '';
|
||||
$list[$k]['utime_text'] = $v['utime'] ? date('Y-m-d H:i:s', $v['utime']) : '';
|
||||
}
|
||||
return jsonSuccess(['list' => $list]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title 重推
|
||||
* @description 针对某条推送日志重新推送(用于失败补推)
|
||||
* @url /master/Datebase/retryPush
|
||||
* @method POST
|
||||
*
|
||||
* @param name:log_id type:int require:1 desc:推送日志id
|
||||
*/
|
||||
public function retryPush()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"log_id" => "require"
|
||||
]);
|
||||
if (!$rule->check($data)) {
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
$manager = new DbPushManager();
|
||||
$res = $manager->retry($data['log_id']);
|
||||
if (empty($res['ok'])) {
|
||||
return jsonError(isset($res['msg']) ? $res['msg'] : '重推失败');
|
||||
}
|
||||
return jsonSuccess($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title 获取推送渠道配置
|
||||
* @description 获取数据库推送渠道配置列表
|
||||
* @url /master/Datebase/getChannels
|
||||
* @method POST
|
||||
* @return list:渠道配置array#
|
||||
*/
|
||||
public function getChannels()
|
||||
{
|
||||
$list = Db::name('db_channel')->where('state', 0)->order('sort desc')->select();
|
||||
return jsonSuccess(['list' => $list]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title 保存推送渠道配置
|
||||
* @description 新增或编辑数据库推送渠道(channel_id 存在则编辑)
|
||||
* @url /master/Datebase/saveChannel
|
||||
* @method POST
|
||||
*
|
||||
* @param name:channel_id type:int require:0 desc:渠道id(编辑时传)
|
||||
* @param name:channel_key type:string require:1 desc:渠道标识
|
||||
* @param name:name type:string require:1 desc:展示名
|
||||
* @param name:transport type:string require:1 desc:传输方式 sftp/ftp/email
|
||||
* @param name:pusher type:string require:0 desc:推送器标识(留空同channel_key)
|
||||
* @param name:journal_ids type:string require:0 desc:适用期刊id逗号分隔(留空=全部)
|
||||
* @param name:config type:string require:0 desc:JSON额外配置
|
||||
* @param name:is_enable type:int require:0 default:1 desc:是否启用
|
||||
* @param name:sort type:int require:0 default:0 desc:排序
|
||||
*/
|
||||
public function saveChannel()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"channel_key" => "require",
|
||||
"name" => "require",
|
||||
"transport" => "require",
|
||||
]);
|
||||
if (!$rule->check($data)) {
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
if (!empty($data['config']) && json_decode($data['config'], true) === null && strtolower(trim($data['config'])) !== 'null') {
|
||||
return jsonError('config 必须为合法 JSON');
|
||||
}
|
||||
$save = [
|
||||
'channel_key' => $data['channel_key'],
|
||||
'name' => $data['name'],
|
||||
'transport' => $data['transport'],
|
||||
'pusher' => isset($data['pusher']) ? $data['pusher'] : '',
|
||||
'journal_ids' => isset($data['journal_ids']) ? $data['journal_ids'] : '',
|
||||
'config' => isset($data['config']) ? $data['config'] : '',
|
||||
'is_enable' => isset($data['is_enable']) ? intval($data['is_enable']) : 1,
|
||||
'sort' => isset($data['sort']) ? intval($data['sort']) : 0,
|
||||
'utime' => time(),
|
||||
];
|
||||
if (!empty($data['channel_id'])) {
|
||||
Db::name('db_channel')->where('channel_id', $data['channel_id'])->update($save);
|
||||
} else {
|
||||
$save['ctime'] = time();
|
||||
Db::name('db_channel')->insert($save);
|
||||
}
|
||||
return jsonSuccess([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title 删除推送渠道配置
|
||||
* @description 删除数据库推送渠道
|
||||
* @url /master/Datebase/delChannel
|
||||
* @method POST
|
||||
*
|
||||
* @param name:channel_id type:int require:1 desc:渠道id
|
||||
*/
|
||||
public function delChannel()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"channel_id" => "require"
|
||||
]);
|
||||
if (!$rule->check($data)) {
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
Db::name('db_channel')->where('channel_id', $data['channel_id'])->update(['state' => 1, 'utime' => time()]);
|
||||
return jsonSuccess([]);
|
||||
}
|
||||
|
||||
public function scopusTest(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
|
||||
@@ -1708,11 +1708,20 @@ class Journal extends Controller
|
||||
public function addJournalAbs()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
'journal_id' => 'require',
|
||||
'title' => 'require',
|
||||
'url' => 'require',
|
||||
]);
|
||||
if (!$rule->check($data)) {
|
||||
return json(['code' => 1, 'msg' => $rule->getError()]);
|
||||
}
|
||||
$add_data['journal_id'] = $data['journal_id'];
|
||||
$add_data['title'] = $data['title'];
|
||||
$add_data['url'] = $data['url'];
|
||||
$add_data['sort'] = $data['sort'];
|
||||
$add_data['is_show'] = $data['is_show'];
|
||||
$add_data['sort'] = isset($data['sort']) ? intval($data['sort']) : 0;
|
||||
$add_data['is_show'] = isset($data['is_show']) ? intval($data['is_show']) : 1;
|
||||
$add_data['state'] = 0;
|
||||
$res = $this->journal_abs_obj->insert($add_data);
|
||||
if ($res) {
|
||||
return json(['code' => 0, 'msg' => 'success']);
|
||||
@@ -1754,7 +1763,46 @@ class Journal extends Controller
|
||||
public function editJournalAbs()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$this->journal_abs_obj->update($data);
|
||||
$rule = new Validate([
|
||||
'journal_abstracting_id' => 'require',
|
||||
]);
|
||||
if (!$rule->check($data)) {
|
||||
return json(['code' => 1, 'msg' => $rule->getError()]);
|
||||
}
|
||||
// 白名单,避免误更新未知字段(数据库开启了 fields_strict)
|
||||
$update = [];
|
||||
foreach (['journal_id', 'title', 'url', 'sort', 'is_show'] as $f) {
|
||||
if (isset($data[$f])) {
|
||||
$update[$f] = $data[$f];
|
||||
}
|
||||
}
|
||||
if (empty($update)) {
|
||||
return json(['code' => 1, 'msg' => 'nothing to update']);
|
||||
}
|
||||
$this->journal_abs_obj->where('journal_abstracting_id', $data['journal_abstracting_id'])->update($update);
|
||||
return json(['code' => 0, 'msg' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title 批量排序期刊外链
|
||||
* @description 拖拽排序后批量保存收录数据库外链顺序
|
||||
* @url /master/Journal/sortJournalAbs
|
||||
* @method POST
|
||||
*
|
||||
* @param name:ids type:string require:1 desc:journal_abstracting_id 顺序数组(从大到小排序权重递减)
|
||||
*/
|
||||
public function sortJournalAbs()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$ids = isset($data['ids']) ? $data['ids'] : [];
|
||||
if (!is_array($ids) || empty($ids)) {
|
||||
return json(['code' => 1, 'msg' => 'ids 不能为空']);
|
||||
}
|
||||
// 数组顺序即展示顺序:越靠前 sort 越大
|
||||
$total = count($ids);
|
||||
foreach ($ids as $index => $id) {
|
||||
$this->journal_abs_obj->where('journal_abstracting_id', $id)->update(['sort' => $total - $index]);
|
||||
}
|
||||
return json(['code' => 0, 'msg' => 'success']);
|
||||
}
|
||||
|
||||
|
||||
89
application/master/service/DbPushLogger.php
Normal file
89
application/master/service/DbPushLogger.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 推送日志读写
|
||||
*/
|
||||
class DbPushLogger
|
||||
{
|
||||
const STATUS_PENDING = 0; // 待推送 / 排队中
|
||||
const STATUS_SUCCESS = 1;
|
||||
const STATUS_FAIL = 2;
|
||||
|
||||
protected $obj;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->obj = Db::name('db_push_log');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一条待推送日志,返回 log_id
|
||||
*/
|
||||
public function create($journalId, $stageId, $channelKey, $channelName, $operator = '', $articleId = 0)
|
||||
{
|
||||
$now = time();
|
||||
try {
|
||||
return $this->obj->insertGetId([
|
||||
'journal_id' => (int) $journalId,
|
||||
'journal_stage_id' => (int) $stageId,
|
||||
'article_id' => (int) $articleId,
|
||||
'channel' => $channelKey,
|
||||
'channel_name' => $channelName,
|
||||
'status' => self::STATUS_PENDING,
|
||||
'attempt' => 0,
|
||||
'message' => '',
|
||||
'operator' => $operator,
|
||||
'ctime' => $now,
|
||||
'utime' => $now,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// 日志表尚未创建时,不影响推送本身
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记结果
|
||||
*/
|
||||
public function finish($logId, $ok, $message = '')
|
||||
{
|
||||
if (empty($logId)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->obj->where('log_id', $logId)->update([
|
||||
'status' => $ok ? self::STATUS_SUCCESS : self::STATUS_FAIL,
|
||||
'message' => mb_substr((string) $message, 0, 2000),
|
||||
'utime' => time(),
|
||||
]);
|
||||
$this->obj->where('log_id', $logId)->setInc('attempt');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public function find($logId)
|
||||
{
|
||||
try {
|
||||
return $this->obj->where('log_id', $logId)->find();
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 某一期的推送情况
|
||||
*/
|
||||
public function stageStatus($stageId)
|
||||
{
|
||||
try {
|
||||
return $this->obj->where('journal_stage_id', $stageId)->order('log_id desc')->select();
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
202
application/master/service/DbPushManager.php
Normal file
202
application/master/service/DbPushManager.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service;
|
||||
|
||||
use think\Db;
|
||||
use think\Queue;
|
||||
use app\master\service\push\PushResult;
|
||||
use app\master\service\push\PorticoPusher;
|
||||
use app\master\service\push\ScopusPusher;
|
||||
use app\master\service\push\EbscoPusher;
|
||||
use app\master\service\push\CnkiPusher;
|
||||
use app\master\service\push\EmailDataPusher;
|
||||
|
||||
/**
|
||||
* 数据库收录推送编排器
|
||||
* 职责:根据分期解析应推送的渠道 -> 逐个执行(或入队) -> 写推送日志。
|
||||
*/
|
||||
class DbPushManager
|
||||
{
|
||||
/** 推送器标识 => 实现类 */
|
||||
protected static $registry = [
|
||||
'portico' => PorticoPusher::class,
|
||||
'scopus' => ScopusPusher::class,
|
||||
'ebsco' => EbscoPusher::class,
|
||||
'cnki' => CnkiPusher::class,
|
||||
'email_data' => EmailDataPusher::class,
|
||||
];
|
||||
|
||||
/** @var DbPushLogger */
|
||||
protected $logger;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logger = new DbPushLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例化推送器
|
||||
* @return \app\master\service\push\PusherInterface|null
|
||||
*/
|
||||
public static function makePusher($code)
|
||||
{
|
||||
if (!isset(self::$registry[$code])) {
|
||||
return null;
|
||||
}
|
||||
$class = self::$registry[$code];
|
||||
return new $class();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析某期刊应推送的渠道列表(配置驱动,数据表优先,回退默认配置)
|
||||
* @return array 每项 [channel_key, name, pusher, options]
|
||||
*/
|
||||
public function resolveChannels($journalId)
|
||||
{
|
||||
try {
|
||||
$rows = Db::name('db_channel')->where('state', 0)->where('is_enable', 1)->order('sort desc')->select();
|
||||
} catch (\Throwable $e) {
|
||||
// 渠道表尚未创建时回退到默认配置
|
||||
$rows = [];
|
||||
}
|
||||
|
||||
$channels = [];
|
||||
if (!empty($rows)) {
|
||||
foreach ($rows as $r) {
|
||||
$journalIds = trim($r['journal_ids']) === '' ? [] : array_map('intval', explode(',', $r['journal_ids']));
|
||||
$options = empty($r['config']) ? [] : (json_decode($r['config'], true) ?: []);
|
||||
$channels[] = [
|
||||
'channel_key' => $r['channel_key'],
|
||||
'name' => $r['name'],
|
||||
'pusher' => $r['pusher'] !== '' ? $r['pusher'] : $r['channel_key'],
|
||||
'journal_ids' => $journalIds,
|
||||
'options' => $options,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
foreach ((array) config('dbpush.default_channels') as $c) {
|
||||
if (empty($c['is_enable'])) {
|
||||
continue;
|
||||
}
|
||||
$channels[] = [
|
||||
'channel_key' => $c['channel_key'],
|
||||
'name' => $c['name'],
|
||||
'pusher' => !empty($c['pusher']) ? $c['pusher'] : $c['channel_key'],
|
||||
'journal_ids' => isset($c['journal_ids']) ? $c['journal_ids'] : [],
|
||||
'options' => isset($c['config']) ? $c['config'] : [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 按期刊过滤:journal_ids 为空表示全部
|
||||
$result = [];
|
||||
foreach ($channels as $c) {
|
||||
if (empty($c['journal_ids']) || in_array((int) $journalId, $c['journal_ids'])) {
|
||||
$result[] = $c;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送整期到所有匹配渠道
|
||||
* @param int $stageId
|
||||
* @param string $operator 触发人
|
||||
* @param bool|null $async 是否异步,null 用配置默认
|
||||
* @return array 各渠道执行概况
|
||||
*/
|
||||
public function run($stageId, $operator = '', $async = null)
|
||||
{
|
||||
if ($async === null) {
|
||||
$async = (bool) config('dbpush.async');
|
||||
}
|
||||
$stage_info = Db::name('journal_stage')->where('journal_stage_id', $stageId)->find();
|
||||
if (!$stage_info) {
|
||||
return [];
|
||||
}
|
||||
$journalId = $stage_info['journal_id'];
|
||||
$channels = $this->resolveChannels($journalId);
|
||||
|
||||
$summary = [];
|
||||
foreach ($channels as $c) {
|
||||
$logId = $this->logger->create($journalId, $stageId, $c['channel_key'], $c['name'], $operator);
|
||||
if ($async) {
|
||||
Queue::push('app\api\job\dbpush@handle', [
|
||||
'log_id' => $logId,
|
||||
'stage_id' => $stageId,
|
||||
'pusher' => $c['pusher'],
|
||||
'options' => $c['options'],
|
||||
], 'dbpush');
|
||||
$summary[] = ['channel' => $c['channel_key'], 'log_id' => $logId, 'queued' => true];
|
||||
} else {
|
||||
$ok = $this->executeAndLog($logId, $stageId, $c['pusher'], $c['options']);
|
||||
$summary[] = ['channel' => $c['channel_key'], 'log_id' => $logId, 'ok' => $ok];
|
||||
}
|
||||
}
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个推送器并写入日志结果(供同步执行 / 队列任务调用)
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public function executeAndLog($logId, $stageId, $pusherCode, array $options = [])
|
||||
{
|
||||
try {
|
||||
$pusher = self::makePusher($pusherCode);
|
||||
if (!$pusher) {
|
||||
$this->logger->finish($logId, false, '未知的推送器: ' . $pusherCode);
|
||||
return false;
|
||||
}
|
||||
$result = $pusher->pushStage($stageId, $options);
|
||||
if (!$result instanceof PushResult) {
|
||||
$result = PushResult::success();
|
||||
}
|
||||
$this->logger->finish($logId, $result->ok, $result->message);
|
||||
return $result->ok;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->finish($logId, false, $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重推某条日志
|
||||
* @return array [ok=>bool, queued=>bool]
|
||||
*/
|
||||
public function retry($logId, $async = null)
|
||||
{
|
||||
if ($async === null) {
|
||||
$async = (bool) config('dbpush.async');
|
||||
}
|
||||
$log = $this->logger->find($logId);
|
||||
if (!$log) {
|
||||
return ['ok' => false, 'queued' => false, 'msg' => '日志不存在'];
|
||||
}
|
||||
// 依据日志里的渠道标识重新解析推送器与配置
|
||||
$channels = $this->resolveChannels($log['journal_id']);
|
||||
$pusher = $log['channel'];
|
||||
$options = [];
|
||||
foreach ($channels as $c) {
|
||||
if ($c['channel_key'] === $log['channel']) {
|
||||
$pusher = $c['pusher'];
|
||||
$options = $c['options'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 重置为待推送
|
||||
Db::name('db_push_log')->where('log_id', $logId)->update(['status' => DbPushLogger::STATUS_PENDING, 'utime' => time()]);
|
||||
|
||||
if ($async) {
|
||||
Queue::push('app\api\job\dbpush@handle', [
|
||||
'log_id' => $logId,
|
||||
'stage_id' => $log['journal_stage_id'],
|
||||
'pusher' => $pusher,
|
||||
'options' => $options,
|
||||
], 'dbpush');
|
||||
return ['ok' => true, 'queued' => true];
|
||||
}
|
||||
$ok = $this->executeAndLog($logId, $log['journal_stage_id'], $pusher, $options);
|
||||
return ['ok' => $ok, 'queued' => false];
|
||||
}
|
||||
}
|
||||
162
application/master/service/XmlBuilder.php
Normal file
162
application/master/service/XmlBuilder.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 文章 JATS XML 生成器
|
||||
* 逻辑迁移自 Datebase::xmlCreateForArticle,保持输出完全一致,供 Scopus / EBSCO 推送复用。
|
||||
*/
|
||||
class XmlBuilder
|
||||
{
|
||||
/**
|
||||
* 为文章生成 XML 文件并返回文件绝对路径(已存在则直接返回)
|
||||
* @param int $article_id
|
||||
* @return string
|
||||
*/
|
||||
public static function forArticle($article_id)
|
||||
{
|
||||
$article_obj = Db::name('article');
|
||||
$journal_obj = Db::name('journal');
|
||||
$journal_stage_obj = Db::name('journal_stage');
|
||||
$article_author_obj = Db::name('article_author');
|
||||
$article_organ_obj = Db::name('article_organ');
|
||||
$article_author_to_organ_obj = Db::name('article_author_to_organ');
|
||||
|
||||
$article_info = $article_obj->where("article_id", $article_id)->find();
|
||||
$journal_info = $journal_obj->where("journal_id", $article_info['journal_id'])->find();
|
||||
|
||||
$file = ROOT_PATH . "public/articleXML/" . substr($article_info['doi'], strpos($article_info['doi'], "/") + 1) . ".xml";
|
||||
if (is_file($file)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
$xml = new \SimpleXMLElement('<article/>');
|
||||
// 设置命名空间
|
||||
$xml->addAttribute('xmlns:mml', 'http://www.w3.org/1998/Math/MathML');
|
||||
$xml->addAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||
$xml->addAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
|
||||
$xml->addAttribute('article-type', $article_info['type']);
|
||||
|
||||
// 创建 <front> 元素
|
||||
$front = $xml->addChild('front');
|
||||
|
||||
// 创建 <journal-meta> 元素并添加子元素
|
||||
$journalMeta = $front->addChild('journal-meta');
|
||||
$journalMeta->addChild('journal-id', $journal_info['usx']);
|
||||
$journalTitleGroup = $journalMeta->addChild('journal-title-group');
|
||||
$journalTitleGroup->addChild('journal-title', $journal_info['title'])->addAttribute('xml:lang', 'en');
|
||||
$journalTitleGroup->addChild('abbrev-journal-title', $journal_info['jabbr']);
|
||||
$journalMeta->addChild('issn', $journal_info['issn'])->addAttribute('pub-type', 'ppub');
|
||||
$publisher = $journalMeta->addChild('publisher');
|
||||
$publisher->addChild('publisher-name', 'TMR publisher group');
|
||||
$publisher->addChild('publisher-name', 'TMR')->addAttribute('specific-use', 'short-name');
|
||||
|
||||
// 创建 <article-meta> 元素并添加子元素
|
||||
$articleMeta = $front->addChild('article-meta');
|
||||
$articleMeta->addChild('article-id', $article_info['article_id']);
|
||||
$articleMeta->addChild('article-id', $article_info['doi'])->addAttribute('pub-id-type', 'doi');
|
||||
|
||||
$articleCategories = $articleMeta->addChild('article-categories');
|
||||
$subjGroup = $articleCategories->addChild('subj-group');
|
||||
$subjGroup->addChild('subject', 'subject')->addAttribute('xml:lang', 'en');
|
||||
|
||||
$articleTypeGroup = $articleCategories->addChild('subj-group');
|
||||
$articleTypeGroup->addAttribute('id', 'article-type');
|
||||
$articleTypeGroup->addChild('subject', $article_info['type'])->addAttribute('xml:lang', 'en');
|
||||
|
||||
// 添加 <title-group> 元素
|
||||
$titleGroup = $articleMeta->addChild('title-group');
|
||||
$titleGroup->addChild('article-title', $article_info['title'])->addAttribute('xml:lang', 'en');
|
||||
$transTitleGroup = $titleGroup->addChild('trans-title-group');
|
||||
$transTitleGroup->addAttribute('xml:lang', 'en');
|
||||
$transTitleGroup->addChild('trans-title', $article_info['title'])->addAttribute('xml:lang', 'en');
|
||||
|
||||
// 添加 <contrib-group> 和相关的 <contrib> 元素
|
||||
$contribGroup = $articleMeta->addChild('contrib-group');
|
||||
$authors = $article_author_obj->where("article_id", $article_id)->where("state", 0)->select();
|
||||
$organs = $article_organ_obj->where("article_id", $article_id)->select();
|
||||
$os = [];
|
||||
foreach ($organs as $k => $v) {
|
||||
$os[$v['article_organ_id']] = $k + 1;
|
||||
}
|
||||
foreach ($authors as $v) {
|
||||
$contrib_cache = $contribGroup->addChild('contrib');
|
||||
$contrib_cache->addAttribute('contrib-type', 'author');
|
||||
$nameAlternatives_cache = $contrib_cache->addChild('name-alternatives');
|
||||
$name_cache = $nameAlternatives_cache->addChild("name");
|
||||
$name_cache->addAttribute("name-style", "western");
|
||||
$name_cache->addAttribute("specific-use", "en");
|
||||
$name_cache->addAttribute("xml:lang", "en");
|
||||
$name_cache->addChild("surname", $v['first_name']);
|
||||
$name_cache->addChild("given-names", $v['last_name']);
|
||||
$l_c = $article_author_to_organ_obj->where("article_id", $article_id)->where("article_author_id", $v['article_author_id'])->where("state", 0)->select();
|
||||
foreach ($l_c as $val) {
|
||||
$xref_cache = $contrib_cache->addChild("xref");
|
||||
$xref_cache->addAttribute("rid", "aff" . $os[$val['article_organ_id']]);
|
||||
$xref_cache->addAttribute("xml:base", "aff");
|
||||
$xref_cache->addAttribute("xml:lang", "en");
|
||||
$xref_cache->addChild("sup", $os[$val['article_organ_id']]);
|
||||
}
|
||||
}
|
||||
foreach ($organs as $v) {
|
||||
$affAl_cache = $contribGroup->addChild("aff-alternatives");
|
||||
$affAl_cache->addAttribute("id", "aff" . $os[$v['article_organ_id']]);
|
||||
$aff_cache = $affAl_cache->addChild("aff");
|
||||
$aff_cache->addAttribute("xml:lang", "en");
|
||||
$aff_cache->addChild("label", htmlspecialchars($os[$v['article_organ_id']] . " " . $v['organ_name']));
|
||||
$ins_cache = $aff_cache->addChild("institution ", htmlspecialchars($v['organ_name']));
|
||||
$ins_cache->addAttribute("content-type", 'orgname');
|
||||
}
|
||||
|
||||
// pub-date
|
||||
$stage_info = $journal_stage_obj->where("journal_stage_id", $article_info['journal_stage_id'])->find();
|
||||
if ($article_info['pub_date'] != "") {
|
||||
$pubDate = $articleMeta->addChild("pub_date");
|
||||
$pub_date_list = explode(" ", $article_info['pub_date']);
|
||||
$pubDate->addChild("year", $pub_date_list[2]);
|
||||
$pubDate->addChild("month", $pub_date_list[1]);
|
||||
}
|
||||
$vol = $articleMeta->addChild("volume", $stage_info['stage_vol']);
|
||||
$vol->addAttribute("content-type", "Vol.");
|
||||
$issue = $articleMeta->addChild("issue", $article_info['npp']);
|
||||
$issue->addAttribute("content-type", "No.");
|
||||
|
||||
// abstract
|
||||
$abstract = $articleMeta->addChild("abstract");
|
||||
$abstract->addAttribute("xml:lang", "en");
|
||||
$abs_p = $abstract->addChild("p", htmlspecialchars($article_info['abstract'], ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$abs_p->addAttribute("indent", "0mm");
|
||||
|
||||
// keywords
|
||||
$kwd = $articleMeta->addChild("kwd-group");
|
||||
$kwd->addAttribute("kwd-group-type", "inspec");
|
||||
$kwd->addAttribute("xml:lang", "en");
|
||||
$keywords_list = explode(";", $article_info['keywords']);
|
||||
foreach ($keywords_list as $v) {
|
||||
if ($v == "") {
|
||||
continue;
|
||||
}
|
||||
$kwd->addChild("kwd", trim($v));
|
||||
}
|
||||
|
||||
// notes
|
||||
$notes = $front->addChild("notes");
|
||||
$notes_p = $notes->addChild("p");
|
||||
$notes_p->addAttribute("ontent-type", "qrcode");
|
||||
$notes_p->addAttribute("xml:specific-use", "website");
|
||||
$url = "https://www.tmrjournals.com/article.html?J_num=" . $journal_info['journal_id'] . "&a_id=" . $article_info['article_id'];
|
||||
$notes_p_uri = $notes_p->addChild("uri", htmlspecialchars($url));
|
||||
$notes_p_uri->addAttribute("xlink:href", htmlspecialchars($url));
|
||||
$notes_p1 = $notes->addChild("p");
|
||||
$notes_p1->addAttribute("xml:specific-use", "crossmark");
|
||||
$notes_p1_uri = $notes_p1->addChild("uri");
|
||||
$notes_p1_uri->addAttribute("xlink:href", "http://crossmark.crossref.org/dialog/?doi=" . $article_info['doi']);
|
||||
|
||||
$x = $xml->asXML();
|
||||
|
||||
file_put_contents($file, $x);
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
53
application/master/service/push/AbstractPusher.php
Normal file
53
application/master/service/push/AbstractPusher.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 推送器基类:提供数据表句柄、凭证读取等公共能力
|
||||
*/
|
||||
abstract class AbstractPusher implements PusherInterface
|
||||
{
|
||||
protected $journal_obj;
|
||||
protected $journal_stage_obj;
|
||||
protected $article_obj;
|
||||
protected $article_author_obj;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->journal_obj = Db::name('journal');
|
||||
$this->journal_stage_obj = Db::name('journal_stage');
|
||||
$this->article_obj = Db::name('article');
|
||||
$this->article_author_obj = Db::name('article_author');
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取某个数据库的凭证配置
|
||||
* @param string $key credentials.<key>
|
||||
* @return array
|
||||
*/
|
||||
protected function cred($key)
|
||||
{
|
||||
$all = config('dbpush.credentials');
|
||||
return isset($all[$key]) ? $all[$key] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取顶层 dbpush 配置项
|
||||
*/
|
||||
protected function conf($key, $default = null)
|
||||
{
|
||||
$val = config('dbpush.' . $key);
|
||||
return $val === null ? $default : $val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组合文章作者名(逗号分隔)
|
||||
*/
|
||||
protected function getAuthorNames($articleId)
|
||||
{
|
||||
$list = $this->article_author_obj->where('article_id', $articleId)->where('state', 0)->column('author_name');
|
||||
return implode(', ', $list);
|
||||
}
|
||||
}
|
||||
146
application/master/service/push/CnkiPusher.php
Normal file
146
application/master/service/push/CnkiPusher.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
/**
|
||||
* CNKI 中国知网 推送器
|
||||
* 生成整期 Excel 并打包 zip,通过 FTP 上传到知网。
|
||||
* 逻辑迁移自 Datebase::createFtpFileInfo / createZip / sendFtp。
|
||||
*/
|
||||
class CnkiPusher extends AbstractPusher
|
||||
{
|
||||
public function code()
|
||||
{
|
||||
return 'cnki';
|
||||
}
|
||||
|
||||
public function label()
|
||||
{
|
||||
return 'CNKI 中国知网';
|
||||
}
|
||||
|
||||
public function pushStage($journal_stage_id, array $options = [])
|
||||
{
|
||||
$stage_info = $this->journal_stage_obj->where("journal_stage_id", $journal_stage_id)->find();
|
||||
$journalInfo = $this->journal_obj->where('journal_id', $stage_info['journal_id'])->find();
|
||||
$articles = $this->article_obj->where(['journal_stage_id' => $journal_stage_id, 'state' => 0])
|
||||
->field('title as articleTitle,abbr as author,pub_date as date,keywords as keyWords,doi,abstract,file_pdf as pdf,article_id,journal_id')
|
||||
->select();
|
||||
|
||||
foreach ($articles as $k => $v) {
|
||||
$articles[$k]['abstract'] = strip_tags($v['abstract']);
|
||||
$articles[$k]['journalTitle'] = $journalInfo['title'];
|
||||
$articles[$k]['issn'] = $journalInfo['issn'];
|
||||
$articles[$k]['vol'] = $stage_info['stage_vol'];
|
||||
$articles[$k]['issue'] = $stage_info['stage_no'];
|
||||
$articles[$k]['pdf'] = 'https://www.tmrjournals.com/public/articlePDF/' . $v['pdf'];
|
||||
$articles[$k]['linkToAbstract'] = 'https://www.tmrjournals.com/article.html?J_num=' . $v['journal_id'] . '&a_id=' . $v['article_id'];
|
||||
$articles[$k]['publisher'] = 'TMR publishing group';
|
||||
$articles[$k]['reference'] = '';
|
||||
$articles[$k]['eissn'] = '';
|
||||
}
|
||||
$baseDir = ROOT_PATH . 'public' . DS . 'ftpFile' . DS;
|
||||
$outfileTitle = $journalInfo['title'] . "-" . $stage_info['stage_vol'] . "卷" . $stage_info['stage_no'] . "期";
|
||||
|
||||
$res = $this->createZip($articles, $baseDir, $outfileTitle);
|
||||
$resJosn = json_decode($res, true);
|
||||
if ($resJosn['code'] != 0) {
|
||||
throw new \Exception('CNKI zip 生成失败: ' . $resJosn['msg']);
|
||||
}
|
||||
$url = $baseDir . $outfileTitle . ".zip";
|
||||
$this->sendFtp($url, $outfileTitle);
|
||||
|
||||
return PushResult::success('CNKI zip 已生成并上传');
|
||||
}
|
||||
|
||||
private function createZip($articles, $baseDir, $outfileTitle)
|
||||
{
|
||||
Vendor('PHPExcel.PHPExcel');
|
||||
Vendor('PHPExcel.PHPExcel.Worksheet.Drawing');
|
||||
Vendor('PHPExcel.PHPExcel.Writer.Excel2007');
|
||||
$objExcel = new \PHPExcel();
|
||||
$objWriter = \PHPExcel_IOFactory::createWriter($objExcel, 'Excel2007');
|
||||
|
||||
$objActSheet = $objExcel->getActiveSheet();
|
||||
$letter = explode(',', "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O");
|
||||
$arrHeader = array('Article Title', 'Author', 'Publishing Date', 'Key Words', 'Journal Title', 'ISSN', 'EISSN', 'Volume', 'Issue', 'Link to Full-Text Articles (PDF)', 'Abstract', 'DOI', 'Link to Abstract', 'Publisher', 'Reference');
|
||||
$lenth = count($arrHeader);
|
||||
for ($i = 0; $i < $lenth; $i++) {
|
||||
$objActSheet->setCellValue("$letter[$i]1", "$arrHeader[$i]");
|
||||
$objActSheet->getRowDimension('1')->setRowHeight(25);
|
||||
}
|
||||
foreach ($articles as $k => $v) {
|
||||
$k += 2;
|
||||
$objActSheet->setCellValue('A' . $k, $v['articleTitle']);
|
||||
$objActSheet->setCellValue('B' . $k, $v['author']);
|
||||
$objActSheet->setCellValue('C' . $k, $v['date']);
|
||||
$objActSheet->setCellValue('D' . $k, $v['keyWords']);
|
||||
$objActSheet->setCellValue('E' . $k, $v['journalTitle']);
|
||||
$objActSheet->setCellValue('F' . $k, $v['issn']);
|
||||
$objActSheet->setCellValue('G' . $k, $v['eissn']);
|
||||
$objActSheet->setCellValue('H' . $k, $v['vol']);
|
||||
$objActSheet->setCellValue('I' . $k, $v['issue']);
|
||||
$objActSheet->setCellValue('J' . $k, $v['pdf']);
|
||||
$objActSheet->setCellValue('K' . $k, $v['abstract']);
|
||||
$objActSheet->setCellValue('L' . $k, $v['doi']);
|
||||
$objActSheet->setCellValue('M' . $k, $v['linkToAbstract']);
|
||||
$objActSheet->setCellValue('N' . $k, $v['publisher']);
|
||||
$objActSheet->setCellValue('O' . $k, $v['reference']);
|
||||
$objActSheet->getRowDimension($k)->setRowHeight(25);
|
||||
}
|
||||
$width = array(10, 15, 20, 25, 30);
|
||||
$objActSheet->getColumnDimension('A')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('B')->setWidth($width[2]);
|
||||
$objActSheet->getColumnDimension('C')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('D')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('E')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('F')->setWidth($width[2]);
|
||||
$objActSheet->getColumnDimension('G')->setWidth($width[3]);
|
||||
$objActSheet->getColumnDimension('H')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('I')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('J')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('K')->setWidth($width[3]);
|
||||
$objActSheet->getColumnDimension('L')->setWidth($width[3]);
|
||||
$objActSheet->getColumnDimension('M')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('N')->setWidth($width[2]);
|
||||
$objActSheet->getColumnDimension('O')->setWidth($width[1]);
|
||||
if (!is_dir($baseDir)) {
|
||||
@mkdir($baseDir, 0777, true);
|
||||
}
|
||||
$objWriter->save($baseDir . $outfileTitle . ".xlsx");
|
||||
|
||||
$path = $baseDir . $outfileTitle . ".xlsx";
|
||||
$fileName = $outfileTitle . ".zip";
|
||||
$zip = new \ZipArchive;
|
||||
$zip->open($baseDir . $fileName, \ZipArchive::CREATE);
|
||||
$zip->addFile($path, basename($path));
|
||||
$zip->close();
|
||||
unlink($baseDir . $outfileTitle . ".xlsx");
|
||||
|
||||
if (!file_exists($baseDir . $fileName)) {
|
||||
return json_encode(['code' => 1, 'msg' => '无法找到文件']);
|
||||
}
|
||||
return json_encode(['code' => 0, 'msg' => '成功', 'data' => $baseDir . $fileName]);
|
||||
}
|
||||
|
||||
private function sendFtp($url, $title)
|
||||
{
|
||||
$cred = $this->cred('cnki');
|
||||
$con = ftp_connect($cred['host'], $cred['port']);
|
||||
if (!$con) {
|
||||
throw new \Exception('CNKI FTP 连接失败');
|
||||
}
|
||||
$res = ftp_login($con, $cred['user'], $cred['pass']);
|
||||
if (!$res) {
|
||||
ftp_close($con);
|
||||
throw new \Exception('CNKI FTP 登录失败');
|
||||
}
|
||||
$name = iconv("UTF-8", "GBK", $title . '.zip');
|
||||
ftp_pasv($con, true);
|
||||
$data = ftp_put($con, '/' . $name, $url, FTP_BINARY);
|
||||
ftp_close($con);
|
||||
if (!$data) {
|
||||
throw new \Exception('CNKI FTP 上传失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
62
application/master/service/push/EbscoPusher.php
Normal file
62
application/master/service/push/EbscoPusher.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
use app\master\service\XmlBuilder;
|
||||
|
||||
/**
|
||||
* EBSCO 推送器
|
||||
* 为每篇文章生成 XML,连同 PDF 通过 FTP 上传到 EBSCO。
|
||||
* 逻辑迁移自 Datebase::pushXmlToEp / pushFtpForEp。
|
||||
*/
|
||||
class EbscoPusher extends AbstractPusher
|
||||
{
|
||||
public function code()
|
||||
{
|
||||
return 'ebsco';
|
||||
}
|
||||
|
||||
public function label()
|
||||
{
|
||||
return 'EBSCO';
|
||||
}
|
||||
|
||||
public function pushStage($stage_id, array $options = [])
|
||||
{
|
||||
$stage_info = $this->journal_stage_obj->where("journal_stage_id", $stage_id)->find();
|
||||
$list = $this->article_obj->where("journal_stage_id", $stage_id)->select();
|
||||
$journal_info = $this->journal_obj->where("journal_id", $stage_info['journal_id'])->find();
|
||||
$count = 0;
|
||||
foreach ($list as $v) {
|
||||
$local_file = XmlBuilder::forArticle($v['article_id']);
|
||||
$file = substr($v['doi'], strpos($v['doi'], "/") + 1) . ".xml";
|
||||
$remote_file = "/" . $journal_info['title'] . "/" . $stage_info['stage_year'] . "v" . $stage_info['stage_vol'] . "n" . $stage_info['stage_no'] . "-" . $file;
|
||||
$this->pushFtp($remote_file, $local_file);
|
||||
$localPdf = ROOT_PATH . "public/articlePDF/" . $v['file_pdf'];
|
||||
$remotePdf = "/" . $journal_info['title'] . "/" . $stage_info['stage_year'] . "v" . $stage_info['stage_vol'] . "n" . $stage_info['stage_no'] . "-" . substr($v['doi'], strpos($v['doi'], "/") + 1) . ".pdf";
|
||||
$this->pushFtp($remotePdf, $localPdf);
|
||||
$count++;
|
||||
}
|
||||
return PushResult::success("EBSCO 已上传 {$count} 篇文章的 XML/PDF");
|
||||
}
|
||||
|
||||
private function pushFtp($remote_file, $local_file)
|
||||
{
|
||||
$cred = $this->cred('ebsco');
|
||||
$con = ftp_connect($cred['host'], $cred['port']);
|
||||
if (!$con) {
|
||||
throw new \Exception('EBSCO FTP 连接失败');
|
||||
}
|
||||
$res = ftp_login($con, $cred['user'], $cred['pass']);
|
||||
if (!$res) {
|
||||
ftp_close($con);
|
||||
throw new \Exception('EBSCO FTP 登录失败');
|
||||
}
|
||||
ftp_pasv($con, true);
|
||||
$data = ftp_put($con, $remote_file, $local_file, FTP_BINARY);
|
||||
ftp_close($con);
|
||||
if (!$data) {
|
||||
throw new \Exception('EBSCO FTP 上传失败: ' . $remote_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
application/master/service/push/EmailDataPusher.php
Normal file
106
application/master/service/push/EmailDataPusher.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
use think\Queue;
|
||||
|
||||
/**
|
||||
* 邮件送交推送器(通用数据库 / 万方等)
|
||||
* 打包整期 docx + PDF + 封面 zip,作为附件发邮件给数据库收件人。
|
||||
* 逻辑迁移自 Datebase::dataPush 的邮件部分 + createEmailFile。
|
||||
*/
|
||||
class EmailDataPusher extends AbstractPusher
|
||||
{
|
||||
public function code()
|
||||
{
|
||||
return 'email_data';
|
||||
}
|
||||
|
||||
public function label()
|
||||
{
|
||||
return '数据库邮件送交';
|
||||
}
|
||||
|
||||
public function pushStage($journal_stage_id, array $options = [])
|
||||
{
|
||||
$stage_info = $this->journal_stage_obj->where("journal_stage_id", $journal_stage_id)->find();
|
||||
$journal_info = $this->journal_obj->where("journal_id", $stage_info["journal_id"])->find();
|
||||
|
||||
// 幂等构建整期附件 zip
|
||||
$zipfile = ROOT_PATH . "public/dataFile/" . $journal_stage_id . '/' . $journal_stage_id . '.zip';
|
||||
if (!file_exists($zipfile)) {
|
||||
$this->buildIssueZip($journal_stage_id, $stage_info, $journal_info);
|
||||
}
|
||||
|
||||
// 收件人:渠道配置优先,否则用默认通用收件人
|
||||
$recipients = !empty($options['recipients']) ? $options['recipients'] : $this->conf('data_recipients', []);
|
||||
if (empty($recipients)) {
|
||||
return PushResult::fail('未配置收件人');
|
||||
}
|
||||
|
||||
$tt = "Dear Database,<br><br>";
|
||||
$tt .= "Please find the PDF files in attachment of published issue (Vol. " . $stage_info['stage_vol'] . ", No." . $stage_info['stage_no'] . ") of " . $journal_info['title'] . ".<br><br>";
|
||||
$tt .= "Yours Sincerely<br>";
|
||||
$tt .= "Dan Chen<br>";
|
||||
$tt .= "Manager<br>";
|
||||
$tt .= "TMR Publishing Group | Editorial Office | New Zealand<br>";
|
||||
$tt .= "Telephone: +64 02108293806<br>";
|
||||
$tt .= "Email: publisher@tmrjournals.com<br>";
|
||||
$tt .= "Website:www.tmrjournals.com";
|
||||
|
||||
foreach ($recipients as $v) {
|
||||
$cdata['email'] = $v;
|
||||
$cdata['title'] = "Data submitted-TMR Publishing Group";
|
||||
$cdata["fromname"] = "Data submitted-TMR Publishing Group";
|
||||
$cdata["content"] = $tt;
|
||||
$cdata["temail"] = $journal_info['email'];
|
||||
$cdata["tpassword"] = $journal_info['epassword'];
|
||||
$cdata["fj"] = $zipfile;
|
||||
Queue::push('app\api\job\mail@puchAndFJ', $cdata, "mail");
|
||||
}
|
||||
|
||||
return PushResult::success('已向 ' . count($recipients) . ' 个收件人加入邮件队列');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建整期 zip(Contents.docx + 各文章 PDF + 封面)
|
||||
*/
|
||||
private function buildIssueZip($journal_stage_id, $stage_info, $journal_info)
|
||||
{
|
||||
$ts_base_url = $this->conf('ts_base_url');
|
||||
$pdfBase = $this->conf('article_pdf');
|
||||
$publicBase = $this->conf('public_base');
|
||||
|
||||
$url = $ts_base_url . "api/dataApi/createTemplate";
|
||||
$re['issue'] = $stage_info['issue_date'] . ", Volume " . $stage_info['stage_vol'] . " Issue " . $stage_info["stage_no"];
|
||||
$adate = [];
|
||||
$alist = $this->article_obj->where("journal_stage_id", $journal_stage_id)->where("state", 0)->orderRaw("npp + 0")->select();
|
||||
foreach ($alist as $v) {
|
||||
$cache["type"] = $v["type"];
|
||||
$cache["no"] = $v["npp"];
|
||||
$cache["title"] = $v["title"];
|
||||
$cache["author"] = $this->getAuthorNames($v['article_id']);
|
||||
$adate[] = $cache;
|
||||
}
|
||||
$re["date_list"] = json_encode($adate);
|
||||
$res = myPost($url, $re);
|
||||
$r = object_to_array(json_decode($res));
|
||||
$file = $r['data']["file"];
|
||||
|
||||
$base_dir = ROOT_PATH . "public/dataFile/" . $stage_info["journal_stage_id"];
|
||||
if (!is_dir($base_dir)) {
|
||||
@mkdir($base_dir, 0777, true);
|
||||
}
|
||||
$zip = new \ZipArchive;
|
||||
$zip->open($base_dir . '/' . $stage_info["journal_stage_id"] . '.zip', \ZipArchive::CREATE);
|
||||
copy($ts_base_url . 'upload/' . $file, $base_dir . '/' . $stage_info["journal_stage_id"] . ".docx");
|
||||
$zip->addFile($base_dir . '/' . $stage_info["journal_stage_id"] . ".docx", "Contents.docx");
|
||||
foreach ($alist as $v) {
|
||||
copy($pdfBase . $v['file_pdf'], $base_dir . '/' . $v["npp"] . ".pdf");
|
||||
$zip->addFile($base_dir . '/' . $v["npp"] . ".pdf", $v["npp"] . ".pdf");
|
||||
}
|
||||
copy($publicBase . $stage_info["stage_icon"], $base_dir . "/journal.jpg");
|
||||
$zip->addFile($base_dir . "/journal.jpg", "journal.jpg");
|
||||
$zip->close();
|
||||
}
|
||||
}
|
||||
158
application/master/service/push/PorticoPusher.php
Normal file
158
application/master/service/push/PorticoPusher.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
use think\Queue;
|
||||
use sftp\Sftp;
|
||||
|
||||
/**
|
||||
* Portico 推送器
|
||||
* 生成 Excel(SPREADSHEET) + 下载 PDF,随后通过队列 SFTP 上传到 Portico。
|
||||
* 逻辑迁移自 Datebase::bf_db_push / push_sftp。
|
||||
*/
|
||||
class PorticoPusher extends AbstractPusher
|
||||
{
|
||||
public function code()
|
||||
{
|
||||
return 'portico';
|
||||
}
|
||||
|
||||
public function label()
|
||||
{
|
||||
return 'Portico';
|
||||
}
|
||||
|
||||
public function pushStage($stage, array $options = [])
|
||||
{
|
||||
Vendor('PHPExcel.PHPExcel');
|
||||
Vendor('PHPExcel.PHPExcel.Worksheet.Drawing');
|
||||
Vendor('PHPExcel.PHPExcel.Writer.Excel2007');
|
||||
|
||||
$pdfBase = $this->conf('article_pdf');
|
||||
$subBase = $this->conf('article_sub');
|
||||
|
||||
$baseDir = ROOT_PATH . 'public' . DS . 'davExcel' . DS . $stage . DS;
|
||||
if (!is_dir($baseDir)) {
|
||||
@mkdir($baseDir, 0777, true);
|
||||
}
|
||||
$objExcel = new \PHPExcel();
|
||||
$objWriter = \PHPExcel_IOFactory::createWriter($objExcel, 'Excel2007');
|
||||
|
||||
$objActSheet = $objExcel->getActiveSheet();
|
||||
$letter = explode(',', "A,B,C,D,E,F,G,H,I,J,K,L,M,N");
|
||||
$arrHeader = array('Journal Title', 'Journal P-ISSN', 'Journal E-ISSN', 'Article DOI/ID', 'PDF File Name', 'Article Title', 'Authors', 'Publication Date', 'Volume', 'Issue', 'Supplement', 'First Page', 'Last Page', 'Copyright Statement');
|
||||
$lenth = count($arrHeader);
|
||||
for ($i = 0; $i < $lenth; $i++) {
|
||||
$objActSheet->setCellValue("$letter[$i]1", "$arrHeader[$i]");
|
||||
$objActSheet->getRowDimension('1')->setRowHeight(25);
|
||||
}
|
||||
|
||||
$stage_info = $this->journal_stage_obj->where('journal_stage_id', $stage)->find();
|
||||
$journal_info = $this->journal_obj->where('journal_id', $stage_info['journal_id'])->find();
|
||||
$articles = $this->article_obj
|
||||
->field('j_article.*,j_journal.title journalTitle,j_journal.issn')
|
||||
->join("j_journal", "j_article.journal_id = j_journal.journal_id", 'left')
|
||||
->where('j_article.journal_stage_id', $stage)
|
||||
->where('j_article.state', 0)
|
||||
->select();
|
||||
|
||||
foreach ($articles as $k => $v) {
|
||||
$k += 2;
|
||||
$objActSheet->setCellValue('A' . $k, choiseti1($v['article_id'], $v['journalTitle']));
|
||||
$objActSheet->setCellValue('B' . $k, $v['issn']);
|
||||
$objActSheet->setCellValue('C' . $k, '');
|
||||
$objActSheet->setCellValue('D' . $k, $v['doi']);
|
||||
$fpdf = explode("/", $v['file_pdf']);
|
||||
$objActSheet->setCellValue('E' . $k, $fpdf[1]);
|
||||
copy($pdfBase . $v['file_pdf'], $baseDir . $fpdf[1]);
|
||||
$objActSheet->setCellValue('F' . $k, $v['title']);
|
||||
|
||||
$cache_author = $this->article_author_obj->where('article_id', $v['article_id'])->where('state', 0)->column('author_name');
|
||||
$objActSheet->setCellValue('G' . $k, implode(",", $cache_author));
|
||||
$objActSheet->setCellValue('H' . $k, $v['pub_date']);
|
||||
$objActSheet->setCellValue('I' . $k, $stage_info['stage_vol']);
|
||||
if (($journal_info['journal_id'] == 2 && $stage_info['stage_year'] >= 2022) || ($journal_info['journal_id'] == 3 && $stage_info['stage_year'] >= 2022) || ($journal_info['journal_id'] == 17 && $stage_info['stage_year'] >= 2019)) {
|
||||
$objActSheet->setCellValue('J' . $k, '');
|
||||
} else {
|
||||
$objActSheet->setCellValue('J' . $k, $stage_info['issue_date']);
|
||||
}
|
||||
|
||||
if ($v['file_sub'] != '') {
|
||||
$spdf = explode("/", $v['file_sub']);
|
||||
copy($subBase . $v['file_sub'], $baseDir . $spdf[1]);
|
||||
$objActSheet->setCellValue('K' . $k, $spdf[1]);
|
||||
} else {
|
||||
$objActSheet->setCellValue('K' . $k, '');
|
||||
}
|
||||
|
||||
$npp = trim($v['npp']);
|
||||
$npp = str_replace('–', '-', $npp);
|
||||
$npp = str_replace("—", '-', $npp);
|
||||
if (strpos($npp, '-') == false) {
|
||||
$objActSheet->setCellValue('L' . $k, '');
|
||||
$objActSheet->setCellValue('M' . $k, '');
|
||||
} else {
|
||||
$cc = explode('-', trim($npp));
|
||||
$objActSheet->setCellValue('L' . $k, $cc[0]);
|
||||
$objActSheet->setCellValue('M' . $k, $cc[1]);
|
||||
}
|
||||
$objActSheet->setCellValue('N' . $k, "© " . $stage_info['stage_year'] . " By Author(s). Published by TMR Publishing Group Limited. This is an open access article under the CC-BY license. (http://creativecommons.org/licenses/BY/4.0/)");
|
||||
$objActSheet->getRowDimension($k)->setRowHeight(25);
|
||||
}
|
||||
$width = array(10, 15, 20, 25, 30);
|
||||
$objActSheet->getColumnDimension('A')->setWidth($width[2]);
|
||||
$objActSheet->getColumnDimension('B')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('C')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('D')->setWidth($width[2]);
|
||||
$objActSheet->getColumnDimension('E')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('F')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('G')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('H')->setWidth($width[2]);
|
||||
$objActSheet->getColumnDimension('I')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('J')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('K')->setWidth($width[4]);
|
||||
$objActSheet->getColumnDimension('L')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('M')->setWidth($width[1]);
|
||||
$objActSheet->getColumnDimension('N')->setWidth($width[4]);
|
||||
|
||||
$outfileTitle = $stage_info['journal_stage_id'] . "dav";
|
||||
$objWriter->save($baseDir . $outfileTitle . ".xlsx");
|
||||
|
||||
$this->pushSftp($stage, $stage_info, $journal_info);
|
||||
|
||||
return PushResult::success('Portico 文件已生成并加入 SFTP 上传队列');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远程目录并将目录内文件通过队列上传到 Portico SFTP
|
||||
*/
|
||||
private function pushSftp($stage, $stage_info, $journal_info)
|
||||
{
|
||||
$cred = $this->cred('portico');
|
||||
// 连接 sftp 服务器,确保远程目录存在
|
||||
$config['host'] = $cred['host'];
|
||||
$config['port'] = $cred['port'];
|
||||
$config['username'] = $cred['user'];
|
||||
$config['password'] = $cred['pass'];
|
||||
$sftp_obj = new Sftp($config);
|
||||
|
||||
$j_base = $sftp_obj->ssh2_dir_exits("/SPREADSHEET/" . $journal_info['issn']);
|
||||
if (!$j_base) {
|
||||
$sftp_obj->ssh2_sftp_mchkdir("/SPREADSHEET/" . $journal_info['issn']);
|
||||
}
|
||||
$m_base = $sftp_obj->ssh2_dir_exits("/SPREADSHEET/" . $journal_info['issn'] . '/' . $stage_info['stage_year'] . '-' . $stage_info['stage_vol'] . '-' . $stage_info['stage_no']);
|
||||
if (!$m_base) {
|
||||
$sftp_obj->ssh2_sftp_mchkdir("/SPREADSHEET/" . $journal_info['issn'] . '/' . $stage_info['stage_year'] . '-' . $stage_info['stage_vol'] . '-' . $stage_info['stage_no']);
|
||||
}
|
||||
|
||||
$files = scandir(ROOT_PATH . 'public' . DS . 'davExcel' . DS . $stage);
|
||||
foreach ($files as $v) {
|
||||
if ($v == '.' || $v == '..') {
|
||||
continue;
|
||||
}
|
||||
$cdata['local'] = ROOT_PATH . 'public' . DS . 'davExcel' . DS . $stage . DS . $v;
|
||||
$cdata['remote'] = "/SPREADSHEET/" . $journal_info['issn'] . '/' . $stage_info['stage_year'] . '-' . $stage_info['stage_vol'] . '-' . $stage_info['stage_no'] . "/" . $v;
|
||||
Queue::push('app\api\job\mysftp@push', $cdata, "mysftp");
|
||||
}
|
||||
}
|
||||
}
|
||||
30
application/master/service/push/PushResult.php
Normal file
30
application/master/service/push/PushResult.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
/**
|
||||
* 推送结果值对象
|
||||
*/
|
||||
class PushResult
|
||||
{
|
||||
/** @var bool 是否成功 */
|
||||
public $ok;
|
||||
/** @var string 描述信息 / 失败原因 */
|
||||
public $message;
|
||||
|
||||
public function __construct($ok, $message = '')
|
||||
{
|
||||
$this->ok = (bool) $ok;
|
||||
$this->message = (string) $message;
|
||||
}
|
||||
|
||||
public static function success($message = 'success')
|
||||
{
|
||||
return new self(true, $message);
|
||||
}
|
||||
|
||||
public static function fail($message)
|
||||
{
|
||||
return new self(false, $message);
|
||||
}
|
||||
}
|
||||
30
application/master/service/push/PusherInterface.php
Normal file
30
application/master/service/push/PusherInterface.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
/**
|
||||
* 数据库推送器接口
|
||||
* 每个学术数据库(Portico / Scopus / EBSCO / CNKI / 邮件送交...)实现一个推送器。
|
||||
*/
|
||||
interface PusherInterface
|
||||
{
|
||||
/**
|
||||
* 渠道唯一标识
|
||||
* @return string
|
||||
*/
|
||||
public function code();
|
||||
|
||||
/**
|
||||
* 展示名
|
||||
* @return string
|
||||
*/
|
||||
public function label();
|
||||
|
||||
/**
|
||||
* 推送整期
|
||||
* @param int $stageId 分期id
|
||||
* @param array $options 渠道额外配置(来自 j_db_channel.config 或默认配置)
|
||||
* @return PushResult
|
||||
*/
|
||||
public function pushStage($stageId, array $options = []);
|
||||
}
|
||||
37
application/master/service/push/ScopusPusher.php
Normal file
37
application/master/service/push/ScopusPusher.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace app\master\service\push;
|
||||
|
||||
use app\master\service\XmlBuilder;
|
||||
|
||||
/**
|
||||
* Scopus (Elsevier) 推送器
|
||||
* 为每篇文章生成 XML,连同 PDF 通过 SFTP 上传到 Elsevier。
|
||||
* 逻辑迁移自 Datebase::scopusPushXML,SFTP 复用 common.php 的 pushSFTPForScopus。
|
||||
*/
|
||||
class ScopusPusher extends AbstractPusher
|
||||
{
|
||||
public function code()
|
||||
{
|
||||
return 'scopus';
|
||||
}
|
||||
|
||||
public function label()
|
||||
{
|
||||
return 'Scopus (Elsevier)';
|
||||
}
|
||||
|
||||
public function pushStage($stage_id, array $options = [])
|
||||
{
|
||||
$list = $this->article_obj->where("journal_stage_id", $stage_id)->where("state", 0)->select();
|
||||
$stage_info = $this->journal_stage_obj->where("journal_stage_id", $stage_id)->find();
|
||||
$count = 0;
|
||||
foreach ($list as $v) {
|
||||
XmlBuilder::forArticle($v['article_id']);
|
||||
$file = substr($v['doi'], strpos($v['doi'], "/") + 1) . ".xml";
|
||||
pushSFTPForScopus($file, $v['file_pdf'], $stage_info);
|
||||
$count++;
|
||||
}
|
||||
return PushResult::success("Scopus 已上传 {$count} 篇文章的 XML/PDF");
|
||||
}
|
||||
}
|
||||
69
sql/db_module_enhance.sql
Normal file
69
sql/db_module_enhance.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- =====================================================================
|
||||
-- 数据库收录模块增强 - 迁移脚本
|
||||
-- 用途:新增 “推送日志表” 与 “推送渠道配置表”
|
||||
-- 表前缀:j_ (与 application/database.php 的 prefix 一致)
|
||||
-- 执行方式:在 phpMyAdmin / Navicat 选中 journal 库后直接执行本文件
|
||||
-- 可重复执行(IF NOT EXISTS),不会影响已有数据
|
||||
-- =====================================================================
|
||||
|
||||
SET NAMES utf8;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 推送日志表:记录每一期 / 每一篇文章推送到各数据库的结果,支持重推
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `j_db_push_log` (
|
||||
`log_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`journal_id` INT NOT NULL DEFAULT 0 COMMENT '期刊id',
|
||||
`journal_stage_id` INT NOT NULL DEFAULT 0 COMMENT '分期id',
|
||||
`article_id` INT NOT NULL DEFAULT 0 COMMENT '文章id,0表示整期推送',
|
||||
`channel` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '渠道标识 portico/scopus/ebsco/cnki/email_data...',
|
||||
`channel_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '渠道中文/展示名',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待推送/排队中 1成功 2失败',
|
||||
`attempt` INT NOT NULL DEFAULT 0 COMMENT '已尝试次数',
|
||||
`message` TEXT NULL COMMENT '返回信息 / 失败原因',
|
||||
`operator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '触发人',
|
||||
`ctime` INT NOT NULL DEFAULT 0 COMMENT '创建时间戳',
|
||||
`utime` INT NOT NULL DEFAULT 0 COMMENT '更新时间戳',
|
||||
PRIMARY KEY (`log_id`),
|
||||
KEY `idx_stage_channel` (`journal_stage_id`, `channel`),
|
||||
KEY `idx_journal` (`journal_id`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据库收录推送日志';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 推送渠道配置表:把 “哪些期刊推给哪些数据库” 从代码硬编码改为可配置
|
||||
-- 若该表为空,程序会回退到 application/extra/dbpush.php 里的默认配置,
|
||||
-- 因此本表可以逐步维护、不影响现有行为。
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `j_db_channel` (
|
||||
`channel_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`channel_key` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '渠道唯一标识(与推送器对应)',
|
||||
`name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '展示名',
|
||||
`transport` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '传输方式 sftp/ftp/email',
|
||||
`pusher` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '推送器标识,留空则同 channel_key',
|
||||
`journal_ids` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '适用期刊id,逗号分隔;留空=全部期刊',
|
||||
`config` TEXT NULL COMMENT 'JSON 额外配置(如邮件收件人)',
|
||||
`is_enable` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用 1是0否',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序(大在前)',
|
||||
`ctime` INT NOT NULL DEFAULT 0 COMMENT '创建时间戳',
|
||||
`utime` INT NOT NULL DEFAULT 0 COMMENT '更新时间戳',
|
||||
`state` TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1删除',
|
||||
PRIMARY KEY (`channel_id`),
|
||||
KEY `idx_key` (`channel_key`),
|
||||
KEY `idx_state` (`state`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据库收录推送渠道配置';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 初始化渠道配置:与当前代码写死的行为保持一致,方便直接切换到配置驱动
|
||||
-- journal_ids 为空表示全部期刊
|
||||
-- ---------------------------------------------------------------------
|
||||
INSERT INTO `j_db_channel` (`channel_key`, `name`, `transport`, `pusher`, `journal_ids`, `config`, `is_enable`, `sort`, `ctime`)
|
||||
SELECT * FROM (
|
||||
SELECT 'portico' AS channel_key, 'Portico' AS name, 'sftp' AS transport, 'portico' AS pusher, '' AS journal_ids, NULL AS config, 1 AS is_enable, 90 AS sort, UNIX_TIMESTAMP() AS ctime
|
||||
UNION ALL SELECT 'scopus', 'Scopus (Elsevier)', 'sftp', 'scopus', '1,11', NULL, 1, 80, UNIX_TIMESTAMP()
|
||||
UNION ALL SELECT 'ebsco', 'EBSCO', 'ftp', 'ebsco', '2,8,20,15,9,10,3,1,14,11,4,18', NULL, 1, 70, UNIX_TIMESTAMP()
|
||||
UNION ALL SELECT 'cnki', 'CNKI 中国知网', 'ftp', 'cnki', '', NULL, 0, 60, UNIX_TIMESTAMP()
|
||||
UNION ALL SELECT 'email_data', '通用数据库邮件送交', 'email', 'email_data', '', NULL, 1, 50, UNIX_TIMESTAMP()
|
||||
UNION ALL SELECT 'email_wanfang','万方数据库', 'email', 'email_data', '1', '{"recipients":["958518573@qq.com","swyy2@wanfangdata.com.cn"]}', 1, 40, UNIX_TIMESTAMP()
|
||||
) AS seed
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `j_db_channel` LIMIT 1);
|
||||
Reference in New Issue
Block a user