Compare commits
23 Commits
c6683493d8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22947a56a4 | ||
|
|
20a68ddc8a | ||
|
|
d1e0f43992 | ||
|
|
e3ec1b0ca1 | ||
|
|
e7bb34e11d | ||
|
|
65ba338a7d | ||
|
|
7d23e9c777 | ||
|
|
2c5e85884e | ||
|
|
a67b0d24b5 | ||
|
|
e7500ab2f1 | ||
|
|
1915dc2e1e | ||
|
|
d31279e684 | ||
|
|
2deee79c9d | ||
|
|
413839f87b | ||
|
|
9d1b50c012 | ||
|
|
df1ff09aa4 | ||
|
|
70f1f801e8 | ||
|
|
a7ce5fe278 | ||
|
|
dea6f8ea1e | ||
|
|
c24dec2e55 | ||
|
|
2fe6984f59 | ||
|
|
e4d1a1f055 | ||
|
|
d037609f4a |
4
.env
4
.env
@@ -1,3 +1,7 @@
|
|||||||
|
[wechat]
|
||||||
|
appid = 你的公众号appid
|
||||||
|
appsecret = 你的公众号appsecret
|
||||||
|
|
||||||
[email]
|
[email]
|
||||||
;发送建议邮件邮箱
|
;发送建议邮件邮箱
|
||||||
send_email = tmrweb@tmrjournals.com
|
send_email = tmrweb@tmrjournals.com
|
||||||
|
|||||||
1474
application/api/controller/Agent.php
Normal file
1474
application/api/controller/Agent.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -743,7 +743,7 @@ class Article extends Base
|
|||||||
$journal_info = $this->journal_obj->where('journal_id', $article_old_info['journal_id'])->find();
|
$journal_info = $this->journal_obj->where('journal_id', $article_old_info['journal_id'])->find();
|
||||||
$editor_info = $this->user_obj->where('user_id', $journal_info['editor_id'])->find();
|
$editor_info = $this->user_obj->where('user_id', $journal_info['editor_id'])->find();
|
||||||
$tt = 'Dear editor,<br>';
|
$tt = 'Dear editor,<br>';
|
||||||
$tt .= 'The author changed the manuscript’s status, please check.<br><br>';
|
$tt .= 'The author changed the manuscript’s status, please check. sn:' . $article_old_info['accept_sn'] . '<br><br>';
|
||||||
$tt .= 'TMR Publishing Group';
|
$tt .= 'TMR Publishing Group';
|
||||||
|
|
||||||
// $sendUser=[
|
// $sendUser=[
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ class Base extends Controller
|
|||||||
protected $major_to_article_obj = "";
|
protected $major_to_article_obj = "";
|
||||||
protected $paystation_obj = "";
|
protected $paystation_obj = "";
|
||||||
protected $exchange_rate_obj = "";
|
protected $exchange_rate_obj = "";
|
||||||
|
protected $expert_obj = "";
|
||||||
|
protected $journal_email_obj = "";
|
||||||
|
|
||||||
public function __construct(\think\Request $request = null)
|
public function __construct(\think\Request $request = null)
|
||||||
{
|
{
|
||||||
@@ -171,6 +172,8 @@ class Base extends Controller
|
|||||||
$this->major_to_article_obj = Db::name("major_to_article");
|
$this->major_to_article_obj = Db::name("major_to_article");
|
||||||
$this->paystation_obj = Db::name("paystation");
|
$this->paystation_obj = Db::name("paystation");
|
||||||
$this->exchange_rate_obj = Db::name("exchange_rate");
|
$this->exchange_rate_obj = Db::name("exchange_rate");
|
||||||
|
$this->expert_obj = Db::name("expert");
|
||||||
|
$this->journal_email_obj = Db::name("journal_email");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
310
application/api/controller/Crossrefdoi.php
Normal file
310
application/api/controller/Crossrefdoi.php
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\api\controller;
|
||||||
|
use app\api\controller\Base;
|
||||||
|
use think\Db;
|
||||||
|
class Crossrefdoi extends Base{
|
||||||
|
|
||||||
|
public function __construct(\think\Request $request = null) {
|
||||||
|
parent::__construct($request);
|
||||||
|
}
|
||||||
|
// 配置项
|
||||||
|
private $mailto; // 邮箱(提升优先级)
|
||||||
|
private $timeout = 15; // 请求超时(秒)
|
||||||
|
private $maxRetry = 2; // 单个DOI最大重试次数
|
||||||
|
private $crossrefUrl = "https://api.crossref.org/works/"; //接口地址
|
||||||
|
/**
|
||||||
|
* 批量查询DOI信息(核心方法)
|
||||||
|
* @param array $dois DOI列表
|
||||||
|
* @return array 包含所有DOI的查询结果
|
||||||
|
*/
|
||||||
|
public function get($aParam = []){
|
||||||
|
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
$iPReferId = empty($aParam['p_refer_id']) ? 0 : $aParam['p_refer_id'];
|
||||||
|
if(empty($iPReferId)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please select the reference to be queried']);
|
||||||
|
}
|
||||||
|
$aWhere = ['p_refer_id' => $iPReferId,'state' => ['in', [0,2]]];
|
||||||
|
$aRefer = Db::name('production_article_refer')->field('title,joura,author,refer_doi,doilink,cs')->where($aWhere)->find();
|
||||||
|
if(empty($aRefer['refer_doi'])){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'The doi of the reference is empty']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$finalResult = [];
|
||||||
|
$sDoi = empty($aRefer['refer_doi']) ? '' : $aRefer['refer_doi'];
|
||||||
|
$sCheckDoi = $this->filterValidDoi($sDoi); // 过滤非法DOI
|
||||||
|
if(empty($sCheckDoi)){
|
||||||
|
return json_encode(['status' => 4,'msg' => 'Doi does not comply with the rules']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用单DOI查询(带重试)
|
||||||
|
$aDoiInfo = $this->fetchSingleDoiWithRetry($sCheckDoi);
|
||||||
|
if (!$aDoiInfo) {
|
||||||
|
$result['status'] = 'fail';
|
||||||
|
$result['fail_reason'] = "请求失败(重试{$this->maxRetry}次后仍失败)";
|
||||||
|
}
|
||||||
|
// 提取核心字段
|
||||||
|
$sTitle = $this->getTitle($aDoiInfo);
|
||||||
|
//期刊信息
|
||||||
|
$aPublisher = $this->getPublisher($aDoiInfo);
|
||||||
|
$sJoura = empty($aPublisher['title']) ? $aPublisher['short_title'] : $aPublisher['title'];
|
||||||
|
//作者信息
|
||||||
|
$aAuthor = $this->getAuthors($aDoiInfo);
|
||||||
|
$sAuthor = empty($aAuthor) ? '' : implode(',', $aAuthor);
|
||||||
|
$sDateno = $this->getVolumeIssuePages($aDoiInfo);
|
||||||
|
|
||||||
|
// 识别撤稿状态
|
||||||
|
$aRetractInfo = $this->checkRetracted($aDoiInfo);
|
||||||
|
$bIsRetracted = 2;
|
||||||
|
if($aRetractInfo['is_retracted'] == true){
|
||||||
|
$bIsRetracted = 1;
|
||||||
|
}
|
||||||
|
$sRetractReason = empty($aRetractInfo['reason']) ? '' : $aRetractInfo['reason'];
|
||||||
|
//获取dolink
|
||||||
|
$sDolink = $this->getDolink($aDoiInfo);
|
||||||
|
$sDolink = empty($sDolink) ? 'https://doi.org/' . $sCheckDoi : $sDolink;
|
||||||
|
|
||||||
|
//数据处理更新数据库
|
||||||
|
$aUpdate = [];
|
||||||
|
if(!empty($sTitle) && empty($aRefer['title'])){
|
||||||
|
$aUpdate['title'] = $sTitle;
|
||||||
|
}
|
||||||
|
if(!empty($sJoura) && empty($aRefer['joura'])){
|
||||||
|
$aUpdate['joura'] = $sJoura;
|
||||||
|
}
|
||||||
|
if(!empty($sAuthor) && empty($aRefer['author'])){
|
||||||
|
$aUpdate['author'] = $sAuthor;
|
||||||
|
}
|
||||||
|
if(!empty($sDateno)){
|
||||||
|
$aUpdate['dateno'] = $sDateno;
|
||||||
|
}
|
||||||
|
if($bIsRetracted == 1){
|
||||||
|
$aUpdate['is_retracted'] = 1;
|
||||||
|
}
|
||||||
|
if(!empty($sDolink) && empty($aRefer['doilink'])){
|
||||||
|
$aUpdate['doilink'] = $sDolink;
|
||||||
|
}
|
||||||
|
if(empty($aUpdate)){
|
||||||
|
return json_encode(['status' => 5,'msg' => 'No update information available']);
|
||||||
|
}
|
||||||
|
$aUpdate['update_time'] = time();
|
||||||
|
$aUpdate['cs'] = 1;
|
||||||
|
$aWhere = ['p_refer_id' => $iPReferId];
|
||||||
|
$result = Db::name('production_article_refer')->where($aWhere)->limit(1)->update($aUpdate);
|
||||||
|
if($result === false){
|
||||||
|
return json_encode(['status' => 6,'msg' => 'Update failed-Cs']);
|
||||||
|
}
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Update successful']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤非法DOI(仅保留10.xxxx/xxx格式)
|
||||||
|
*/
|
||||||
|
private function filterValidDoi($doi = ''){
|
||||||
|
$doi = trim($doi);
|
||||||
|
if (empty($doi)) return '';
|
||||||
|
// 正则匹配:10. + 至少4位数字 + / + 任意字符
|
||||||
|
if (preg_match('/^10\.\d{4,}\/.+/', $doi)) {
|
||||||
|
return $doi;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单DOI查询
|
||||||
|
*/
|
||||||
|
private function fetchSingleDoiWithRetry($doi){
|
||||||
|
$retryCount = 0;
|
||||||
|
while ($retryCount < $this->maxRetry) {
|
||||||
|
$url = $this->crossrefUrl. rawurlencode($doi) . "?mailto=" . rawurlencode($this->mailto);
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
"User-Agent: DOI-Fetcher/1.0 (mailto:{$this->mailto})"
|
||||||
|
]);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
// 成功返回
|
||||||
|
if ($httpCode == 200) {
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
return $data['status'] == 'ok' ? $data['message'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 429速率限制:延长等待后重试
|
||||||
|
if ($httpCode == 429) {
|
||||||
|
sleep(5);
|
||||||
|
$retryCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$retryCount++;
|
||||||
|
sleep(1); // 普通失败,1秒后重试
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取标题
|
||||||
|
*/
|
||||||
|
private function getTitle($aDoiInfo = []){
|
||||||
|
return $aDoiInfo['title'][0] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取出版社名
|
||||||
|
*/
|
||||||
|
private function getPublisher($aDoiInfo = []){
|
||||||
|
$aJournal = [
|
||||||
|
'title' => isset($aDoiInfo['container-title'][0]) ? $aDoiInfo['container-title'][0] : '',
|
||||||
|
'short_title'=> isset($aDoiInfo['short-container-title'][0]) ? $aDoiInfo['short-container-title'][0] : '',
|
||||||
|
'ISSN' => $aDoiInfo['ISSN'] ?? [],
|
||||||
|
'publisher' => $aDoiInfo['publisher'] ?? '',
|
||||||
|
];
|
||||||
|
return $aJournal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取作者
|
||||||
|
*/
|
||||||
|
private function getAuthors($aDoiInfo = []){
|
||||||
|
$authors = [];
|
||||||
|
if (!empty($aDoiInfo['author'])) {
|
||||||
|
foreach ($aDoiInfo['author'] as $author) {
|
||||||
|
$name = $author['family'] ?? '';
|
||||||
|
if (!empty($author['given'])) {
|
||||||
|
$name = $author['given'] . ' ' . $name;
|
||||||
|
}
|
||||||
|
if (!empty($name)) {
|
||||||
|
$authors[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $authors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取发表年份
|
||||||
|
*/
|
||||||
|
private function getPublishYear($aDoiInfo = []){
|
||||||
|
if (!empty($aDoiInfo['issued']['date-parts'][0][0])) {
|
||||||
|
return (string)$aDoiInfo['issued']['date-parts'][0][0];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取卷(期):起始页-终止页(格式:2024;10(2):100-120)
|
||||||
|
*/
|
||||||
|
private function getVolumeIssuePages($aDoiInfo = []){
|
||||||
|
$parts = [];
|
||||||
|
// 年
|
||||||
|
$year = $this->getPublishYear($aDoiInfo);
|
||||||
|
if ($year) $parts[] = $year;
|
||||||
|
|
||||||
|
// 卷(期)
|
||||||
|
$volume = $aDoiInfo['volume'] ?? '';
|
||||||
|
$issue = $aDoiInfo['issue'] ?? '';
|
||||||
|
if ($volume) {
|
||||||
|
$volumeIssue = $volume . ($issue ? "({$issue})" : '');
|
||||||
|
$parts[] = $volumeIssue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 起始页-终止页
|
||||||
|
$pageStart = $aDoiInfo['page']['start'] ?? ($aDoiInfo['first-page'] ?? '');
|
||||||
|
$pageEnd = $aDoiInfo['page']['end'] ?? ($aDoiInfo['last-page'] ?? '');
|
||||||
|
$pages = '';
|
||||||
|
if ($pageStart) {
|
||||||
|
$pages = $pageStart . ($pageEnd ? "-{$pageEnd}" : '');
|
||||||
|
}else{
|
||||||
|
$pages = $aDoiInfo['page'] ?? '';
|
||||||
|
}
|
||||||
|
if ($pages) $parts[] = $pages;
|
||||||
|
|
||||||
|
return implode(':', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 识别撤稿文章
|
||||||
|
*/
|
||||||
|
private function checkRetracted($aDoiInfo = []){
|
||||||
|
$isRetracted = false;
|
||||||
|
$reason = "未撤稿";
|
||||||
|
|
||||||
|
// 1. 文章类型为撤稿声明type/subtype
|
||||||
|
$sType = strtolower($aDoiInfo['type'] ?? '');
|
||||||
|
$sSubtype = strtolower($aDoiInfo['subtype'] ?? '');
|
||||||
|
if (isset($sType) && in_array($sType, ['retraction', 'correction'])) {
|
||||||
|
$isRetracted = true;
|
||||||
|
$reason = "文章类型为{$sType}(撤稿/更正声明)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($sSubtype) && in_array($sSubtype, ['retraction', 'correction'])) {
|
||||||
|
$isRetracted = true;
|
||||||
|
$reason = "文章类型为{$sSubtype}(撤稿/更正声明)";
|
||||||
|
}
|
||||||
|
// 2. update-type包含撤稿
|
||||||
|
if (isset($aDoiInfo['update-type']) && in_array('retraction', $aDoiInfo['update-type'])) {
|
||||||
|
$isRetracted = true;
|
||||||
|
$reason = "官方标记为撤稿(update-type: retraction)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 关联撤稿文章
|
||||||
|
if (isset($aDoiInfo['relation']) && !empty($aDoiInfo['relation'])) {
|
||||||
|
foreach ($aDoiInfo['relation'] as $relType => $relItems) {
|
||||||
|
if (in_array($relType, ['is-retraction-of', 'corrects'])) {
|
||||||
|
$isRetracted = true;
|
||||||
|
$relatedDoi = $relItems[0]['id'] ?? '未知';
|
||||||
|
$reason = "关联撤稿文章{$relatedDoi}(关系:{$relType})";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. update-to 字段
|
||||||
|
if (isset($aDoiInfo['update-to']) && is_array($aDoiInfo['update-to'])) {
|
||||||
|
foreach ($aDoiInfo['update-to'] as $update) {
|
||||||
|
$updateType = strtolower($update['type'] ?? '');
|
||||||
|
$updateLabel = strtolower($update['label'] ?? '');
|
||||||
|
if (strpos($updateType, 'retract') !== false || strpos($updateLabel, 'retract') !== false) {
|
||||||
|
$isRetracted = true;
|
||||||
|
$retractionDetail['retraction_notice'] = [
|
||||||
|
'type' => $update['type'] ?? '',
|
||||||
|
'label' => $update['label'] ?? '',
|
||||||
|
'DOI' => $update['DOI'] ?? '',
|
||||||
|
'date' => isset($update['updated']) ? $this->parseDateParts($update['updated']) : '',
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//5.title 关键词
|
||||||
|
$aTitles = $aDoiInfo['title'] ?? [];
|
||||||
|
foreach ($aTitles as $value) {
|
||||||
|
$sTitleLower = strtolower($value);
|
||||||
|
if (strpos($sTitleLower, 'retraction') !== false || strpos($sTitleLower, 'retracted') !== false
|
||||||
|
|| strpos($sTitleLower, 'withdrawal') !== false || strpos($sTitleLower, 'withdrawn') !== false) {
|
||||||
|
$isRetracted = true;
|
||||||
|
$retractionDetail['title_keyword'] = $value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'is_retracted' => $isRetracted,
|
||||||
|
'reason' => $reason
|
||||||
|
];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 识别doi链接
|
||||||
|
*/
|
||||||
|
private function getDolink($aDoiInfo = []){
|
||||||
|
return $aDoiInfo['URL'] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
2560
application/api/controller/EmailClient.php
Normal file
2560
application/api/controller/EmailClient.php
Normal file
File diff suppressed because it is too large
Load Diff
431
application/api/controller/ExpertFinder.php
Normal file
431
application/api/controller/ExpertFinder.php
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use think\Cache;
|
||||||
|
use think\Db;
|
||||||
|
use think\Validate;
|
||||||
|
use app\common\ExpertFinderService;
|
||||||
|
|
||||||
|
class ExpertFinder extends Base
|
||||||
|
{
|
||||||
|
private $service;
|
||||||
|
|
||||||
|
public function __construct(\think\Request $request = null)
|
||||||
|
{
|
||||||
|
parent::__construct($request);
|
||||||
|
$this->service = new ExpertFinderService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main search endpoint
|
||||||
|
*/
|
||||||
|
public function search()
|
||||||
|
{
|
||||||
|
$keyword = trim($this->request->param('keyword', ''));
|
||||||
|
$page = max(1, intval($this->request->param('page', 1)));
|
||||||
|
$perPage = max(10, min(intval($this->request->param('per_page', 100)), 100));
|
||||||
|
$minYear = intval($this->request->param('min_year', date('Y') - 3));
|
||||||
|
$source = $this->request->param('source', 'pubmed');
|
||||||
|
|
||||||
|
if (empty($keyword)) {
|
||||||
|
return jsonError('keyword is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = 'expert_finder_' . md5($keyword . $page . $perPage . $minYear . $source);
|
||||||
|
$cached = Cache::get($cacheKey);
|
||||||
|
if ($cached) {
|
||||||
|
return jsonSuccess($cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->service->searchExperts($keyword, $perPage, $minYear, $page, $source);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return jsonError('Search failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$saveResult = $this->service->saveExperts($result['experts'], $keyword, $source);
|
||||||
|
$result['saved_new'] = $saveResult['inserted'];
|
||||||
|
$result['saved_exist'] = $saveResult['existing'];
|
||||||
|
|
||||||
|
Cache::set($cacheKey, $result, 3600);
|
||||||
|
|
||||||
|
return jsonSuccess($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get experts from local database
|
||||||
|
*/
|
||||||
|
public function getList()
|
||||||
|
{
|
||||||
|
$field = trim($this->request->param('field', ''));
|
||||||
|
$majorId = intval($this->request->param('major_id', 0));
|
||||||
|
$state = $this->request->param('state', '-1');
|
||||||
|
$keyword = trim($this->request->param('keyword', ''));
|
||||||
|
$noRecent = intval($this->request->param('no_recent', 0));
|
||||||
|
$recentDays = max(1, intval($this->request->param('recent_days', 30)));
|
||||||
|
$page = max(1, intval($this->request->param('page', 1)));
|
||||||
|
$perPage = max(1, min(intval($this->request->param('per_page', 20)), 100));
|
||||||
|
$minExperts = max(0, intval($this->request->param('min_experts', 50)));
|
||||||
|
|
||||||
|
$query = Db::name('expert')->alias('e');
|
||||||
|
$needJoin = ($field !== '' || $majorId > 0);
|
||||||
|
|
||||||
|
if ($needJoin) {
|
||||||
|
$query->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner');
|
||||||
|
if ($field !== '') {
|
||||||
|
$query->where('ef.field', 'like', '%' . $field . '%');
|
||||||
|
}
|
||||||
|
if ($majorId > 0) {
|
||||||
|
$query->where('ef.major_id', $majorId);
|
||||||
|
}
|
||||||
|
$query->group('e.expert_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state !== '-1' && $state !== '') {
|
||||||
|
$query->where('e.state', intval($state));
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$query->where('e.name|e.email|e.affiliation', 'like', '%' . $keyword . '%');
|
||||||
|
}
|
||||||
|
if ($noRecent) {
|
||||||
|
$cutoff = time() - ($recentDays * 86400);
|
||||||
|
$query->where(function ($q) use ($cutoff) {
|
||||||
|
$q->where('e.ltime', 0)->whereOr('e.ltime', '<', $cutoff);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQuery = clone $query;
|
||||||
|
$total = $countQuery->count('distinct e.expert_id');
|
||||||
|
|
||||||
|
$list = $query
|
||||||
|
->field('e.*')
|
||||||
|
->order('e.ctime desc')
|
||||||
|
->page($page, $perPage)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
$item['fields'] = Db::name('expert_field')
|
||||||
|
->where('expert_id', $item['expert_id'])
|
||||||
|
->where('state', 0)
|
||||||
|
->column('field');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fetching = false;
|
||||||
|
if ($field !== '' && $total < $minExperts && $minExperts > 0) {
|
||||||
|
$this->triggerBackgroundFetch($field);
|
||||||
|
$fetching = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'list' => $list,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'total_pages' => $total > 0 ? ceil($total / $perPage) : 0,
|
||||||
|
'fetching' => $fetching,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all fields associated with an expert
|
||||||
|
*/
|
||||||
|
public function getExpertFields()
|
||||||
|
{
|
||||||
|
$expertId = intval($this->request->param('expert_id', 0));
|
||||||
|
if (!$expertId) {
|
||||||
|
return jsonError('expert_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = Db::name('expert_field')
|
||||||
|
->where('expert_id', $expertId)
|
||||||
|
->where('state', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
return jsonSuccess($fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update expert state
|
||||||
|
*/
|
||||||
|
public function updateState()
|
||||||
|
{
|
||||||
|
$expertId = $this->request->param('expert_id', '');
|
||||||
|
$state = intval($this->request->param('state', 0));
|
||||||
|
|
||||||
|
if (empty($expertId)) {
|
||||||
|
return jsonError('expert_id is required');
|
||||||
|
}
|
||||||
|
if ($state < 0 || $state > 5) {
|
||||||
|
return jsonError('state must be 0-5');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_map('intval', explode(',', $expertId));
|
||||||
|
$count = Db::name('expert')->where('expert_id', 'in', $ids)->update(['state' => $state]);
|
||||||
|
|
||||||
|
return jsonSuccess(['updated' => $count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete expert (soft: set state=5 blacklist, or hard delete)
|
||||||
|
*/
|
||||||
|
public function deleteExpert()
|
||||||
|
{
|
||||||
|
$expertId = $this->request->param('expert_id', '');
|
||||||
|
$hard = intval($this->request->param('hard', 0));
|
||||||
|
|
||||||
|
if (empty($expertId)) {
|
||||||
|
return jsonError('expert_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_map('intval', explode(',', $expertId));
|
||||||
|
|
||||||
|
if ($hard) {
|
||||||
|
$count = Db::name('expert')->where('expert_id', 'in', $ids)->delete();
|
||||||
|
} else {
|
||||||
|
$count = Db::name('expert')->where('expert_id', 'in', $ids)->update(['state' => 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess(['affected' => $count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export search results to Excel
|
||||||
|
*/
|
||||||
|
public function export()
|
||||||
|
{
|
||||||
|
$keyword = trim($this->request->param('keyword', ''));
|
||||||
|
$page = max(1, intval($this->request->param('page', 1)));
|
||||||
|
$perPage = max(10, min(intval($this->request->param('per_page', 100)), 100));
|
||||||
|
$minYear = intval($this->request->param('min_year', date('Y') - 3));
|
||||||
|
$source = $this->request->param('source', 'pubmed');
|
||||||
|
|
||||||
|
if (empty($keyword)) {
|
||||||
|
return jsonError('keyword is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheKey = 'expert_finder_' . md5($keyword . $page . $perPage . $minYear . $source);
|
||||||
|
$cached = Cache::get($cacheKey);
|
||||||
|
|
||||||
|
if (!$cached) {
|
||||||
|
try {
|
||||||
|
$cached = $this->service->searchExperts($keyword, $perPage, $minYear, $page, $source);
|
||||||
|
Cache::set($cacheKey, $cached, 3600);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return jsonError('Search failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($cached['experts'])) {
|
||||||
|
return jsonError('No experts found to export');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->generateExcel($cached['experts'], $keyword, $page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear search cache
|
||||||
|
*/
|
||||||
|
public function clearCache()
|
||||||
|
{
|
||||||
|
$keyword = trim($this->request->param('keyword', ''));
|
||||||
|
$maxResults = intval($this->request->param('max_results', 200));
|
||||||
|
$minYear = intval($this->request->param('min_year', date('Y') - 3));
|
||||||
|
$source = $this->request->param('source', 'pubmed');
|
||||||
|
|
||||||
|
$cacheKey = 'expert_finder_' . md5($keyword . $maxResults . $minYear . $source);
|
||||||
|
Cache::rm($cacheKey);
|
||||||
|
|
||||||
|
return jsonSuccess(['msg' => 'Cache cleared']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Cron / Auto Fetch ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily cron: auto-fetch experts for all active fields in t_expert_fetch via queue.
|
||||||
|
* No longer tied to journals; t_expert_fetch is the sole source of crawl targets.
|
||||||
|
*/
|
||||||
|
public function dailyFetchAll()
|
||||||
|
{
|
||||||
|
$perPage = max(10, intval($this->request->param('per_page', 200)));
|
||||||
|
$source = $this->request->param('source', 'pubmed');
|
||||||
|
$minYear = intval($this->request->param('min_year', date('Y') - 3));
|
||||||
|
|
||||||
|
$fetchList = Db::name('expert_fetch')
|
||||||
|
->where('state', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
if (empty($fetchList)) {
|
||||||
|
return jsonSuccess(['msg' => 'No active fetch fields found', 'queued' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queued = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$details = [];
|
||||||
|
$todayStart = strtotime(date('Y-m-d'));
|
||||||
|
|
||||||
|
foreach ($fetchList as $item) {
|
||||||
|
$keyword = trim($item['field']);
|
||||||
|
$itemSource = trim($item['source'] ?: $source);
|
||||||
|
if ($keyword === '') continue;
|
||||||
|
|
||||||
|
$fetchLog = $this->service->getFetchLog($keyword, $itemSource);
|
||||||
|
if ($fetchLog['last_time'] >= $todayStart) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$delay = $queued * 10;
|
||||||
|
\think\Queue::later($delay, 'app\api\job\FetchExperts@fire', [
|
||||||
|
'field' => $keyword,
|
||||||
|
'source' => $itemSource,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'min_year' => $minYear,
|
||||||
|
], 'FetchExperts');
|
||||||
|
|
||||||
|
$queued++;
|
||||||
|
$details[] = [
|
||||||
|
'expert_fetch_id' => $item['expert_fetch_id'],
|
||||||
|
'field' => $keyword,
|
||||||
|
'source' => $itemSource,
|
||||||
|
'delay_s' => $delay,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'queued' => $queued,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'details' => $details,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cron job: daily fetch experts for given keywords
|
||||||
|
*/
|
||||||
|
public function cronFetch()
|
||||||
|
{
|
||||||
|
$keywordsStr = trim($this->request->param('keywords', ''));
|
||||||
|
$source = $this->request->param('source', 'pubmed');
|
||||||
|
$perPage = max(10, min(intval($this->request->param('per_page', 100)), 100));
|
||||||
|
$minYear = intval($this->request->param('min_year', date('Y') - 3));
|
||||||
|
|
||||||
|
if (empty($keywordsStr)) {
|
||||||
|
return jsonError('keywords is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
set_time_limit(0);
|
||||||
|
$keywords = array_map('trim', explode(',', $keywordsStr));
|
||||||
|
$report = [];
|
||||||
|
|
||||||
|
foreach ($keywords as $kw) {
|
||||||
|
if (empty($kw)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->service->doFetchForField($kw, $source, $perPage, $minYear);
|
||||||
|
$report[] = $result;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$report[] = [
|
||||||
|
'keyword' => $kw,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess(['report' => $report]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a background fetch for a specific field via queue
|
||||||
|
*/
|
||||||
|
private function triggerBackgroundFetch($field)
|
||||||
|
{
|
||||||
|
$lockKey = 'fetch_lock_' . md5($field);
|
||||||
|
if (Cache::get($lockKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Cache::set($lockKey, 1, 300);
|
||||||
|
|
||||||
|
\think\Queue::push('app\api\job\FetchExperts@fire', [
|
||||||
|
'field' => $field,
|
||||||
|
'source' => 'pubmed',
|
||||||
|
'per_page' => 100,
|
||||||
|
'min_year' => date('Y') - 3,
|
||||||
|
], 'FetchExperts');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchOneField()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$rule = new Validate([
|
||||||
|
"field" => "require"
|
||||||
|
]);
|
||||||
|
if (!$rule->check($data)) {
|
||||||
|
return jsonError($rule->getError());
|
||||||
|
}
|
||||||
|
$res = $this->service->doFetchForField($data['field'], "pubmed", 100, date('Y') - 3);
|
||||||
|
return jsonSuccess($res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Excel Export ====================
|
||||||
|
|
||||||
|
private function generateExcel($experts, $keyword, $page = 1)
|
||||||
|
{
|
||||||
|
vendor("PHPExcel.PHPExcel");
|
||||||
|
|
||||||
|
$objPHPExcel = new \PHPExcel();
|
||||||
|
$sheet = $objPHPExcel->getActiveSheet();
|
||||||
|
$sheet->setTitle('Experts');
|
||||||
|
|
||||||
|
$headers = ['A' => '#', 'B' => 'Name', 'C' => 'Email', 'D' => 'Affiliation', 'E' => 'Paper Count', 'F' => 'Representative Papers'];
|
||||||
|
foreach ($headers as $col => $header) {
|
||||||
|
$sheet->setCellValue($col . '1', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerStyle = [
|
||||||
|
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||||
|
'fill' => ['type' => \PHPExcel_Style_Fill::FILL_SOLID, 'startcolor' => ['rgb' => '4472C4']],
|
||||||
|
'alignment' => ['horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER],
|
||||||
|
];
|
||||||
|
$sheet->getStyle('A1:F1')->applyFromArray($headerStyle);
|
||||||
|
|
||||||
|
foreach ($experts as $i => $expert) {
|
||||||
|
$row = $i + 2;
|
||||||
|
$paperTitles = array_map(function ($p) {
|
||||||
|
return $p['title'];
|
||||||
|
}, $expert['papers']);
|
||||||
|
|
||||||
|
$sheet->setCellValue('A' . $row, $i + 1);
|
||||||
|
$sheet->setCellValue('B' . $row, $expert['name']);
|
||||||
|
$sheet->setCellValue('C' . $row, $expert['email']);
|
||||||
|
$sheet->setCellValue('D' . $row, $expert['affiliation']);
|
||||||
|
$sheet->setCellValue('E' . $row, $expert['paper_count']);
|
||||||
|
$sheet->setCellValue('F' . $row, implode("\n", $paperTitles));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(6);
|
||||||
|
$sheet->getColumnDimension('B')->setWidth(25);
|
||||||
|
$sheet->getColumnDimension('C')->setWidth(35);
|
||||||
|
$sheet->getColumnDimension('D')->setWidth(50);
|
||||||
|
$sheet->getColumnDimension('E')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('F')->setWidth(60);
|
||||||
|
|
||||||
|
$filename = 'experts_' . preg_replace('/[^a-zA-Z0-9]/', '_', $keyword) . '_p' . $page . '_' . date('Ymd_His') . '.xlsx';
|
||||||
|
$filepath = ROOT_PATH . 'public' . DS . 'exports' . DS . $filename;
|
||||||
|
|
||||||
|
$dir = ROOT_PATH . 'public' . DS . 'exports';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$writer = \PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
|
||||||
|
$writer->save($filepath);
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'file_url' => '/exports/' . $filename,
|
||||||
|
'file_name' => $filename,
|
||||||
|
'count' => count($experts),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
643
application/api/controller/ExpertManage.php
Normal file
643
application/api/controller/ExpertManage.php
Normal file
@@ -0,0 +1,643 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class ExpertManage extends Base
|
||||||
|
{
|
||||||
|
private $stateMap = [
|
||||||
|
0 => '待联系',
|
||||||
|
1 => '已发邮件',
|
||||||
|
2 => '已回复',
|
||||||
|
3 => '已投稿',
|
||||||
|
4 => '退信/无效',
|
||||||
|
5 => '黑名单(退订)',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(\think\Request $request = null)
|
||||||
|
{
|
||||||
|
parent::__construct($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专家列表(支持多条件筛选 + 分页)
|
||||||
|
*
|
||||||
|
* 参数:
|
||||||
|
* keyword - 搜索姓名/邮箱/单位
|
||||||
|
* field - 按领域关键词筛选
|
||||||
|
* major_id - 按学科ID筛选
|
||||||
|
* state - 状态筛选 (0-5, 传-1或不传则不过滤)
|
||||||
|
* source - 来源筛选
|
||||||
|
* pageIndex - 页码 (默认1)
|
||||||
|
* pageSize - 每页条数 (默认20)
|
||||||
|
*/
|
||||||
|
public function getList()
|
||||||
|
{
|
||||||
|
$data = $this->request->param();
|
||||||
|
$keyword = trim(isset($data['keyword']) ? $data['keyword'] : '');
|
||||||
|
$field = trim(isset($data['field']) ? $data['field'] : '');
|
||||||
|
$state = isset($data['state']) ? $data['state'] : '-1';
|
||||||
|
$source = trim(isset($data['source']) ? $data['source'] : '');
|
||||||
|
$page = max(1, intval(isset($data['pageIndex']) ? $data['pageIndex'] : 1));
|
||||||
|
$pageSize = max(1, intval(isset($data['pageSize']) ? $data['pageSize'] : 20));
|
||||||
|
|
||||||
|
$query = Db::name('expert')->alias('e');
|
||||||
|
$needJoin = ($field !== '');
|
||||||
|
|
||||||
|
if ($needJoin) {
|
||||||
|
$query->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner');
|
||||||
|
if ($field !== '') {
|
||||||
|
$query->where('ef.field', 'like', '%' . $field . '%');
|
||||||
|
}
|
||||||
|
$query->group('e.expert_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state !== '-1' && $state !== '') {
|
||||||
|
$query->where('e.state', intval($state));
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$query->where('e.name|e.email|e.affiliation', 'like', '%' . $keyword . '%');
|
||||||
|
}
|
||||||
|
if ($source !== '') {
|
||||||
|
$query->where('e.source', $source);
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQuery = clone $query;
|
||||||
|
$total = $countQuery->distinct('e.expert_id')->count();
|
||||||
|
|
||||||
|
$list = $query
|
||||||
|
->field('e.*')
|
||||||
|
->order('e.ctime desc')
|
||||||
|
->page($page, $pageSize)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
$item['fields'] = Db::name('expert_field')
|
||||||
|
->where('expert_id', $item['expert_id'])
|
||||||
|
->where('state', 0)
|
||||||
|
->select();
|
||||||
|
$item['state_text'] = isset($this->stateMap[$item['state']]) ? $this->stateMap[$item['state']] : '未知';
|
||||||
|
$item['ctime_text'] = $item['ctime'] ? date('Y-m-d H:i:s', $item['ctime']) : '';
|
||||||
|
$item['ltime_text'] = $item['ltime'] ? date('Y-m-d H:i:s', $item['ltime']) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'list' => $list,
|
||||||
|
'total' => $total,
|
||||||
|
'pageIndex' => $page,
|
||||||
|
'pageSize' => $pageSize,
|
||||||
|
'totalPages' => $total > 0 ? ceil($total / $pageSize) : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取专家详情(含所有领域)
|
||||||
|
*/
|
||||||
|
public function getDetail()
|
||||||
|
{
|
||||||
|
$expertId = intval($this->request->param('expert_id', 0));
|
||||||
|
if (!$expertId) {
|
||||||
|
return jsonError('expert_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$expert = Db::name('expert')->where('expert_id', $expertId)->find();
|
||||||
|
if (!$expert) {
|
||||||
|
return jsonError('专家不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$expert['fields'] = Db::name('expert_field')
|
||||||
|
->where('expert_id', $expertId)
|
||||||
|
->where('state', 0)
|
||||||
|
->select();
|
||||||
|
$expert['state_text'] = isset($this->stateMap[$expert['state']]) ? $this->stateMap[$expert['state']] : '未知';
|
||||||
|
$expert['ctime_text'] = $expert['ctime'] ? date('Y-m-d H:i:s', $expert['ctime']) : '';
|
||||||
|
$expert['ltime_text'] = $expert['ltime'] ? date('Y-m-d H:i:s', $expert['ltime']) : '';
|
||||||
|
|
||||||
|
return jsonSuccess($expert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加专家
|
||||||
|
*/
|
||||||
|
public function addExpert()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$name = trim(isset($data['name']) ? $data['name'] : '');
|
||||||
|
$email = trim(isset($data['email']) ? $data['email'] : '');
|
||||||
|
|
||||||
|
if ($name === '' || $email === '') {
|
||||||
|
return jsonError('name和email不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = Db::name('expert')->where('email', $email)->find();
|
||||||
|
if ($exists) {
|
||||||
|
return jsonError('该邮箱已存在,expert_id=' . $exists['expert_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$insert = [
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'affiliation' => trim(isset($data['affiliation']) ? $data['affiliation'] : ''),
|
||||||
|
'source' => trim(isset($data['source']) ? $data['source'] : 'manual'),
|
||||||
|
'ctime' => time(),
|
||||||
|
'ltime' => 0,
|
||||||
|
'state' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$expertId = Db::name('expert')->insertGetId($insert);
|
||||||
|
|
||||||
|
if (!empty($data['fields'])) {
|
||||||
|
$this->saveExpertFields($expertId, $data['fields']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess(['expert_id' => $expertId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑专家
|
||||||
|
*/
|
||||||
|
public function editExpert()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$expertId = intval(isset($data['expert_id']) ? $data['expert_id'] : 0);
|
||||||
|
if (!$expertId) {
|
||||||
|
return jsonError('expert_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$expert = Db::name('expert')->where('expert_id', $expertId)->find();
|
||||||
|
if (!$expert) {
|
||||||
|
return jsonError('专家不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = [];
|
||||||
|
if (isset($data['name'])) $update['name'] = trim($data['name']);
|
||||||
|
if (isset($data['email'])) $update['email'] = trim($data['email']);
|
||||||
|
if (isset($data['affiliation'])) $update['affiliation'] = trim($data['affiliation']);
|
||||||
|
if (isset($data['source'])) $update['source'] = trim($data['source']);
|
||||||
|
if (isset($data['state'])) $update['state'] = intval($data['state']);
|
||||||
|
|
||||||
|
if (!empty($update)) {
|
||||||
|
Db::name('expert')->where('expert_id', $expertId)->update($update);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['fields'])) {
|
||||||
|
Db::name('expert_field')->where('expert_id', $expertId)->where('state', 0)->update(['state' => 1]);
|
||||||
|
if (!empty($data['fields'])) {
|
||||||
|
$this->saveExpertFields($expertId, $data['fields']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess(['expert_id' => $expertId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量修改状态
|
||||||
|
*
|
||||||
|
* 参数:
|
||||||
|
* expert_ids - 逗号分隔的ID列表 "1,2,3"
|
||||||
|
* state - 目标状态 0-5
|
||||||
|
*/
|
||||||
|
public function updateState()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$expertIds = isset($data['expert_ids']) ? $data['expert_ids'] : '';
|
||||||
|
$state = intval(isset($data['state']) ? $data['state'] : -1);
|
||||||
|
|
||||||
|
if (empty($expertIds)) {
|
||||||
|
return jsonError('expert_ids is required');
|
||||||
|
}
|
||||||
|
if ($state < 0 || $state > 5) {
|
||||||
|
return jsonError('state取值范围0-5');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_map('intval', explode(',', $expertIds));
|
||||||
|
$count = Db::name('expert')->where('expert_id', 'in', $ids)->update(['state' => $state]);
|
||||||
|
|
||||||
|
return jsonSuccess(['updated' => $count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除专家(软删除,设为黑名单状态5)
|
||||||
|
*
|
||||||
|
* 参数:
|
||||||
|
* expert_ids - 逗号分隔的ID列表
|
||||||
|
* hard - 传1则物理删除
|
||||||
|
*/
|
||||||
|
public function deleteExpert()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$expertIds = isset($data['expert_ids']) ? $data['expert_ids'] : '';
|
||||||
|
$hard = intval(isset($data['hard']) ? $data['hard'] : 0);
|
||||||
|
|
||||||
|
if (empty($expertIds)) {
|
||||||
|
return jsonError('expert_ids is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_map('intval', explode(',', $expertIds));
|
||||||
|
|
||||||
|
if ($hard) {
|
||||||
|
Db::name('expert_field')->where('expert_id', 'in', $ids)->delete();
|
||||||
|
$count = Db::name('expert')->where('expert_id', 'in', $ids)->delete();
|
||||||
|
} else {
|
||||||
|
$count = Db::name('expert')->where('expert_id', 'in', $ids)->update(['state' => 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess(['affected' => $count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给专家添加领域
|
||||||
|
*/
|
||||||
|
public function addField()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$expertId = intval(isset($data['expert_id']) ? $data['expert_id'] : 0);
|
||||||
|
$majorId = intval(isset($data['major_id']) ? $data['major_id'] : 0);
|
||||||
|
$field = trim(isset($data['field']) ? $data['field'] : '');
|
||||||
|
|
||||||
|
if (!$expertId || $field === '') {
|
||||||
|
return jsonError('expert_id和field不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = Db::name('expert_field')
|
||||||
|
->where('expert_id', $expertId)
|
||||||
|
->where('field', $field)
|
||||||
|
->where('state', 0)
|
||||||
|
->find();
|
||||||
|
if ($exists) {
|
||||||
|
return jsonError('该领域已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = Db::name('expert_field')->insertGetId([
|
||||||
|
'expert_id' => $expertId,
|
||||||
|
'major_id' => $majorId,
|
||||||
|
'field' => $field,
|
||||||
|
'state' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return jsonSuccess(['expert_field_id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除领域(软删除)
|
||||||
|
*/
|
||||||
|
public function removeField()
|
||||||
|
{
|
||||||
|
$efId = intval($this->request->param('expert_field_id', 0));
|
||||||
|
if (!$efId) {
|
||||||
|
return jsonError('expert_field_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::name('expert_field')->where('expert_field_id', $efId)->update(['state' => 1]);
|
||||||
|
|
||||||
|
return jsonSuccess([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有不重复的领域列表(用于筛选下拉框)
|
||||||
|
*/
|
||||||
|
public function getFieldOptions()
|
||||||
|
{
|
||||||
|
$list = Db::name('expert_field')
|
||||||
|
->where('state', 0)
|
||||||
|
->group('field')
|
||||||
|
->column('field');
|
||||||
|
|
||||||
|
return jsonSuccess($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有来源列表(用于筛选下拉框)
|
||||||
|
*/
|
||||||
|
public function getSourceOptions()
|
||||||
|
{
|
||||||
|
$list = Db::name('expert')
|
||||||
|
->where('source', '<>', '')
|
||||||
|
->group('source')
|
||||||
|
->column('source');
|
||||||
|
|
||||||
|
return jsonSuccess($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出某个领域的专家为Excel
|
||||||
|
*
|
||||||
|
* 参数:
|
||||||
|
* field - 领域关键词(必填)
|
||||||
|
* major_id - 学科ID(可选)
|
||||||
|
* state - 状态筛选(可选,默认不过滤)
|
||||||
|
* keyword - 搜索姓名/邮箱/单位
|
||||||
|
* source - 来源筛选
|
||||||
|
*/
|
||||||
|
public function exportExcel()
|
||||||
|
{
|
||||||
|
$data = $this->request->param();
|
||||||
|
$field = trim(isset($data['field']) ? $data['field'] : '');
|
||||||
|
$state = isset($data['state']) ? $data['state'] : '-1';
|
||||||
|
$keyword = trim(isset($data['keyword']) ? $data['keyword'] : '');
|
||||||
|
$source = trim(isset($data['source']) ? $data['source'] : '');
|
||||||
|
|
||||||
|
$query = Db::name('expert')->alias('e');
|
||||||
|
|
||||||
|
if ($field !== '') {
|
||||||
|
$query->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner');
|
||||||
|
if ($field !== '') {
|
||||||
|
$query->where('ef.field', 'like', '%' . $field . '%');
|
||||||
|
}
|
||||||
|
$query->group('e.expert_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state !== '-1' && $state !== '') {
|
||||||
|
$query->where('e.state', intval($state));
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$query->where('e.name|e.email|e.affiliation', 'like', '%' . $keyword . '%');
|
||||||
|
}
|
||||||
|
if ($source !== '') {
|
||||||
|
$query->where('e.source', $source);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $query->field('e.*')->order('e.ctime desc')->select();
|
||||||
|
|
||||||
|
if (empty($list)) {
|
||||||
|
return jsonError('没有符合条件的数据可导出');
|
||||||
|
}
|
||||||
|
|
||||||
|
$expertIds = array_column($list, 'expert_id');
|
||||||
|
$allFields = Db::name('expert_field')
|
||||||
|
->where('expert_id', 'in', $expertIds)
|
||||||
|
->where('state', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$fieldMap = [];
|
||||||
|
foreach ($allFields as $f) {
|
||||||
|
$fieldMap[$f['expert_id']][] = $f['field'];
|
||||||
|
}
|
||||||
|
|
||||||
|
vendor("PHPExcel.PHPExcel");
|
||||||
|
|
||||||
|
$objPHPExcel = new \PHPExcel();
|
||||||
|
$sheet = $objPHPExcel->getActiveSheet();
|
||||||
|
$sheet->setTitle('Expert List');
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'A' => '#',
|
||||||
|
'B' => 'Name',
|
||||||
|
'C' => 'Email',
|
||||||
|
'D' => 'Affiliation',
|
||||||
|
'E' => 'Source',
|
||||||
|
'F' => 'Fields',
|
||||||
|
'G' => 'State',
|
||||||
|
'H' => 'Add Time',
|
||||||
|
'I' => 'Last Promotion',
|
||||||
|
];
|
||||||
|
foreach ($headers as $col => $header) {
|
||||||
|
$sheet->setCellValue($col . '1', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerStyle = [
|
||||||
|
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||||
|
'fill' => ['type' => \PHPExcel_Style_Fill::FILL_SOLID, 'startcolor' => ['rgb' => '4472C4']],
|
||||||
|
'alignment' => ['horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER],
|
||||||
|
];
|
||||||
|
$sheet->getStyle('A1:I1')->applyFromArray($headerStyle);
|
||||||
|
|
||||||
|
foreach ($list as $i => $item) {
|
||||||
|
$row = $i + 2;
|
||||||
|
$fields = isset($fieldMap[$item['expert_id']]) ? implode(', ', $fieldMap[$item['expert_id']]) : '';
|
||||||
|
$stateText = isset($this->stateMap[$item['state']]) ? $this->stateMap[$item['state']] : '未知';
|
||||||
|
|
||||||
|
$sheet->setCellValue('A' . $row, $i + 1);
|
||||||
|
$sheet->setCellValue('B' . $row, $item['name']);
|
||||||
|
$sheet->setCellValueExplicit('C' . $row, $item['email'], \PHPExcel_Cell_DataType::TYPE_STRING);
|
||||||
|
$sheet->setCellValue('D' . $row, $item['affiliation']);
|
||||||
|
$sheet->setCellValue('E' . $row, $item['source']);
|
||||||
|
$sheet->setCellValue('F' . $row, $fields);
|
||||||
|
$sheet->setCellValue('G' . $row, $stateText);
|
||||||
|
$sheet->setCellValue('H' . $row, $item['ctime'] ? date('Y-m-d H:i:s', $item['ctime']) : '');
|
||||||
|
$sheet->setCellValue('I' . $row, $item['ltime'] ? date('Y-m-d H:i:s', $item['ltime']) : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(6);
|
||||||
|
$sheet->getColumnDimension('B')->setWidth(25);
|
||||||
|
$sheet->getColumnDimension('C')->setWidth(35);
|
||||||
|
$sheet->getColumnDimension('D')->setWidth(40);
|
||||||
|
$sheet->getColumnDimension('E')->setWidth(15);
|
||||||
|
$sheet->getColumnDimension('F')->setWidth(50);
|
||||||
|
$sheet->getColumnDimension('G')->setWidth(15);
|
||||||
|
$sheet->getColumnDimension('H')->setWidth(20);
|
||||||
|
$sheet->getColumnDimension('I')->setWidth(20);
|
||||||
|
|
||||||
|
$label = $field !== '' ? preg_replace('/[^a-zA-Z0-9_\x{4e00}-\x{9fa5}]/u', '_', $field) : 'all';
|
||||||
|
$filename = 'expert_' . $label . '_' . date('Ymd_His') . '.xlsx';
|
||||||
|
|
||||||
|
$dir = ROOT_PATH . 'public' . DS . 'exports';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filepath = $dir . DS . $filename;
|
||||||
|
$writer = \PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
|
||||||
|
$writer->save($filepath);
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'file_url' => '/exports/' . $filename,
|
||||||
|
'file_name' => $filename,
|
||||||
|
'count' => count($list),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量保存专家领域
|
||||||
|
* @param int $expertId
|
||||||
|
* @param array $fields [{"major_id":1,"field":"xxx"}, ...]
|
||||||
|
*/
|
||||||
|
private function saveExpertFields($expertId, $fields)
|
||||||
|
{
|
||||||
|
if (is_string($fields)) {
|
||||||
|
$fields = json_decode($fields, true);
|
||||||
|
}
|
||||||
|
if (!is_array($fields)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
$majorId = intval(isset($f['major_id']) ? $f['major_id'] : 0);
|
||||||
|
$fieldName = trim(isset($f['field']) ? $f['field'] : '');
|
||||||
|
if ($fieldName === '') continue;
|
||||||
|
|
||||||
|
$exists = Db::name('expert_field')
|
||||||
|
->where('expert_id', $expertId)
|
||||||
|
->where('field', $fieldName)
|
||||||
|
->where('state', 0)
|
||||||
|
->find();
|
||||||
|
if ($exists) continue;
|
||||||
|
|
||||||
|
Db::name('expert_field')->insert([
|
||||||
|
'expert_id' => $expertId,
|
||||||
|
'major_id' => $majorId,
|
||||||
|
'field' => $fieldName,
|
||||||
|
'state' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Expert Fetch Field Management ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取抓取领域列表
|
||||||
|
* 参数: state(-1不过滤), keyword(搜索field), pageIndex, pageSize
|
||||||
|
*/
|
||||||
|
public function getFetchList()
|
||||||
|
{
|
||||||
|
$data = $this->request->param();
|
||||||
|
$state = isset($data['state']) ? $data['state'] : '-1';
|
||||||
|
$keyword = trim(isset($data['keyword']) ? $data['keyword'] : '');
|
||||||
|
$page = max(1, intval(isset($data['pageIndex']) ? $data['pageIndex'] : 1));
|
||||||
|
$pageSize = max(1, intval(isset($data['pageSize']) ? $data['pageSize'] : 20));
|
||||||
|
|
||||||
|
$query = Db::name('expert_fetch');
|
||||||
|
|
||||||
|
if ($state !== '-1' && $state !== '') {
|
||||||
|
$query->where('state', intval($state));
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$query->where('field', 'like', '%' . $keyword . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$countQuery = Db::name('expert_fetch');
|
||||||
|
if ($state !== '-1' && $state !== '') {
|
||||||
|
$countQuery->where('state', intval($state));
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$countQuery->where('field', 'like', '%' . $keyword . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $countQuery->count();
|
||||||
|
$list = $query->order('expert_fetch_id desc')->page($page, $pageSize)->select();
|
||||||
|
|
||||||
|
foreach ($list as &$item) {
|
||||||
|
$item['last_time_text'] = $item['last_time'] ? date('Y-m-d H:i:s', $item['last_time']) : '';
|
||||||
|
$item['ctime_text'] = $item['ctime'] ? date('Y-m-d H:i:s', $item['ctime']) : '';
|
||||||
|
$fieldName = trim($item['field']);
|
||||||
|
|
||||||
|
$item['journal_count'] = Db::name('journal_promotion_field')
|
||||||
|
->where('expert_fetch_id', $item['expert_fetch_id'])
|
||||||
|
->where('state', 0)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
// 与推广选人一致:按字段关键词匹配,统计可用专家(去重)
|
||||||
|
$item['expert_count'] = Db::name('expert_field')->alias('ef')
|
||||||
|
->join('t_expert e', 'e.expert_id = ef.expert_id', 'inner')
|
||||||
|
->where('ef.state', 0)
|
||||||
|
->where('e.state', 0)
|
||||||
|
->where('ef.field', 'like', '%' . $fieldName . '%')
|
||||||
|
->count('distinct ef.expert_id');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'list' => $list,
|
||||||
|
'total' => $total,
|
||||||
|
'pageIndex' => $page,
|
||||||
|
'pageSize' => $pageSize,
|
||||||
|
'totalPages' => $total > 0 ? ceil($total / $pageSize) : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增抓取领域
|
||||||
|
* 参数: field(必填), source(选填,默认pubmed)
|
||||||
|
*/
|
||||||
|
public function addFetchField()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$field = trim(isset($data['field']) ? $data['field'] : '');
|
||||||
|
$source = trim(isset($data['source']) ? $data['source'] : 'pubmed');
|
||||||
|
|
||||||
|
if ($field === '') {
|
||||||
|
return jsonError('field不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = Db::name('expert_fetch')
|
||||||
|
->where('field', $field)
|
||||||
|
->where('source', $source)
|
||||||
|
->find();
|
||||||
|
if ($exists) {
|
||||||
|
if ($exists['state'] == 1) {
|
||||||
|
Db::name('expert_fetch')
|
||||||
|
->where('expert_fetch_id', $exists['expert_fetch_id'])
|
||||||
|
->update(['state' => 0]);
|
||||||
|
return jsonSuccess(['expert_fetch_id' => $exists['expert_fetch_id'], 'msg' => 'reactivated']);
|
||||||
|
}
|
||||||
|
return jsonError('该领域已存在 (expert_fetch_id=' . $exists['expert_fetch_id'] . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = Db::name('expert_fetch')->insertGetId([
|
||||||
|
'field' => mb_substr($field, 0, 128),
|
||||||
|
'source' => mb_substr($source, 0, 128),
|
||||||
|
'last_page' => 0,
|
||||||
|
'total_pages' => 0,
|
||||||
|
'last_time' => 0,
|
||||||
|
'state' => 0,
|
||||||
|
'ctime' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return jsonSuccess(['expert_fetch_id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑抓取领域
|
||||||
|
* 参数: expert_fetch_id(必填), field, source, state
|
||||||
|
*/
|
||||||
|
public function editFetchField()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$id = intval(isset($data['expert_fetch_id']) ? $data['expert_fetch_id'] : 0);
|
||||||
|
if (!$id) {
|
||||||
|
return jsonError('expert_fetch_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$record = Db::name('expert_fetch')->where('expert_fetch_id', $id)->find();
|
||||||
|
if (!$record) {
|
||||||
|
return jsonError('记录不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = [];
|
||||||
|
if (isset($data['field'])) $update['field'] = mb_substr(trim($data['field']), 0, 128);
|
||||||
|
if (isset($data['source'])) $update['source'] = mb_substr(trim($data['source']), 0, 128);
|
||||||
|
if (isset($data['state'])) $update['state'] = intval($data['state']);
|
||||||
|
|
||||||
|
if (!empty($update)) {
|
||||||
|
Db::name('expert_fetch')->where('expert_fetch_id', $id)->update($update);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess(['expert_fetch_id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除/停用抓取领域(软删除 state=1)
|
||||||
|
* 参数: expert_fetch_id(必填), hard(传1物理删除)
|
||||||
|
*/
|
||||||
|
public function deleteFetchField()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$id = intval(isset($data['expert_fetch_id']) ? $data['expert_fetch_id'] : 0);
|
||||||
|
$hard = intval(isset($data['hard']) ? $data['hard'] : 0);
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
return jsonError('expert_fetch_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hard) {
|
||||||
|
Db::name('journal_promotion_field')->where('expert_fetch_id', $id)->delete();
|
||||||
|
Db::name('expert_fetch')->where('expert_fetch_id', $id)->delete();
|
||||||
|
} else {
|
||||||
|
Db::name('expert_fetch')->where('expert_fetch_id', $id)->update(['state' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1214,14 +1214,14 @@ class Finalreview extends Base
|
|||||||
|
|
||||||
//获取文章记录
|
//获取文章记录
|
||||||
$aWhere = ['article_id' => $iArticleId,'state' => 0,'state_to' => ['in',[0,1,5]]];
|
$aWhere = ['article_id' => $iArticleId,'state' => 0,'state_to' => ['in',[0,1,5]]];
|
||||||
$aArticleMsg = Db::name('article_msg')->field('state_from,state_to,ctime')->where($aWhere)->order('ctime deac')->select();
|
$aArticleMsg = Db::name('article_msg')->field('state_to,ctime')->where($aWhere)->order('ctime deac')->select();
|
||||||
if(!empty($aArticleMsg)){
|
if(!empty($aArticleMsg)){
|
||||||
$iReceivedTime = $iRevisionTime = $iAcceptedTime = 0;
|
$iReceivedTime = $iRevisionTime = $iAcceptedTime = 0;
|
||||||
foreach ($aArticleMsg as $key => $value) {
|
foreach ($aArticleMsg as $key => $value) {
|
||||||
if(empty($iReceivedTime) && $value['state_to'] == 0){
|
if(empty($iReceivedTime) && $value['state_to'] == 0){
|
||||||
$iReceivedTime = $value['ctime'];
|
$iReceivedTime = $value['ctime'];
|
||||||
}
|
}
|
||||||
if(empty($iRevisionTime) && $value['state_to'] == 1){
|
if($value['state_to'] == 1 && $iRevisionTime < $value['ctime']){
|
||||||
$iRevisionTime = $value['ctime'];
|
$iRevisionTime = $value['ctime'];
|
||||||
}
|
}
|
||||||
if(empty($iAcceptedTime) && $value['state_to'] == 5){
|
if(empty($iAcceptedTime) && $value['state_to'] == 5){
|
||||||
|
|||||||
@@ -334,6 +334,14 @@ class Journal extends Base {
|
|||||||
$aJournalUpdate['wechat_app_secret'] = $update['wechat_app_secret'];
|
$aJournalUpdate['wechat_app_secret'] = $update['wechat_app_secret'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(isset($data['editor_name'])&&$data['editor_name']!=''){
|
||||||
|
$update['editor_name'] = $data['editor_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($data['databases'])&&$data['databases']!=''){
|
||||||
|
$update['databases'] = $data['databases'];
|
||||||
|
}
|
||||||
|
|
||||||
if(!empty($aJournalUpdate)){
|
if(!empty($aJournalUpdate)){
|
||||||
$aJournalUpdate['issn'] = $journal_info['issn'];
|
$aJournalUpdate['issn'] = $journal_info['issn'];
|
||||||
$sUrl = $this->sJournalUrl."wechat/Article/updateJournal";
|
$sUrl = $this->sJournalUrl."wechat/Article/updateJournal";
|
||||||
|
|||||||
261
application/api/controller/MailTemplate.php
Normal file
261
application/api/controller/MailTemplate.php
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
class MailTemplate extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create or update a mail template (V2)
|
||||||
|
* POST fields:
|
||||||
|
* - template_id (optional)
|
||||||
|
* - journal_id, scene, language, title, subject, body_html, body_text, variables_json, version, is_active
|
||||||
|
*/
|
||||||
|
public function saveTemplate()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
|
||||||
|
$rule = new Validate([
|
||||||
|
'journal_id' => 'require|number',
|
||||||
|
'scene' => 'require',
|
||||||
|
'language' => 'require',
|
||||||
|
'title' => 'require',
|
||||||
|
'subject' => 'require',
|
||||||
|
]);
|
||||||
|
if (!$rule->check($data)) {
|
||||||
|
return jsonError($rule->getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'journal_id' => intval($data['journal_id']),
|
||||||
|
'scene' => trim($data['scene']),
|
||||||
|
'language' => trim($data['language']),
|
||||||
|
'title' => trim($data['title']),
|
||||||
|
'subject' => trim($data['subject']),
|
||||||
|
'body_html' => $data['body_html'] ?? '',
|
||||||
|
'body_text' => $data['body_text'] ?? '',
|
||||||
|
'variables_json' => $data['variables_json'] ?? '',
|
||||||
|
'version' => intval($data['version'] ?? 1),
|
||||||
|
'is_active' => intval($data['is_active'] ?? 1),
|
||||||
|
'utime' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$templateId = intval($data['template_id'] ?? 0);
|
||||||
|
if ($templateId) {
|
||||||
|
$exists = Db::name('mail_template')->where('template_id', $templateId)->where('state', 0)->find();
|
||||||
|
if (!$exists) {
|
||||||
|
return jsonError('Template not found');
|
||||||
|
}
|
||||||
|
Db::name('mail_template')->where('template_id', $templateId)->update($payload);
|
||||||
|
return jsonSuccess(['template_id' => $templateId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['ctime'] = time();
|
||||||
|
$payload['state'] = 0;
|
||||||
|
$newId = Db::name('mail_template')->insertGetId($payload);
|
||||||
|
return jsonSuccess(['template_id' => $newId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft delete a template
|
||||||
|
* Params: template_id
|
||||||
|
*/
|
||||||
|
public function deleteTemplate()
|
||||||
|
{
|
||||||
|
$templateId = intval($this->request->param('template_id', 0));
|
||||||
|
if (!$templateId) {
|
||||||
|
return jsonError('template_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::name('mail_template')->where('template_id', $templateId)->update(['state' => 1, 'utime' => time()]);
|
||||||
|
return jsonSuccess([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get template detail
|
||||||
|
*/
|
||||||
|
public function getTemplate()
|
||||||
|
{
|
||||||
|
$templateId = intval($this->request->param('template_id', 0));
|
||||||
|
if (!$templateId) {
|
||||||
|
return jsonError('template_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tpl = Db::name('mail_template')->where('template_id', $templateId)->where('state', 0)->find();
|
||||||
|
if (!$tpl) {
|
||||||
|
return jsonError('Template not found');
|
||||||
|
}
|
||||||
|
return jsonSuccess(['template' => $tpl]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List templates
|
||||||
|
* Params: journal_id, scene(optional), language(optional), is_active(optional)
|
||||||
|
*/
|
||||||
|
public function listTemplates()
|
||||||
|
{
|
||||||
|
$journalId = intval($this->request->param('journal_id', 0));
|
||||||
|
if (!$journalId) {
|
||||||
|
return jsonError('journal_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$scene = trim($this->request->param('scene', ''));
|
||||||
|
$language = trim($this->request->param('language', ''));
|
||||||
|
$isActive = $this->request->param('is_active', '');
|
||||||
|
|
||||||
|
$where = ['journal_id' => $journalId, 'state' => 0];
|
||||||
|
if ($scene !== '') $where['scene'] = $scene;
|
||||||
|
if ($language !== '') $where['language'] = $language;
|
||||||
|
if ($isActive !== '') $where['is_active'] = intval($isActive);
|
||||||
|
|
||||||
|
$list = Db::name('mail_template')
|
||||||
|
->where($where)
|
||||||
|
->order('is_active desc, utime desc, template_id desc')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
return jsonSuccess(['list' => $list]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listTemplatesAll(){
|
||||||
|
$list = Db::name('mail_template')
|
||||||
|
->where('state', 0)
|
||||||
|
->order('is_active desc, utime desc, template_id desc')
|
||||||
|
->select();
|
||||||
|
return jsonSuccess(['list'=>$list]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update a global mail style
|
||||||
|
* 当前 style 表字段:
|
||||||
|
* - style_id, name, description, header_html, footer_html, state, ctime
|
||||||
|
*/
|
||||||
|
public function saveStyle()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
$rule = new Validate([
|
||||||
|
'name' => 'require',
|
||||||
|
]);
|
||||||
|
if (!$rule->check($data)) {
|
||||||
|
return jsonError($rule->getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'name' => trim($data['name']),
|
||||||
|
'description' => trim($data['description'] ?? ''),
|
||||||
|
'header_html' => $data['header_html'] ?? '',
|
||||||
|
'footer_html' => $data['footer_html'] ?? '',
|
||||||
|
'state' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$styleId = intval($data['style_id'] ?? 0);
|
||||||
|
if ($styleId) {
|
||||||
|
$exists = Db::name('mail_style')->where('style_id', $styleId)->where('state', 0)->find();
|
||||||
|
if (!$exists) {
|
||||||
|
return jsonError('Style not found');
|
||||||
|
}
|
||||||
|
Db::name('mail_style')->where('style_id', $styleId)->update($payload);
|
||||||
|
return jsonSuccess(['style_id' => $styleId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['ctime'] = time();
|
||||||
|
$newId = Db::name('mail_style')->insertGetId($payload);
|
||||||
|
return jsonSuccess(['style_id' => $newId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a global mail style (soft delete)
|
||||||
|
*/
|
||||||
|
public function deleteStyle()
|
||||||
|
{
|
||||||
|
$styleId = intval($this->request->param('style_id', 0));
|
||||||
|
if (!$styleId) {
|
||||||
|
return jsonError('style_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::name('mail_style')->where('style_id', $styleId)->update(['state' => 1]);
|
||||||
|
return jsonSuccess([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get style detail
|
||||||
|
*/
|
||||||
|
public function getStyle()
|
||||||
|
{
|
||||||
|
$styleId = intval($this->request->param('style_id', 0));
|
||||||
|
if (!$styleId) {
|
||||||
|
return jsonError('style_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$style = Db::name('mail_style')->where('style_id', $styleId)->where('state', 0)->find();
|
||||||
|
if (!$style) {
|
||||||
|
return jsonError('Style not found');
|
||||||
|
}
|
||||||
|
return jsonSuccess(['style' => $style]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List styles
|
||||||
|
* 现在样式不再按 scene / language 区分,只返回全部正常状态的样式
|
||||||
|
*/
|
||||||
|
public function listStyles()
|
||||||
|
{
|
||||||
|
$where = ['state' => 0];
|
||||||
|
|
||||||
|
$list = Db::name('mail_style')
|
||||||
|
->where($where)
|
||||||
|
->order('style_id desc')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
return jsonSuccess(['list' => $list]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render preview for a template预览效果,没啥用
|
||||||
|
* Params: template_id, vars (json string)
|
||||||
|
*/
|
||||||
|
public function preview()
|
||||||
|
{
|
||||||
|
$templateId = intval($this->request->param('template_id', 0));
|
||||||
|
$varsJson = $this->request->param('vars', '');
|
||||||
|
if (!$templateId) {
|
||||||
|
return jsonError('template_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tpl = Db::name('mail_template')->where('template_id', $templateId)->where('state', 0)->find();
|
||||||
|
if (!$tpl) {
|
||||||
|
return jsonError('Template not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$vars = [];
|
||||||
|
if ($varsJson) {
|
||||||
|
$decoded = json_decode($varsJson, true);
|
||||||
|
if (is_array($decoded)) $vars = $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = $this->render($tpl['subject'], $vars);
|
||||||
|
$body = $this->render($tpl['body_html'], $vars);
|
||||||
|
|
||||||
|
// For preview we do not enforce a style; caller can combine with style if needed
|
||||||
|
return jsonSuccess(['subject' => $subject, 'body' => $body]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render($tpl, $vars)
|
||||||
|
{
|
||||||
|
if (!is_string($tpl) || empty($tpl)) return '';
|
||||||
|
if (!is_array($vars) || empty($vars)) return $tpl;
|
||||||
|
|
||||||
|
$replace = [];
|
||||||
|
foreach ($vars as $k => $v) {
|
||||||
|
$key = trim((string)$k);
|
||||||
|
if ($key === '') continue;
|
||||||
|
$replace['{{' . $key . '}}'] = (string)$v;
|
||||||
|
// backward compatible placeholders
|
||||||
|
$replace['{' . $key . '}'] = (string)$v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace(array_keys($replace), array_values($replace), $tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -843,14 +843,14 @@ class Preaccept extends Base
|
|||||||
}
|
}
|
||||||
$am_info = $this->article_main_obj->where("am_id",$data['am_id'])->find();
|
$am_info = $this->article_main_obj->where("am_id",$data['am_id'])->find();
|
||||||
//上一行,空行
|
//上一行,空行
|
||||||
$p_list = $this->article_main_obj->where("article_id",$am_info['article_id'])->where("sort","<",$am_info['sort'])->whereIn("state",[0,2])->order("sort desc")->limit(1)->select();
|
// $p_list = $this->article_main_obj->where("article_id",$am_info['article_id'])->where("sort","<",$am_info['sort'])->whereIn("state",[0,2])->order("sort desc")->limit(1)->select();
|
||||||
if($p_list&&($p_list[0]['type']>0||$p_list[0]['content']!="")){
|
// if($p_list&&($p_list[0]['type']>0||$p_list[0]['content']!="")){
|
||||||
$this->addBRow($am_info['article_id'],$p_list[0]['am_id']);
|
// $this->addBRow($am_info['article_id'],$p_list[0]['am_id']);
|
||||||
}
|
// }
|
||||||
$n_list = $this->article_main_obj->where("article_id",$am_info['article_id'])->where("sort",">",$am_info['sort'])->whereIn("state",[0,2])->order("sort asc")->limit(1)->select();
|
// $n_list = $this->article_main_obj->where("article_id",$am_info['article_id'])->where("sort",">",$am_info['sort'])->whereIn("state",[0,2])->order("sort asc")->limit(1)->select();
|
||||||
if($n_list[0]['type']>0||$n_list[0]['content']!=""){
|
// if($n_list[0]['type']>0||$n_list[0]['content']!=""){
|
||||||
$this->addBRow($am_info['article_id'],$data['am_id']);
|
// $this->addBRow($am_info['article_id'],$data['am_id']);
|
||||||
}
|
// }
|
||||||
$this->article_main_obj->where("am_id",$data['am_id'])->update(["is_h1"=>1,"is_h2"=>0,"is_h3"=>0]);
|
$this->article_main_obj->where("am_id",$data['am_id'])->update(["is_h1"=>1,"is_h2"=>0,"is_h3"=>0]);
|
||||||
// return jsonSuccess([]);
|
// return jsonSuccess([]);
|
||||||
//返回数据 20260119 start
|
//返回数据 20260119 start
|
||||||
@@ -872,10 +872,10 @@ class Preaccept extends Base
|
|||||||
}
|
}
|
||||||
$am_info = $this->article_main_obj->where("am_id",$data['am_id'])->find();
|
$am_info = $this->article_main_obj->where("am_id",$data['am_id'])->find();
|
||||||
//上一行,空行
|
//上一行,空行
|
||||||
$p_list = $this->article_main_obj->where("article_id",$am_info['article_id'])->where("sort","<",$am_info['sort'])->whereIn("state",[0,2])->order("sort desc")->limit(1)->select();
|
// $p_list = $this->article_main_obj->where("article_id",$am_info['article_id'])->where("sort","<",$am_info['sort'])->whereIn("state",[0,2])->order("sort desc")->limit(1)->select();
|
||||||
if($p_list&&($p_list[0]['type']>0||$p_list[0]['content']!="")){
|
// if($p_list&&($p_list[0]['type']>0||$p_list[0]['content']!="")){
|
||||||
$this->addBRow($am_info['article_id'],$p_list[0]['am_id']);
|
// $this->addBRow($am_info['article_id'],$p_list[0]['am_id']);
|
||||||
}
|
// }
|
||||||
$this->article_main_obj->where("am_id",$data['am_id'])->update(["is_h1"=>0,"is_h2"=>1,"is_h3"=>0]);
|
$this->article_main_obj->where("am_id",$data['am_id'])->update(["is_h1"=>0,"is_h2"=>1,"is_h3"=>0]);
|
||||||
// return jsonSuccess([]);
|
// return jsonSuccess([]);
|
||||||
//返回数据 20260119 start
|
//返回数据 20260119 start
|
||||||
|
|||||||
@@ -518,6 +518,12 @@ class Production extends Base
|
|||||||
$p_info = $this->production_article_obj->where('p_article_id', $data['p_article_id'])->find();
|
$p_info = $this->production_article_obj->where('p_article_id', $data['p_article_id'])->find();
|
||||||
$journal_info = $this->journal_obj->where('journal_id', $p_info['journal_id'])->find();
|
$journal_info = $this->journal_obj->where('journal_id', $p_info['journal_id'])->find();
|
||||||
|
|
||||||
|
//是否显示图文摘要图片1是2否3默认 20260206 start
|
||||||
|
$is_graphical_abstract = empty($data['is_graphical_abstract']) ? 3 : $data['is_graphical_abstract'];
|
||||||
|
if(!in_array($is_graphical_abstract, [1,2])){
|
||||||
|
return jsonError('Please select whether the uploaded picture should be shown as graphic abstract on the PDF title page');
|
||||||
|
}
|
||||||
|
//是否显示图文摘要图片1是2否3默认 20260206 end
|
||||||
|
|
||||||
$update['title'] = trim($data['title']);
|
$update['title'] = trim($data['title']);
|
||||||
$update['journal_stage_id'] = $data['journal_stage_id'];
|
$update['journal_stage_id'] = $data['journal_stage_id'];
|
||||||
@@ -537,6 +543,10 @@ class Production extends Base
|
|||||||
// $update['pub_date'] = trim($data['pub_date']);
|
// $update['pub_date'] = trim($data['pub_date']);
|
||||||
$update['npp'] = isset($data['npp']) ? $data['npp'] : '';
|
$update['npp'] = isset($data['npp']) ? $data['npp'] : '';
|
||||||
|
|
||||||
|
//是否显示图文摘要图片1是2否3默认 20260206 start
|
||||||
|
$update['is_graphical_abstract'] = $is_graphical_abstract;
|
||||||
|
//是否显示图文摘要图片1是2否3默认 20260206 end
|
||||||
|
|
||||||
//生成doi号
|
//生成doi号
|
||||||
// $doi = '';
|
// $doi = '';
|
||||||
// if ($p_info['doi'] == '') {
|
// if ($p_info['doi'] == '') {
|
||||||
@@ -1709,6 +1719,11 @@ class Production extends Base
|
|||||||
}
|
}
|
||||||
//判断邮箱是否重复 chengxiaoling 20250926 end
|
//判断邮箱是否重复 chengxiaoling 20250926 end
|
||||||
|
|
||||||
|
//判断是否是通讯作者 通讯地址必填 20260206 start
|
||||||
|
if(isset($data['is_report']) && $data['is_report'] == 1 && empty($data['mailing_address'])){
|
||||||
|
return jsonError("Please enter your mailing address");
|
||||||
|
}
|
||||||
|
//判断是否是通讯作者 通讯地址必填 20260206 end
|
||||||
$article_info = $this->article_obj->where('article_id', $old_article_author_info['article_id'])->find();
|
$article_info = $this->article_obj->where('article_id', $old_article_author_info['article_id'])->find();
|
||||||
$updata['author_name'] = $article_info['journal_id'] == 21 ? trim($data['last_name']) . trim($data['first_name']) : trim($data['first_name']) . ' ' . trim($data['last_name']);
|
$updata['author_name'] = $article_info['journal_id'] == 21 ? trim($data['last_name']) . trim($data['first_name']) : trim($data['first_name']) . ' ' . trim($data['last_name']);
|
||||||
$updata['first_name'] = trim($data['first_name']);
|
$updata['first_name'] = trim($data['first_name']);
|
||||||
@@ -1718,6 +1733,11 @@ class Production extends Base
|
|||||||
$updata['is_first'] = $data['is_first'];
|
$updata['is_first'] = $data['is_first'];
|
||||||
$updata['is_report'] = $data['is_report'];
|
$updata['is_report'] = $data['is_report'];
|
||||||
$updata['email'] = isset($data['email']) ? trim($data['email']) : '';
|
$updata['email'] = isset($data['email']) ? trim($data['email']) : '';
|
||||||
|
|
||||||
|
//通信地址 20260206 start
|
||||||
|
$updata['mailing_address'] = isset($data['mailing_address']) ? trim($data['mailing_address']) : '';
|
||||||
|
//通信地址 20260206 end
|
||||||
|
|
||||||
$this->production_article_author_obj->where('p_article_author_id', $data['p_article_author_id'])->update($updata);
|
$this->production_article_author_obj->where('p_article_author_id', $data['p_article_author_id'])->update($updata);
|
||||||
if (is_array($data['organs'])) {
|
if (is_array($data['organs'])) {
|
||||||
$has_ids = [];
|
$has_ids = [];
|
||||||
@@ -1811,6 +1831,12 @@ class Production extends Base
|
|||||||
}
|
}
|
||||||
//判断邮箱是否重复 chengxiaoling 20250926 end
|
//判断邮箱是否重复 chengxiaoling 20250926 end
|
||||||
|
|
||||||
|
//判断是否是通讯作者 通讯地址必填 20260206 start
|
||||||
|
if(isset($data['is_report']) && $data['is_report'] == 1 && empty($data['mailing_address'])){
|
||||||
|
return jsonError("Please enter your mailing address");
|
||||||
|
}
|
||||||
|
//判断是否是通讯作者 通讯地址必填 20260206 end
|
||||||
|
|
||||||
$insert['p_article_id'] = $data['p_article_id'];
|
$insert['p_article_id'] = $data['p_article_id'];
|
||||||
$insert['article_id'] = $p_info['article_id'];
|
$insert['article_id'] = $p_info['article_id'];
|
||||||
$insert['author_name'] = $article_info['journal_id'] == 21 ? trim($data['last_name']) . trim($data['first_name']) : trim($data['first_name']) . ' ' . trim($data['last_name']);
|
$insert['author_name'] = $article_info['journal_id'] == 21 ? trim($data['last_name']) . trim($data['first_name']) : trim($data['first_name']) . ' ' . trim($data['last_name']);
|
||||||
@@ -1822,6 +1848,10 @@ class Production extends Base
|
|||||||
$insert['is_report'] = $data['is_report'];
|
$insert['is_report'] = $data['is_report'];
|
||||||
$insert['email'] = isset($data['email']) ? trim($data['email']) : '';
|
$insert['email'] = isset($data['email']) ? trim($data['email']) : '';
|
||||||
|
|
||||||
|
//通信地址 20260206 start
|
||||||
|
$insert['mailing_address'] = isset($data['mailing_address']) ? trim($data['mailing_address']) : '';
|
||||||
|
//通信地址 20260206 end
|
||||||
|
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
$pa_id = $this->production_article_author_obj->insertGetId($insert);
|
$pa_id = $this->production_article_author_obj->insertGetId($insert);
|
||||||
$or_res = true;
|
$or_res = true;
|
||||||
@@ -2248,8 +2278,8 @@ class Production extends Base
|
|||||||
return jsonError($rule->getError());
|
return jsonError($rule->getError());
|
||||||
}
|
}
|
||||||
$info = $this->article_main_obj->where("am_id",$data['id'])->find();
|
$info = $this->article_main_obj->where("am_id",$data['id'])->find();
|
||||||
if(isset($data["code"])){
|
if(isset($data["content"])){
|
||||||
$content = $data['code'];
|
$content = $data['content'];
|
||||||
}else{
|
}else{
|
||||||
$content = $info['content'];
|
$content = $info['content'];
|
||||||
}
|
}
|
||||||
@@ -2270,9 +2300,21 @@ class Production extends Base
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
//单行处理公式内容
|
//单行处理公式内容
|
||||||
$pattern = '/^<wmath[^>]*?\s+data-latex="([^"]*)"[^>]*?>.*?<\/wmath>$/';
|
/* $pattern = '/^<wmath[^>]*?\s+data-latex="([^"]*)"[^>]*?>.*?<\/wmath>$/';*/
|
||||||
|
// if(preg_match($pattern, $content) === 1){
|
||||||
|
// $pattern1 = '/<wmath[^>]*?\s+data-latex="([^"]*)">/';
|
||||||
|
// if (preg_match($pattern1, $content, $matches)) {
|
||||||
|
// $latexContent = $matches[1];
|
||||||
|
// // 将 $$ 转换为 $,因为equation环境不需要$$
|
||||||
|
// $latexContent = str_replace('$$', '', $latexContent);
|
||||||
|
//
|
||||||
|
// return "\\begin{equation}\n\t{$latexContent}\n\\end{equation}";
|
||||||
|
// }
|
||||||
|
// return $content;
|
||||||
|
// }
|
||||||
|
$pattern = '/^<wmath[^>]*?\s+data-latex="([^"]*)"[^>]*?\s+data-wrap="block"[^>]*?>.*?<\/wmath>$/';
|
||||||
if(preg_match($pattern, $content) === 1){
|
if(preg_match($pattern, $content) === 1){
|
||||||
$pattern1 = '/<wmath[^>]*?\s+data-latex="([^"]*)">/';
|
$pattern1 = '/<wmath[^>]*?\s+data-latex="([^"]*)"[^>]*?\s+data-wrap="block"[^>]*?>/';
|
||||||
if (preg_match($pattern1, $content, $matches)) {
|
if (preg_match($pattern1, $content, $matches)) {
|
||||||
$latexContent = $matches[1];
|
$latexContent = $matches[1];
|
||||||
// 将 $$ 转换为 $,因为equation环境不需要$$
|
// 将 $$ 转换为 $,因为equation环境不需要$$
|
||||||
@@ -2330,7 +2372,7 @@ class Production extends Base
|
|||||||
$latex .= $this->escapeLatexSpecialChars($content);
|
$latex .= $this->escapeLatexSpecialChars($content);
|
||||||
$content = '';
|
$content = '';
|
||||||
}else {
|
}else {
|
||||||
$tagOpen = "<{$nextTag}>";
|
$tagOpen = "<{$nextTag}";
|
||||||
$tagClose = "</{$nextTag}>";
|
$tagClose = "</{$nextTag}>";
|
||||||
$beginTag = strpos($content, $tagOpen);
|
$beginTag = strpos($content, $tagOpen);
|
||||||
|
|
||||||
@@ -2357,11 +2399,12 @@ class Production extends Base
|
|||||||
|
|
||||||
|
|
||||||
//这里处理引用
|
//这里处理引用
|
||||||
if ($check_refer&&preg_match('/(?:<blue>)?\[(\d+(?:[-,]\d+)*)\](?:<\/blue>)?/', $tagWrappedContent, $matches)) {
|
if ($check_refer&&preg_match('/(?:<blue>)?\[\s*(\d+(?:\s*[-,]\s*\d+)*)\s*\](?:<\/blue>)?/', $tagWrappedContent, $matches)) {
|
||||||
// 去除匹配中的 <blue> 和 </blue>
|
// 去除匹配中的 <blue> 和 </blue>
|
||||||
$cleanedMatch = str_replace(['<blue>', '</blue>'], '', $matches[0]);
|
$cleanedMatch = str_replace(['<blue>', '</blue>'], '', $matches[0]);
|
||||||
// 提取引用编号部分(去掉方括号)
|
// 提取引用编号部分(去掉方括号)
|
||||||
$referencePart = trim($cleanedMatch, '[]');
|
$referencePart = trim($cleanedMatch, '[]');
|
||||||
|
$referencePart = preg_replace('/\s+/', '', $referencePart); // 移除所有空格
|
||||||
|
|
||||||
// 分割逗号分隔的不同引用项
|
// 分割逗号分隔的不同引用项
|
||||||
$parts = explode(',', $referencePart);
|
$parts = explode(',', $referencePart);
|
||||||
@@ -2419,7 +2462,7 @@ class Production extends Base
|
|||||||
* @return int|false 闭合标签的起始位置
|
* @return int|false 闭合标签的起始位置
|
||||||
*/
|
*/
|
||||||
private function getLastTabIndex($content, $tag) {
|
private function getLastTabIndex($content, $tag) {
|
||||||
$tagOpen = "<{$tag}>";
|
$tagOpen = "<{$tag}";
|
||||||
$tagClose = "</{$tag}>";
|
$tagClose = "</{$tag}>";
|
||||||
|
|
||||||
$beginTag = strpos($content, $tagOpen);
|
$beginTag = strpos($content, $tagOpen);
|
||||||
@@ -2435,7 +2478,7 @@ class Production extends Base
|
|||||||
|
|
||||||
// 统计当前标签内的开放标签数量
|
// 统计当前标签内的开放标签数量
|
||||||
$caNowStr = substr($content, $beginTag, $endTagCa - $beginTag + strlen($tagClose));
|
$caNowStr = substr($content, $beginTag, $endTagCa - $beginTag + strlen($tagClose));
|
||||||
$caCount = preg_match_all("/<{$tag}>/", $caNowStr, $matches);
|
$caCount = preg_match_all("/<{$tag}/", $caNowStr, $matches);
|
||||||
|
|
||||||
if ($caCount === 1) {
|
if ($caCount === 1) {
|
||||||
return $endTagCa;
|
return $endTagCa;
|
||||||
@@ -2448,7 +2491,7 @@ class Production extends Base
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$substring = substr($content, $beginTag, $numIndex - $beginTag + strlen($tagClose));
|
$substring = substr($content, $beginTag, $numIndex - $beginTag + strlen($tagClose));
|
||||||
$ccaCount = preg_match_all("/<{$tag}>/", $substring, $matches);
|
$ccaCount = preg_match_all("/<{$tag}/", $substring, $matches);
|
||||||
if ($ccaCount !== $caCount) {
|
if ($ccaCount !== $caCount) {
|
||||||
$caCount = $ccaCount;
|
$caCount = $ccaCount;
|
||||||
} else {
|
} else {
|
||||||
@@ -2503,7 +2546,7 @@ class Production extends Base
|
|||||||
if (preg_match($pattern, $now, $matches)) {
|
if (preg_match($pattern, $now, $matches)) {
|
||||||
$figureId = $matches[1];
|
$figureId = $matches[1];
|
||||||
//处理查找到id之后的逻辑
|
//处理查找到id之后的逻辑
|
||||||
$textRenderData[] = "\\ref{fig:".$figureId."}";
|
$textRenderData[] = "\\autoref{fig:".$figureId."}";
|
||||||
}
|
}
|
||||||
}elseif ($tag === 'mytable'){//对表格的正文处理
|
}elseif ($tag === 'mytable'){//对表格的正文处理
|
||||||
$pattern = '/data-id=["\'](\d+)["\']/';
|
$pattern = '/data-id=["\'](\d+)["\']/';
|
||||||
@@ -2528,7 +2571,7 @@ class Production extends Base
|
|||||||
if(strlen($cache_table['table_data'])>6000){
|
if(strlen($cache_table['table_data'])>6000){
|
||||||
$textRenderData[] = "\\textcolor[HTML]{".$this->colorMap['blue']."}{Supplementary Table ".$key_num."}}";
|
$textRenderData[] = "\\textcolor[HTML]{".$this->colorMap['blue']."}{Supplementary Table ".$key_num."}}";
|
||||||
}else{
|
}else{
|
||||||
$textRenderData[] = "\\ref{tab:".$tableId."}";
|
$textRenderData[] = "\\autoref{tab:".$tableId."}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
@@ -2601,7 +2644,7 @@ class Production extends Base
|
|||||||
* @return string 标签名或'no'
|
* @return string 标签名或'no'
|
||||||
*/
|
*/
|
||||||
private function determineNextTag($content) {
|
private function determineNextTag($content) {
|
||||||
$tagPattern = '/<(' . implode('|', $this->supportedTags) . ')>/i';
|
$tagPattern = '/<(' . implode('|', $this->supportedTags) . ')\b[^>]*>/i';
|
||||||
if (preg_match($tagPattern, $content, $matches)) {
|
if (preg_match($tagPattern, $content, $matches)) {
|
||||||
return $matches[1];
|
return $matches[1];
|
||||||
}
|
}
|
||||||
@@ -2637,6 +2680,9 @@ class Production extends Base
|
|||||||
$style['color'] = $this->colorMap['r'];
|
$style['color'] = $this->colorMap['r'];
|
||||||
break;
|
break;
|
||||||
// tr标签已被移除,无需处理
|
// tr标签已被移除,无需处理
|
||||||
|
case 'myh3':
|
||||||
|
$style['h3'] = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2678,6 +2724,11 @@ class Production extends Base
|
|||||||
$content = '$_{{'.$content.'}}$';
|
$content = '$_{{'.$content.'}}$';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理h3标签
|
||||||
|
if (isset($style['h3']) && $style['h3']) {
|
||||||
|
$content = "\\textbf{{$content}}";
|
||||||
|
}
|
||||||
|
|
||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2718,12 +2769,15 @@ class Production extends Base
|
|||||||
if ($ref['refer_type'] == "journal") {
|
if ($ref['refer_type'] == "journal") {
|
||||||
return $this->generateFromParsedData($ref, $citationKey);
|
return $this->generateFromParsedData($ref, $citationKey);
|
||||||
} elseif ($ref['refer_type'] == "book"){
|
} elseif ($ref['refer_type'] == "book"){
|
||||||
|
return $this->generateOtherBook($ref, $citationKey);
|
||||||
} else {
|
} else {
|
||||||
return $this->generateOtherMsic($ref, $citationKey);
|
return $this->generateOtherMsic($ref, $citationKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private function generateOtherBook($ref, $citationKey){
|
private function generateOtherBook($ref, $citationKey){
|
||||||
$entry = "@book{{$citationKey},\n";
|
$entry = "@book{{$citationKey},\n";
|
||||||
|
|
||||||
@@ -3270,6 +3324,16 @@ class Production extends Base
|
|||||||
$article_info = $this->article_obj->where('article_id', $p_info['article_id'])->find();
|
$article_info = $this->article_obj->where('article_id', $p_info['article_id'])->find();
|
||||||
$journal_info = $this->journal_obj->where('journal_id', $article_info['journal_id'])->find();
|
$journal_info = $this->journal_obj->where('journal_id', $article_info['journal_id'])->find();
|
||||||
$user_info = $this->user_obj->where('user_id', $article_info['user_id'])->find();
|
$user_info = $this->user_obj->where('user_id', $article_info['user_id'])->find();
|
||||||
|
|
||||||
|
//查询文章是否有Accept记录 20260210 start
|
||||||
|
$article_info_id = empty($article_info['article_id']) ? 0 : $article_info['article_id'];
|
||||||
|
$aMsgWhere = ['article_id' => $article_info_id,'state_to' => 5];
|
||||||
|
$aArticleMsg = Db::name('article_msg')->field('msg_id')->where($aMsgWhere)->find();
|
||||||
|
if(empty($aArticleMsg)){
|
||||||
|
return jsonError("Please set the paper as Accept before asking the author to confirm");
|
||||||
|
}
|
||||||
|
//查询文章是否有Accept记录 20260210 end
|
||||||
|
|
||||||
$this->pdfAddProof($p_info['article_id']);
|
$this->pdfAddProof($p_info['article_id']);
|
||||||
if ($p_info['file_pdf'] == '') {
|
if ($p_info['file_pdf'] == '') {
|
||||||
return jsonError('To the editor: PROOF is the final form before the article goes online. The PROOF link step not be opened if you have not completed the previous steps.');
|
return jsonError('To the editor: PROOF is the final form before the article goes online. The PROOF link step not be opened if you have not completed the previous steps.');
|
||||||
|
|||||||
276
application/api/controller/Wechat.php
Normal file
276
application/api/controller/Wechat.php
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use think\Cache;
|
||||||
|
use think\Env;
|
||||||
|
|
||||||
|
class Wechat extends Base
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $appid;
|
||||||
|
protected $appsecret;
|
||||||
|
|
||||||
|
public function __construct(\think\Request $request = null)
|
||||||
|
{
|
||||||
|
parent::__construct($request);
|
||||||
|
$this->appid = Env::get('wechat.appid', '');
|
||||||
|
$this->appsecret = Env::get('wechat.appsecret', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信公众号 access_token(带缓存)
|
||||||
|
*/
|
||||||
|
private function getAccessToken()
|
||||||
|
{
|
||||||
|
$cacheKey = 'wechat_access_token';
|
||||||
|
$token = Cache::get($cacheKey);
|
||||||
|
if ($token) {
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appid}&secret={$this->appsecret}";
|
||||||
|
$res = json_decode(myGet($url), true);
|
||||||
|
|
||||||
|
if (!isset($res['access_token'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::set($cacheKey, $res['access_token'], $res['expires_in'] - 200);
|
||||||
|
return $res['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取公众号图文素材列表
|
||||||
|
*
|
||||||
|
* @param int offset 起始位置,默认0
|
||||||
|
* @param int count 获取数量,默认10,最大20
|
||||||
|
*/
|
||||||
|
public function getArticleList()
|
||||||
|
{
|
||||||
|
$data = $this->request->param();
|
||||||
|
$offset = isset($data['offset']) ? intval($data['offset']) : 0;
|
||||||
|
$count = isset($data['count']) ? intval($data['count']) : 10;
|
||||||
|
|
||||||
|
if ($count > 20) {
|
||||||
|
$count = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessToken = $this->getAccessToken();
|
||||||
|
if (!$accessToken) {
|
||||||
|
return jsonError('获取access_token失败,请检查appid和appsecret配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token={$accessToken}";
|
||||||
|
$postData = [
|
||||||
|
'type' => 'news',
|
||||||
|
'offset' => $offset,
|
||||||
|
'count' => $count,
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = json_decode(myPost1($url, $postData), true);
|
||||||
|
|
||||||
|
if (isset($res['errcode']) && $res['errcode'] != 0) {
|
||||||
|
return jsonError('微信接口错误: ' . ($res['errmsg'] ?? '未知错误'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = [];
|
||||||
|
if (isset($res['item'])) {
|
||||||
|
foreach ($res['item'] as $item) {
|
||||||
|
$articles = [];
|
||||||
|
$newsItems = isset($item['content']['news_item']) ? $item['content']['news_item'] : [];
|
||||||
|
foreach ($newsItems as $news) {
|
||||||
|
$articles[] = [
|
||||||
|
'title' => $news['title'] ?? '',
|
||||||
|
'thumb_media_id' => $news['thumb_media_id'] ?? '',
|
||||||
|
'thumb_url' => $news['thumb_url'] ?? '',
|
||||||
|
'author' => $news['author'] ?? '',
|
||||||
|
'digest' => $news['digest'] ?? '',
|
||||||
|
'content_source_url' => $news['content_source_url'] ?? '',
|
||||||
|
'url' => $news['url'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$list[] = [
|
||||||
|
'media_id' => $item['media_id'],
|
||||||
|
'update_time' => isset($item['content']['update_time']) ? date('Y-m-d H:i:s', $item['content']['update_time']) : '',
|
||||||
|
'articles' => $articles,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'total_count' => $res['total_count'] ?? 0,
|
||||||
|
'item_count' => $res['item_count'] ?? 0,
|
||||||
|
'list' => $list,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取公众号图文素材详情
|
||||||
|
*
|
||||||
|
* @param string media_id 素材的media_id
|
||||||
|
*/
|
||||||
|
public function getArticleDetail()
|
||||||
|
{
|
||||||
|
$data = $this->request->param();
|
||||||
|
if (!isset($data['media_id']) || $data['media_id'] == '') {
|
||||||
|
return jsonError('media_id不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessToken = $this->getAccessToken();
|
||||||
|
if (!$accessToken) {
|
||||||
|
return jsonError('获取access_token失败,请检查appid和appsecret配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token={$accessToken}";
|
||||||
|
$postData = [
|
||||||
|
'media_id' => $data['media_id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = json_decode(myPost1($url, $postData), true);
|
||||||
|
|
||||||
|
if (isset($res['errcode']) && $res['errcode'] != 0) {
|
||||||
|
return jsonError('微信接口错误: ' . ($res['errmsg'] ?? '未知错误'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$articles = [];
|
||||||
|
$newsItems = isset($res['news_item']) ? $res['news_item'] : [];
|
||||||
|
foreach ($newsItems as $news) {
|
||||||
|
$articles[] = [
|
||||||
|
'title' => $news['title'] ?? '',
|
||||||
|
'thumb_media_id' => $news['thumb_media_id'] ?? '',
|
||||||
|
'thumb_url' => $news['thumb_url'] ?? '',
|
||||||
|
'author' => $news['author'] ?? '',
|
||||||
|
'digest' => $news['digest'] ?? '',
|
||||||
|
'content' => $news['content'] ?? '',
|
||||||
|
'content_source_url' => $news['content_source_url'] ?? '',
|
||||||
|
'url' => $news['url'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'media_id' => $data['media_id'],
|
||||||
|
'articles' => $articles,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已发布的文章列表(发布接口)
|
||||||
|
*
|
||||||
|
* @param int offset 起始位置,默认0
|
||||||
|
* @param int count 获取数量,默认10,最大20
|
||||||
|
* @param int no_content 1=不返回content字段,0=返回,默认0
|
||||||
|
*/
|
||||||
|
public function getPublishedArticles()
|
||||||
|
{
|
||||||
|
$data = $this->request->param();
|
||||||
|
$offset = isset($data['offset']) ? intval($data['offset']) : 0;
|
||||||
|
$count = isset($data['count']) ? intval($data['count']) : 10;
|
||||||
|
$noContent = isset($data['no_content']) ? intval($data['no_content']) : 0;
|
||||||
|
|
||||||
|
if ($count > 20) {
|
||||||
|
$count = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessToken = $this->getAccessToken();
|
||||||
|
if (!$accessToken) {
|
||||||
|
return jsonError('获取access_token失败,请检查appid和appsecret配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "https://api.weixin.qq.com/cgi-bin/freepublish/batchget?access_token={$accessToken}";
|
||||||
|
$postData = [
|
||||||
|
'offset' => $offset,
|
||||||
|
'count' => $count,
|
||||||
|
'no_content' => $noContent,
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = json_decode(myPost1($url, $postData), true);
|
||||||
|
|
||||||
|
if (isset($res['errcode']) && $res['errcode'] != 0) {
|
||||||
|
return jsonError('微信接口错误: ' . ($res['errmsg'] ?? '未知错误'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = [];
|
||||||
|
if (isset($res['item'])) {
|
||||||
|
foreach ($res['item'] as $item) {
|
||||||
|
$articles = [];
|
||||||
|
$newsItems = isset($item['content']['news_item']) ? $item['content']['news_item'] : [];
|
||||||
|
foreach ($newsItems as $news) {
|
||||||
|
$article = [
|
||||||
|
'title' => $news['title'] ?? '',
|
||||||
|
'author' => $news['author'] ?? '',
|
||||||
|
'digest' => $news['digest'] ?? '',
|
||||||
|
'thumb_url' => $news['thumb_url'] ?? '',
|
||||||
|
'content_source_url' => $news['content_source_url'] ?? '',
|
||||||
|
'url' => $news['url'] ?? '',
|
||||||
|
'is_deleted' => $news['is_deleted'] ?? false,
|
||||||
|
];
|
||||||
|
if (!$noContent) {
|
||||||
|
$article['content'] = $news['content'] ?? '';
|
||||||
|
}
|
||||||
|
$articles[] = $article;
|
||||||
|
}
|
||||||
|
$list[] = [
|
||||||
|
'article_id' => $item['article_id'] ?? '',
|
||||||
|
'update_time' => isset($item['content']['update_time']) ? date('Y-m-d H:i:s', $item['content']['update_time']) : '',
|
||||||
|
'articles' => $articles,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'total_count' => $res['total_count'] ?? 0,
|
||||||
|
'item_count' => $res['item_count'] ?? 0,
|
||||||
|
'list' => $list,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已发布的文章详情
|
||||||
|
*
|
||||||
|
* @param string article_id 发布文章的article_id
|
||||||
|
*/
|
||||||
|
public function getPublishedArticleDetail()
|
||||||
|
{
|
||||||
|
$data = $this->request->param();
|
||||||
|
if (!isset($data['article_id']) || $data['article_id'] == '') {
|
||||||
|
return jsonError('article_id不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$accessToken = $this->getAccessToken();
|
||||||
|
if (!$accessToken) {
|
||||||
|
return jsonError('获取access_token失败,请检查appid和appsecret配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = "https://api.weixin.qq.com/cgi-bin/freepublish/getarticle?access_token={$accessToken}";
|
||||||
|
$postData = [
|
||||||
|
'article_id' => $data['article_id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = json_decode(myPost1($url, $postData), true);
|
||||||
|
|
||||||
|
if (isset($res['errcode']) && $res['errcode'] != 0) {
|
||||||
|
return jsonError('微信接口错误: ' . ($res['errmsg'] ?? '未知错误'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$articles = [];
|
||||||
|
$newsItems = isset($res['news_item']) ? $res['news_item'] : [];
|
||||||
|
foreach ($newsItems as $news) {
|
||||||
|
$articles[] = [
|
||||||
|
'title' => $news['title'] ?? '',
|
||||||
|
'author' => $news['author'] ?? '',
|
||||||
|
'digest' => $news['digest'] ?? '',
|
||||||
|
'thumb_url' => $news['thumb_url'] ?? '',
|
||||||
|
'content' => $news['content'] ?? '',
|
||||||
|
'content_source_url' => $news['content_source_url'] ?? '',
|
||||||
|
'url' => $news['url'] ?? '',
|
||||||
|
'is_deleted' => $news['is_deleted'] ?? false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSuccess([
|
||||||
|
'article_id' => $data['article_id'],
|
||||||
|
'articles' => $articles,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
553
application/api/controller/Wechatprogram.php
Normal file
553
application/api/controller/Wechatprogram.php
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
use app\api\controller\Base;
|
||||||
|
use think\Db;
|
||||||
|
/**
|
||||||
|
* @title 微信小程序相关方法
|
||||||
|
*/
|
||||||
|
class Wechatprogram extends Base
|
||||||
|
{
|
||||||
|
|
||||||
|
private $sAppID = 'wxe07868a09209e18b';
|
||||||
|
private $sAppSecret = '77bebfbae54b78bf0c79eb3ddd05be87';
|
||||||
|
private $sCode2SessionUrl = 'https://api.weixin.qq.com/sns/jscode2session';//登录凭证验证
|
||||||
|
|
||||||
|
public function __construct(\think\Request $request = null) {
|
||||||
|
parent::__construct($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取登录微信的openid和unionid
|
||||||
|
*/
|
||||||
|
public function getLogin(){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
//获取微信登录code
|
||||||
|
$sLoginCode = empty($aParam['login_code']) ? '' : $aParam['login_code'];
|
||||||
|
if(empty($sLoginCode)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the login code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//调用接口
|
||||||
|
//url拼接
|
||||||
|
$sCode2SessionUrl = $this->sCode2SessionUrl;
|
||||||
|
$sCode2SessionUrl .= '?appid='.$this->sAppID.'&secret='.$this->sAppSecret.'&js_code='.$sLoginCode.'&grant_type=GRANT_TYPE';
|
||||||
|
$aResult = json_decode(myGet($sCode2SessionUrl),true);
|
||||||
|
if(isset($aResult['errcode'])){
|
||||||
|
$sMsg = empty($aResult['errmsg']) ? 'Interface request error:'.$aResult['errcode'] : $aResult['errmsg'];
|
||||||
|
return json_encode(['status' => 3,'msg' => $sMsg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取微信登录openid
|
||||||
|
$sOpenId= empty($aResult['openid']) ? '' : trim($aResult['openid']);
|
||||||
|
if(empty($sOpenId)){
|
||||||
|
return json_encode(['status' => 4,'msg' => 'User login information not obtained']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//根绝openid获取用户信息
|
||||||
|
$aData = [];
|
||||||
|
$aUser = json_decode($this->getUser(['openid' => $sOpenId,'is_select_role' => 2]),true);
|
||||||
|
$aUser = empty($aUser['data']) ? [] : $aUser['data'];
|
||||||
|
if(!empty($aUser)){
|
||||||
|
$aData['user'] = $aUser;
|
||||||
|
}
|
||||||
|
$aData['wechat'] = $aResult;
|
||||||
|
return json_encode(['status' => 1,'msg' => 'success','data' => $aData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据OPENID查询用户信息
|
||||||
|
*/
|
||||||
|
public function getUser($aParam = []){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
//获取用户ID
|
||||||
|
$iUserId= empty($aParam['user_id']) ? '' : $aParam['user_id'];
|
||||||
|
|
||||||
|
//获取微信登录openid
|
||||||
|
$sOpenId= empty($aParam['openid']) ? '' : $aParam['openid'];
|
||||||
|
if(empty($sOpenId) && empty($iUserId)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the login openid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询用户是否存在
|
||||||
|
$aWhere = ['state' => 0];
|
||||||
|
if(!empty($iUserId)){
|
||||||
|
$aWhere['user_id'] = $iUserId;
|
||||||
|
}
|
||||||
|
if(!empty($sOpenId)){
|
||||||
|
$aWhere['openid'] = $sOpenId;
|
||||||
|
}
|
||||||
|
$aUser = Db::name('user')->field('user_id,account,openid,icon,email,type')->where($aWhere)->find();
|
||||||
|
if(empty($aUser)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'No user information found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//判断是否查询用户角色
|
||||||
|
$iIsSelectRole = empty($aParam['is_select_role']) ? 1 : $aParam['is_select_role'];
|
||||||
|
if($iIsSelectRole == 1){
|
||||||
|
$aUser['roles'] = $this->getUserRoles($aUser);
|
||||||
|
}
|
||||||
|
return json_encode(['status' => 1,'msg' => 'success','data' => $aUser]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名密码绑定openid
|
||||||
|
*/
|
||||||
|
public function bindAccount($aParam = []){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
//账号名
|
||||||
|
$sAccount= empty($aParam['account']) ? '' : trim($aParam['account']);
|
||||||
|
if(empty($sAccount)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter your account']);
|
||||||
|
}
|
||||||
|
//密码
|
||||||
|
$sPassword= empty($aParam['password']) ? '' : trim($aParam['password']);
|
||||||
|
if(empty($sPassword)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the password']);
|
||||||
|
}
|
||||||
|
//openid
|
||||||
|
$sOpenId= empty($aParam['openid']) ? '' : trim($aParam['openid']);
|
||||||
|
// //unionid
|
||||||
|
// $sUnionId= empty($aParam['unionid']) ? '' : trim($aParam['unionid']);
|
||||||
|
if(empty($sOpenId)){// && empty($sUnionId)
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the login openid/unionid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询用户是否存在
|
||||||
|
$aWhere = ['account|email' => $sAccount,'state' => 0];
|
||||||
|
$aUser = Db::name('user')->field('user_id,account,password,openid')->where($aWhere)->find();
|
||||||
|
if(empty($aUser)){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'Login account does not exist, please confirm']);
|
||||||
|
}
|
||||||
|
//验证密码是否一致
|
||||||
|
$sPassword = md5($sPassword);
|
||||||
|
if($aUser['password'] != $sPassword){
|
||||||
|
return json_encode(['status' => 4,'msg' => 'Login password input error, please confirm']);
|
||||||
|
}
|
||||||
|
//验证是否绑定账号
|
||||||
|
if(!empty($aUser['openid'])){// || !empty($aUser['unionid'])
|
||||||
|
return json_encode(['status' => 5,'msg' => 'Login account already linked to WeChat account']);
|
||||||
|
}
|
||||||
|
//验证openid是否被绑定
|
||||||
|
$iUserId = empty($aUser['user_id']) ? 0 : $aUser['user_id'];
|
||||||
|
$aWhere = ['state' => 0,'user_id' => ['<>',$iUserId]];
|
||||||
|
// if(!empty($sUnionId)){
|
||||||
|
// $aWhere['unionid'] = $sUnionId;
|
||||||
|
// }
|
||||||
|
if(!empty($sOpenId)){
|
||||||
|
$aWhere['openid'] = $sOpenId;
|
||||||
|
}
|
||||||
|
$aUserOpenId = Db::name('user')->field('user_id')->where($aWhere)->find();
|
||||||
|
if(!empty($aUserOpenId)){
|
||||||
|
return json_encode(['status' => 6,'msg' => 'This WeChat account has been bound']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//更新
|
||||||
|
$aUpdate = [];
|
||||||
|
// if(!empty($sUnionId)){
|
||||||
|
// $aUpdate['unionid'] = $sUnionId;
|
||||||
|
// }
|
||||||
|
if(!empty($sOpenId)){
|
||||||
|
$aUpdate['openid'] = $sOpenId;
|
||||||
|
}
|
||||||
|
if(empty($aUpdate)){
|
||||||
|
return json_encode(['status' => 7,'msg' => 'Update data to empty']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//执行操作
|
||||||
|
$aWhere = ['user_id' => $iUserId,'state' => 0];
|
||||||
|
$result = Db::name('user')->where($aWhere)->limit(1)->update($aUpdate);
|
||||||
|
if($result === false){
|
||||||
|
return json_encode(['status' => 8,'msg' => "Binding failed"]);
|
||||||
|
}
|
||||||
|
// $aUser['unionid'] = empty($aUpdate['unionid']) ? $aUser['unionid'] : $aUpdate['unionid'];
|
||||||
|
$aUser['openid'] = empty($aUpdate['openid']) ? $aUser['openid'] : $aUpdate['openid'];
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Binding successful','data' => $aUser]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名密码解绑openid
|
||||||
|
*/
|
||||||
|
public function unbindAccount(){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
//账号名
|
||||||
|
$sAccount= empty($aParam['account']) ? '' : trim($aParam['account']);
|
||||||
|
if(empty($sAccount)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter your account']);
|
||||||
|
}
|
||||||
|
//密码
|
||||||
|
$sPassword= empty($aParam['password']) ? '' : trim($aParam['password']);
|
||||||
|
if(empty($sPassword)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the password']);
|
||||||
|
}
|
||||||
|
//openid
|
||||||
|
$sOpenId= empty($aParam['openid']) ? '' : trim($aParam['openid']);
|
||||||
|
//unionid
|
||||||
|
// $sUnionId= empty($aParam['unionid']) ? '' : trim($aParam['unionid']);
|
||||||
|
if(empty($sOpenId)){// && empty($sUnionId)
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the login openid/unionid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询用户是否存在
|
||||||
|
$aWhere = ['account|email' => $sAccount,'state' => 0];
|
||||||
|
$aUser = Db::name('user')->field('user_id,account,password,openid')->where($aWhere)->find();
|
||||||
|
if(empty($aUser)){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'Login account does not exist, please confirm']);
|
||||||
|
}
|
||||||
|
//验证密码是否一致
|
||||||
|
$sPassword = md5($sPassword);
|
||||||
|
if($aUser['password'] != $sPassword){
|
||||||
|
return json_encode(['status' => 4,'msg' => 'Login password input error, please confirm']);
|
||||||
|
}
|
||||||
|
//验证是否绑定账号
|
||||||
|
if(empty($aUser['openid'])){// && empty($aUser['unionid'])
|
||||||
|
return json_encode(['status' => 5,'msg' => 'This account is not bound to any mini program, there is no need to unbind it']);
|
||||||
|
}
|
||||||
|
//验证unionid是否相等
|
||||||
|
// if(!empty($sUnionId) && !empty($aUser['unionid']) & $sUnionId != $aUser['unionid']){
|
||||||
|
// return json_encode(['status' => 6,'msg' => 'Unbind account not bound']);
|
||||||
|
// }
|
||||||
|
//验证openid是否相等
|
||||||
|
if(!empty($sOpenId) && !empty($aUser['openid']) & $sOpenId != $aUser['openid']){
|
||||||
|
return json_encode(['status' => 7,'msg' => 'Unbind account not bound']);
|
||||||
|
}
|
||||||
|
$iUserId = empty($aUser['user_id']) ? 0 : $aUser['user_id'];
|
||||||
|
|
||||||
|
//执行操作
|
||||||
|
$aWhere = ['user_id' => $iUserId,'state' => 0];
|
||||||
|
$aUpdate = ['openid' => ''];//,'unionid' => ''
|
||||||
|
$result = Db::name('user')->where($aWhere)->limit(1)->update($aUpdate);
|
||||||
|
if($result === false){
|
||||||
|
return json_encode(['status' => 8,'msg' => "Unbinding failed"]);
|
||||||
|
}
|
||||||
|
$aUpdate['user_id'] = $iUserId;
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Unbound successfully','data' => $aUpdate]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户账号注册
|
||||||
|
*/
|
||||||
|
public function registerAccount(){
|
||||||
|
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
//邮箱
|
||||||
|
$sEmail = empty($aParam['email']) ? '' : trim($aParam['email']);
|
||||||
|
if(empty($sEmail)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter your email']);
|
||||||
|
}
|
||||||
|
//密码
|
||||||
|
$sPassword= empty($aParam['password']) ? '' : trim($aParam['password']);
|
||||||
|
if(empty($sPassword)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the password']);
|
||||||
|
}
|
||||||
|
//openid
|
||||||
|
$sOpenId= empty($aParam['openid']) ? '' : trim($aParam['openid']);
|
||||||
|
if(empty($sOpenId)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter the login openid/unionid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询用户是否存在
|
||||||
|
$aWhere = ['account|email' => $sEmail,'state' => 0];
|
||||||
|
$aUser = Db::name('user')->field('user_id')->where($aWhere)->find();
|
||||||
|
if(!empty($aUser)){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'The registered account already exists, please confirm']);
|
||||||
|
}
|
||||||
|
//验证OPENID是否绑定
|
||||||
|
$aWhere = ['openid' => $sOpenId,'state' => 0];
|
||||||
|
$aUser = Db::name('user')->field('user_id')->where($aWhere)->find();
|
||||||
|
if(!empty($aUser)){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'WeChat account has been bound']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//数据插入
|
||||||
|
Db::startTrans();
|
||||||
|
//用户主表
|
||||||
|
$aInsert = ['account' => $sEmail,'email' => $sEmail,'password' => md5($sPassword),'ctime' => time(),'openid' => $sOpenId];
|
||||||
|
$iId = Db::name('user')->insertGetId($aInsert);
|
||||||
|
if(empty($iId)){
|
||||||
|
return json_encode(['status' => 4,'msg' => 'Registration failed']);
|
||||||
|
}
|
||||||
|
//用户附属表
|
||||||
|
$aReviewInsert = ['reviewer_id' => $iId,'test_from' => 'wechat_register'];
|
||||||
|
$iInfoId = Db::name('user_reviewer_info')->insertGetId($aReviewInsert);
|
||||||
|
if(empty($iInfoId)){
|
||||||
|
return json_encode(['status' => 5,'msg' => 'Registration failed']);
|
||||||
|
}
|
||||||
|
Db::commit();
|
||||||
|
$aInsert['user_id'] = $iId;
|
||||||
|
return json_encode(['status' => 1,'msg' => 'registered successfully','data' => $aInsert]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取用户身份
|
||||||
|
*/
|
||||||
|
private function getUserRoles($aUser = []){
|
||||||
|
if(empty($aUser)){
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取账号名
|
||||||
|
$sAccount = empty($aUser['account']) ? '' : trim($aUser['account']);
|
||||||
|
if($aUser['type'] == 2) {
|
||||||
|
$aRoles = ['editor'];
|
||||||
|
if($sAccount=="liuna" || $sAccount=="zhuwenjing"){
|
||||||
|
array_push($aRoles, 'superadmin');
|
||||||
|
}
|
||||||
|
return $aRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
$aRoles = ['author'];
|
||||||
|
//查询是否是审稿人
|
||||||
|
$iUserId = empty($aUser['user_id']) ? 0 : $aUser['user_id'];
|
||||||
|
$aWhere = ['reviewer_id' => $iUserId,'state' => 0];
|
||||||
|
$aUserInfo = Db::name('reviewer_to_journal')->field('rtj_id')->where($aWhere)->find();
|
||||||
|
if(!empty($aUserInfo)) {
|
||||||
|
array_push($aRoles,'reviewer');
|
||||||
|
}
|
||||||
|
|
||||||
|
//青年编委
|
||||||
|
$aWhere = ['user_id' => $iUserId,'state' => 0];
|
||||||
|
$aUserInfo = Db::name('user_to_yboard')->field('user_id')->where($aWhere)->find();
|
||||||
|
if(!empty($aUserInfo)) {
|
||||||
|
array_push($aRoles,'yboard');
|
||||||
|
}
|
||||||
|
//主编与期刊
|
||||||
|
$aWhere = ['user_id' => $iUserId,'state' => 0];
|
||||||
|
$aUserInfo = Db::name('chief_to_journal')->field('user_id')->where($aWhere)->find();
|
||||||
|
if(!empty($aUserInfo)) {
|
||||||
|
array_push($aRoles,'chief');
|
||||||
|
}
|
||||||
|
//期刊主编类型
|
||||||
|
$aWhere = ['user_id' => $iUserId,'state' => 0];
|
||||||
|
$aUserInfo = Db::name('board_to_journal')->field('user_id,type')->where($aWhere)->find();
|
||||||
|
if(!empty($aUserInfo)) {
|
||||||
|
array_push($aRoles,'board');
|
||||||
|
$iType = isset($aUserInfo['type']) ? $aUserInfo['type'] : '-1';
|
||||||
|
if($iType == 0){
|
||||||
|
array_push($aRoles,'chief_editor');
|
||||||
|
}
|
||||||
|
if($iType == 1){
|
||||||
|
array_push($aRoles,'deputy_editor');
|
||||||
|
}
|
||||||
|
if($iType == 2){
|
||||||
|
array_push($aRoles,'editor_board');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//期刊主编类型
|
||||||
|
$aWhere = ['user_id' => $iUserId,'uts_state' => 0];
|
||||||
|
$aUserInfo = Db::name('user_to_special')->field('user_id')->where($aWhere)->find();
|
||||||
|
if (!empty($aUserInfo)) {
|
||||||
|
array_push($aRoles,'special');
|
||||||
|
}
|
||||||
|
return $aRoles;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取我的稿件
|
||||||
|
*/
|
||||||
|
public function getManuscript($aParam = []){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
//获取用户ID
|
||||||
|
$iUserId= empty($aParam['user_id']) ? '' : $aParam['user_id'];
|
||||||
|
//获取状态
|
||||||
|
$iState = isset($aParam['state']) ? $aParam['state'] : -2;
|
||||||
|
//获取微信登录openid
|
||||||
|
$sOpenId= empty($aParam['openid']) ? '' : $aParam['openid'];
|
||||||
|
if(empty($sOpenId) && empty($iUserId)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter your login account']);
|
||||||
|
}
|
||||||
|
//标题
|
||||||
|
$sTitle = empty($aParam['title']) ? '': $aParam['title'];
|
||||||
|
//获取用户信息
|
||||||
|
$aParam['is_select_role'] = 2;
|
||||||
|
$aUser = json_decode($this->getUser($aParam),true);
|
||||||
|
$aUser = empty($aUser['data']) ? [] : $aUser['data'];
|
||||||
|
if(empty($aUser)){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'No user information found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取分页相关参数
|
||||||
|
$iSize = empty($aParam['size']) ? 15 : $aParam['size'];//每页显示条数
|
||||||
|
$iPage = empty($aParam['page']) ? 1 : $aParam['page'];// 当前页码
|
||||||
|
|
||||||
|
$iUserId = empty($aUser['user_id']) ? 0 : $aUser['user_id'];
|
||||||
|
//获取数量
|
||||||
|
$aWhere = ['user_id' => $iUserId,'accept_sn' => ['not like','Draft%']];
|
||||||
|
$aWhere['state'] = ['<>',-1];
|
||||||
|
if($iState != -2 && $iState != -1){
|
||||||
|
$aWhere['state'] = $iState;
|
||||||
|
}
|
||||||
|
if($iState == -2){
|
||||||
|
$aWhere['state'] = ['<>',-1];
|
||||||
|
}
|
||||||
|
if(!empty($sTitle)){
|
||||||
|
$aWhere['title'] =['like','%'.trim($sTitle).'%'];
|
||||||
|
}
|
||||||
|
$iCount = Db::name('article')->where($aWhere)->count();
|
||||||
|
if(empty($iCount)){
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Article not found','data' => ['total' => 0,'lists' => []]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//判断页数是否超过最大分页限制
|
||||||
|
$iPageNum = ceil($iCount/$iSize);
|
||||||
|
if($iPage > $iPageNum){
|
||||||
|
return json_encode(['status' => 1,'msg' => 'The number of pages has exceeded the limit, maximum page number:'.$iPageNum,'data' => ['total' => $iCount,'lists' => []]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询详细数据
|
||||||
|
$sField = 'article_id,journal_id,accept_sn,title,type,abstrart,ctime,state';
|
||||||
|
$sOrder = 'article_id desc';
|
||||||
|
$aArticle = Db::name('article')
|
||||||
|
->field($sField)
|
||||||
|
->where($aWhere)
|
||||||
|
->page($iPage, $iSize)
|
||||||
|
->order($sOrder)
|
||||||
|
->select();
|
||||||
|
if(empty($aArticle)){
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Data is empty','data' => ['total' => 0,'lists' => []]]);
|
||||||
|
}
|
||||||
|
//获取期刊
|
||||||
|
$aJournalId = array_unique(array_column($aArticle, 'journal_id'));
|
||||||
|
$aWhere = ['journal_id' => ['in',$aJournalId],'state' => 0];
|
||||||
|
$aJournal = DB::name('journal')->where($aWhere)->column('journal_id,title');
|
||||||
|
//数据处理
|
||||||
|
foreach ($aArticle as $key => $value) {
|
||||||
|
$aArticle[$key]['type_name'] = translateType($value['type']);
|
||||||
|
$aArticle[$key]['journal_title'] = empty($aJournal[$value['journal_id']]) ? '' : $aJournal[$value['journal_id']];
|
||||||
|
$aArticle[$key]['ctime'] = empty($value['ctime']) ? '' : date('Y-m-d',$value['ctime']);
|
||||||
|
}
|
||||||
|
return json_encode(['status' => 1,'msg' => 'success','data' => ['total' => $iCount,'lists' => $aArticle]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的消息
|
||||||
|
*/
|
||||||
|
public function getMessagesLists($aParam = []){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
|
||||||
|
//获取用户ID
|
||||||
|
$iUserId= empty($aParam['user_id']) ? '' : $aParam['user_id'];
|
||||||
|
//获取微信登录openid
|
||||||
|
$sOpenId= empty($aParam['openid']) ? '' : $aParam['openid'];
|
||||||
|
if(empty($sOpenId) && empty($iUserId)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter your login account']);
|
||||||
|
}
|
||||||
|
//标题
|
||||||
|
$sTitle = empty($aParam['title']) ? '': $aParam['title'];
|
||||||
|
|
||||||
|
//状态
|
||||||
|
$iIsRead = empty($aParam['is_read']) ? -1 : $aParam['is_read'];
|
||||||
|
|
||||||
|
//获取用户信息
|
||||||
|
$aParam['is_select_role'] = 2;
|
||||||
|
$aUser = json_decode($this->getUser($aParam),true);
|
||||||
|
$aUser = empty($aUser['data']) ? [] : $aUser['data'];
|
||||||
|
if(empty($aUser)){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'No user information found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取分页相关参数
|
||||||
|
$iSize = empty($aParam['size']) ? 15 : $aParam['size'];//每页显示条数
|
||||||
|
$iPage = empty($aParam['page']) ? 1 : $aParam['page'];// 当前页码
|
||||||
|
|
||||||
|
$iUserId = empty($aUser['user_id']) ? 0 : $aUser['user_id'];
|
||||||
|
//获取数量
|
||||||
|
$aWhere = ['user_id' => $iUserId];
|
||||||
|
if(!empty($sTitle)){
|
||||||
|
$aWhere['title'] =['like','%'.trim($sTitle).'%'];
|
||||||
|
}
|
||||||
|
if(in_array($iIsRead, [1,2])){
|
||||||
|
$aWhere['is_read'] = $iIsRead;
|
||||||
|
}
|
||||||
|
$iCount = Db::name('messages')->where($aWhere)->count();
|
||||||
|
if(empty($iCount)){
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Message is empty','data' => ['total' => 0,'lists' => []]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//判断页数是否超过最大分页限制
|
||||||
|
$iPageNum = ceil($iCount/$iSize);
|
||||||
|
if($iPage > $iPageNum){
|
||||||
|
return json_encode(['status' => 1,'msg' => 'The number of pages has exceeded the limit, maximum page number:'.$iPageNum,'data' => ['total' => $iCount,'lists' => []]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//查询详细数据
|
||||||
|
$sField = 'message_id,article_id,type,title,content,is_read,create_time';
|
||||||
|
$sOrder = 'create_time desc';
|
||||||
|
$aMessages = Db::name('messages')
|
||||||
|
->field($sField)
|
||||||
|
->where($aWhere)
|
||||||
|
->page($iPage, $iSize)
|
||||||
|
->order($sOrder)
|
||||||
|
->select();
|
||||||
|
if(empty($aMessages)){
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Data is empty','data' => ['total' => 0,'lists' => []]]);
|
||||||
|
}
|
||||||
|
//获取期刊
|
||||||
|
$aJournalId = array_unique(array_column($aMessages, 'journal_id'));
|
||||||
|
$aWhere = ['journal_id' => ['in',$aJournalId],'state' => 0];
|
||||||
|
$aJournal = DB::name('journal')->where($aWhere)->column('journal_id,title');
|
||||||
|
//数据处理
|
||||||
|
foreach ($aMessages as $key => $value) {
|
||||||
|
$aMessages[$key]['create_time'] = empty($value['create_time']) ? '' : date('Y-m-d H:i:s',$value['create_time']);
|
||||||
|
}
|
||||||
|
return json_encode(['status' => 1,'msg' => 'success','data' => ['total' => $iCount,'lists' => $aMessages]]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 更改消息状态
|
||||||
|
*/
|
||||||
|
public function markRead($aParam = []){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? $this->request->post() : $aParam;
|
||||||
|
//获取消息ID
|
||||||
|
$iMessageId= empty($aParam['message_id']) ? 0 : $aParam['message_id'];
|
||||||
|
if(empty($iMessageId)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please select a message']);
|
||||||
|
}
|
||||||
|
//获取用户ID
|
||||||
|
$iUserId= empty($aParam['user_id']) ? '' : $aParam['user_id'];
|
||||||
|
//获取微信登录openid
|
||||||
|
$sOpenId= empty($aParam['openid']) ? '' : $aParam['openid'];
|
||||||
|
if(empty($sOpenId) && empty($iUserId)){
|
||||||
|
return json_encode(['status' => 2,'msg' => 'Please enter your login account']);
|
||||||
|
}
|
||||||
|
//获取用户信息
|
||||||
|
$aParam['is_select_role'] = 2;
|
||||||
|
$aUser = json_decode($this->getUser($aParam),true);
|
||||||
|
$aUser = empty($aUser['data']) ? [] : $aUser['data'];
|
||||||
|
if(empty($aUser)){
|
||||||
|
return json_encode(['status' => 3,'msg' => 'No user information found']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$iUserId = empty($aUser['user_id']) ? 0 : $aUser['user_id'];
|
||||||
|
//获取未读数据
|
||||||
|
$aWhere = ['user_id' => $iUserId,'is_read' => 2];
|
||||||
|
if($iMessageId != -1){
|
||||||
|
$aWhere['message_id'] = ['in',$iMessageId];
|
||||||
|
}
|
||||||
|
$aMessagesId = Db::name('messages')->where($aWhere)->column('message_id');
|
||||||
|
if(empty($aMessagesId)){
|
||||||
|
return json_encode(['status' => 4,'msg' => 'Message is empty']);
|
||||||
|
}
|
||||||
|
//更新为已读
|
||||||
|
$aWhere = ['is_read' => 2,'message_id' => ['in',$aMessagesId]];
|
||||||
|
$aUpdate = ['is_read' => 1,'update_time' => time(),'update_user_id' => $iUserId];
|
||||||
|
$result = Db::name('messages')->where($aWhere)->limit(count($aMessagesId))->update($aUpdate);
|
||||||
|
if($result === false){
|
||||||
|
return json_encode(['status' => 5,'msg' => 'Marking failed']);
|
||||||
|
}
|
||||||
|
return json_encode(['status' => 1,'msg' => 'Marking successful']);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
application/api/job/FetchExperts.php
Normal file
51
application/api/job/FetchExperts.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\job;
|
||||||
|
|
||||||
|
use think\queue\Job;
|
||||||
|
use app\common\ExpertFinderService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专家抓取队列任务
|
||||||
|
* 注意:此任务推送到队列名 "FetchExperts",必须单独启动 worker 才会执行:
|
||||||
|
* php think queue:listen --queue FetchExperts
|
||||||
|
* 若只运行 queue:listen 不指定队列,默认只消费 "mail",本任务不会被执行。
|
||||||
|
*/
|
||||||
|
class FetchExperts
|
||||||
|
{
|
||||||
|
public function fire(Job $job, $data)
|
||||||
|
{
|
||||||
|
$field = isset($data['field']) ? $data['field'] : '';
|
||||||
|
// $attempts = $job->attempts();
|
||||||
|
//
|
||||||
|
$service = new ExpertFinderService();
|
||||||
|
// $service->log('[FetchExperts] start field=' . $field . ' attempts=' . $attempts);
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
$result = $service->doFetchForField(
|
||||||
|
$field,
|
||||||
|
isset($data['source']) ? $data['source'] : 'pubmed',
|
||||||
|
isset($data['per_page']) ? intval($data['per_page']) : 100,
|
||||||
|
isset($data['min_year']) ? $data['min_year'] : null
|
||||||
|
);
|
||||||
|
// $service->log('[FetchExperts] completed field=' . $field . ' result=' . json_encode($result));
|
||||||
|
// } catch (\Throwable $e) {
|
||||||
|
// $service->log(
|
||||||
|
// '[FetchExperts] failed field=' . $field .
|
||||||
|
// ' msg=' . $e->getMessage() .
|
||||||
|
// ' file=' . $e->getFile() .
|
||||||
|
// ' line=' . $e->getLine()
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// if ($attempts >= 3) {
|
||||||
|
// $job->delete();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $job->release(60);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
$job->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
application/api/job/PromotionSend.php
Normal file
35
application/api/job/PromotionSend.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\job;
|
||||||
|
|
||||||
|
use think\queue\Job;
|
||||||
|
use app\common\PromotionService;
|
||||||
|
|
||||||
|
class PromotionSend
|
||||||
|
{
|
||||||
|
public function fire(Job $job, $data)
|
||||||
|
{
|
||||||
|
$taskId = intval(isset($data['task_id']) ? $data['task_id'] : 0);
|
||||||
|
$service = new PromotionService();
|
||||||
|
|
||||||
|
if (!$taskId) {
|
||||||
|
// $service->log('[PromotionSend] missing task_id, job deleted');
|
||||||
|
$job->delete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// try {
|
||||||
|
$result = $service->processNextEmail($taskId);
|
||||||
|
// $service->log('[PromotionSend] task=' . $taskId . ' result=' . json_encode($result));
|
||||||
|
|
||||||
|
// if (!empty($result['done'])) {
|
||||||
|
// $reason = isset($result['reason']) ? $result['reason'] : '';
|
||||||
|
// $service->log('[PromotionSend] task=' . $taskId . ' finished, reason=' . $reason);
|
||||||
|
// }
|
||||||
|
// } catch (\Exception $e) {
|
||||||
|
// $service->log('[PromotionSend] task=' . $taskId . ' exception=' . $e->getMessage());
|
||||||
|
// }
|
||||||
|
|
||||||
|
$job->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
620
application/common/ExpertFinderService.php
Normal file
620
application/common/ExpertFinderService.php
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class ExpertFinderService
|
||||||
|
{
|
||||||
|
private $httpClient;
|
||||||
|
private $ncbiBaseUrl = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/';
|
||||||
|
private $logFile;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->httpClient = new Client([
|
||||||
|
'timeout' => 180,
|
||||||
|
'connect_timeout' => 15,
|
||||||
|
'verify' => false,
|
||||||
|
]);
|
||||||
|
$this->logFile = ROOT_PATH . 'runtime' . DS . 'expert_finder.log';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function doFetchForField($field, $source = 'pubmed', $perPage = 100, $minYear = null)
|
||||||
|
{
|
||||||
|
if ($minYear === null) {
|
||||||
|
$minYear = date('Y') - 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fetchLog = $this->getFetchLog($field, $source);
|
||||||
|
$page = $fetchLog['last_page'] + 1;
|
||||||
|
|
||||||
|
if ($source === 'pmc') {
|
||||||
|
$result = $this->searchViaPMC($field, $perPage, $minYear, $page);
|
||||||
|
} else {
|
||||||
|
$result = $this->searchViaPubMed($field, $perPage, $minYear, $page);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($result['total'])){
|
||||||
|
return [
|
||||||
|
"has_more"=>"no"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$saveResult = $this->saveExperts($result['experts'], $field, $source);
|
||||||
|
|
||||||
|
$nextPage = $result['has_more'] ? $page : $fetchLog['last_page'];
|
||||||
|
$totalPages = $result['total_pages'] ?? $fetchLog['total_pages'];
|
||||||
|
$this->updateFetchLog($field, $source, $nextPage, $totalPages);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'keyword' => $field,
|
||||||
|
'page' => $page,
|
||||||
|
'experts_found' => $result['total'],
|
||||||
|
'saved_new' => $saveResult['inserted'],
|
||||||
|
'saved_exist' => $saveResult['existing'],
|
||||||
|
'list' => $result['experts'],
|
||||||
|
'field_enriched' => $saveResult['field_enriched'],
|
||||||
|
'has_more' => $result['has_more'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchExperts($keyword, $perPage, $minYear, $page, $source)
|
||||||
|
{
|
||||||
|
if ($source === 'pmc') {
|
||||||
|
return $this->searchViaPMC($keyword, $perPage, $minYear, $page);
|
||||||
|
}
|
||||||
|
return $this->searchViaPubMed($keyword, $perPage, $minYear, $page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveExperts($experts, $field, $source)
|
||||||
|
{
|
||||||
|
$inserted = 0;
|
||||||
|
$existing = 0;
|
||||||
|
$fieldEnrich = 0;
|
||||||
|
|
||||||
|
foreach ($experts as $expert) {
|
||||||
|
|
||||||
|
|
||||||
|
$email = strtolower(trim($expert['email']));
|
||||||
|
if (empty($email)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = Db::name('expert')->where('email', $email)->find();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
$existing++;
|
||||||
|
$fieldEnrich += $this->enrichExpertField($exists['expert_id'], $field);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insert = [
|
||||||
|
'name' => mb_substr($expert['name'], 0, 255),
|
||||||
|
'email' => mb_substr($email, 0, 128),
|
||||||
|
'affiliation' => mb_substr($expert['affiliation'], 0, 128),
|
||||||
|
'source' => mb_substr($source, 0, 128),
|
||||||
|
'ctime' => time(),
|
||||||
|
'ltime' => 0,
|
||||||
|
'state' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$expertId = Db::name('expert')->insertGetId($insert);
|
||||||
|
$this->enrichExpertField($expertId, $field);
|
||||||
|
if(isset($expert['papers'])&&is_array($expert['papers'])){
|
||||||
|
$this->savePaper($expertId, $expert['papers']);
|
||||||
|
}
|
||||||
|
$inserted++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$existing++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['inserted' => $inserted, 'existing' => $existing, 'field_enriched' => $fieldEnrich];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function savePaper($expertId, $papers)
|
||||||
|
{
|
||||||
|
foreach ($papers as $paper){
|
||||||
|
$check = Db::name('expert_paper')->where("expert_id",$expertId)->where('paper_article_id',$paper['article_id'])->find();
|
||||||
|
if($check){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$insert = [
|
||||||
|
'expert_id' => $expertId,
|
||||||
|
'paper_title' => isset($paper['title'])?mb_substr($paper['title'], 0, 255):"",
|
||||||
|
'paper_article_id' => $paper['article_id'] ?? 0,
|
||||||
|
'paper_journal' => isset($paper['journal'])?mb_substr($paper['journal'], 0, 128):"",
|
||||||
|
'ctime' => time(),
|
||||||
|
];
|
||||||
|
Db::name('expert_paper')->insert($insert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getFetchLog($field, $source)
|
||||||
|
{
|
||||||
|
$log = Db::name('expert_fetch')
|
||||||
|
->where('field', $field)
|
||||||
|
->where('source', $source)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$log) {
|
||||||
|
return ['last_page' => 0, 'total_pages' => 0, 'last_time' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateFetchLog($field, $source, $lastPage, $totalPages)
|
||||||
|
{
|
||||||
|
$exists = Db::name('expert_fetch')
|
||||||
|
->where('field', $field)
|
||||||
|
->where('source', $source)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
Db::name('expert_fetch')
|
||||||
|
->where('expert_fetch_id', $exists['expert_fetch_id'])
|
||||||
|
->update([
|
||||||
|
'last_page' => $lastPage,
|
||||||
|
'total_pages' => $totalPages,
|
||||||
|
'last_time' => time(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
Db::name('expert_fetch')->insert([
|
||||||
|
'field' => mb_substr($field, 0, 128),
|
||||||
|
'source' => mb_substr($source, 0, 128),
|
||||||
|
'last_page' => $lastPage,
|
||||||
|
'total_pages' => $totalPages,
|
||||||
|
'last_time' => time(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PubMed Search ====================
|
||||||
|
|
||||||
|
private function searchViaPubMed($keyword, $perPage, $minYear, $page = 1)
|
||||||
|
{
|
||||||
|
set_time_limit(600);
|
||||||
|
|
||||||
|
$searchResult = $this->esearch('pubmed', $keyword, $perPage, $minYear, $page);
|
||||||
|
$ids = $searchResult['ids'];
|
||||||
|
$totalArticles = $searchResult['total'];
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
return $this->buildPagedResult([], 0, 0, $totalArticles, $page, $perPage, 'pubmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$allAuthors = [];
|
||||||
|
$batches = array_chunk($ids, 50);
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
$xml = $this->efetchWithRetry('pubmed', $batch);
|
||||||
|
if ($xml) {
|
||||||
|
$authors = $this->parsePubMedXml($xml);
|
||||||
|
$allAuthors = array_merge($allAuthors, $authors);
|
||||||
|
}
|
||||||
|
usleep(400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$experts = $this->aggregateExperts($allAuthors);
|
||||||
|
|
||||||
|
return $this->buildPagedResult($experts, count($experts), count($ids), $totalArticles, $page, $perPage, 'pubmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PMC Search ====================
|
||||||
|
|
||||||
|
private function searchViaPMC($keyword, $perPage, $minYear, $page = 1)
|
||||||
|
{
|
||||||
|
set_time_limit(600);
|
||||||
|
|
||||||
|
$searchResult = $this->esearch('pmc', $keyword, $perPage, $minYear, $page);
|
||||||
|
$ids = $searchResult['ids'];
|
||||||
|
$totalArticles = $searchResult['total'];
|
||||||
|
|
||||||
|
if (empty($ids)) {
|
||||||
|
return $this->buildPagedResult([], 0, 0, $totalArticles, $page, $perPage, 'pmc');
|
||||||
|
}
|
||||||
|
|
||||||
|
$allAuthors = [];
|
||||||
|
$batches = array_chunk($ids, 5);
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
$xml = $this->efetchWithRetry('pmc', $batch);
|
||||||
|
if ($xml) {
|
||||||
|
$authors = $this->parsePMCXml($xml);
|
||||||
|
$allAuthors = array_merge($allAuthors, $authors);
|
||||||
|
}
|
||||||
|
usleep(500000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$experts = $this->aggregateExperts($allAuthors);
|
||||||
|
|
||||||
|
return $this->buildPagedResult($experts, count($experts), count($ids), $totalArticles, $page, $perPage, 'pmc');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== NCBI API ====================
|
||||||
|
|
||||||
|
private function esearch($db, $keyword, $perPage, $minYear, $page = 1)
|
||||||
|
{
|
||||||
|
$term = $keyword . ' AND ' . $minYear . ':' . date('Y') . '[pdat]';
|
||||||
|
$retstart = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$response = $this->httpClient->get($this->ncbiBaseUrl . 'esearch.fcgi', [
|
||||||
|
'query' => [
|
||||||
|
'db' => $db,
|
||||||
|
'term' => $term,
|
||||||
|
'retstart' => $retstart,
|
||||||
|
'retmax' => $perPage,
|
||||||
|
'retmode' => 'json',
|
||||||
|
'sort' => 'relevance',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = json_decode($response->getBody()->getContents(), true);
|
||||||
|
$ids = $data['esearchresult']['idlist'] ?? [];
|
||||||
|
$total = intval($data['esearchresult']['count'] ?? 0);
|
||||||
|
|
||||||
|
return ['ids' => $ids, 'total' => $total];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function efetch($db, $ids)
|
||||||
|
{
|
||||||
|
$response = $this->httpClient->post($this->ncbiBaseUrl . 'efetch.fcgi', [
|
||||||
|
'form_params' => [
|
||||||
|
'db' => $db,
|
||||||
|
'id' => implode(',', $ids),
|
||||||
|
'retmode' => 'xml',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response->getBody()->getContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function efetchWithRetry($db, $ids, $maxRetries = 3)
|
||||||
|
{
|
||||||
|
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
||||||
|
try {
|
||||||
|
return $this->efetch($db, $ids);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($attempt === $maxRetries) {
|
||||||
|
if (count($ids) > 1) {
|
||||||
|
$half = ceil(count($ids) / 2);
|
||||||
|
$firstHalf = array_slice($ids, 0, $half);
|
||||||
|
$secondHalf = array_slice($ids, $half);
|
||||||
|
$xml1 = $this->efetchWithRetry($db, $firstHalf, 2);
|
||||||
|
$xml2 = $this->efetchWithRetry($db, $secondHalf, 2);
|
||||||
|
return $this->mergeXml($xml1, $xml2);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
sleep($attempt * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mergeXml($xml1, $xml2)
|
||||||
|
{
|
||||||
|
if (empty($xml1)) return $xml2;
|
||||||
|
if (empty($xml2)) return $xml1;
|
||||||
|
return $xml1 . "\n" . $xml2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PubMed XML Parsing ====================
|
||||||
|
|
||||||
|
private function parsePubMedXml($xmlString)
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$xml = simplexml_load_string($xmlString);
|
||||||
|
if ($xml === false) {
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($xml->PubmedArticle as $article) {
|
||||||
|
$citation = $article->MedlineCitation;
|
||||||
|
$articleData = $citation->Article;
|
||||||
|
$title = $this->xmlNodeToString($articleData->ArticleTitle);
|
||||||
|
$pmid = (string) $citation->PMID;
|
||||||
|
|
||||||
|
$journal = '';
|
||||||
|
if (isset($articleData->Journal->Title)) {
|
||||||
|
$journal = (string) $articleData->Journal->Title;
|
||||||
|
}
|
||||||
|
if (!isset($articleData->AuthorList->Author)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($articleData->AuthorList->Author as $author) {
|
||||||
|
$lastName = (string) ($author->LastName ?? '');
|
||||||
|
$foreName = (string) ($author->ForeName ?? '');
|
||||||
|
$fullName = trim($foreName . ' ' . $lastName);
|
||||||
|
if (empty($fullName)) continue;
|
||||||
|
|
||||||
|
$email = '';
|
||||||
|
$affiliation = '';
|
||||||
|
if (isset($author->AffiliationInfo)) {
|
||||||
|
foreach ($author->AffiliationInfo as $affInfo) {
|
||||||
|
$affText = (string) $affInfo->Affiliation;
|
||||||
|
if (empty($affiliation)) $affiliation = $affText;
|
||||||
|
if (empty($email)) $email = $this->extractEmailFromText($affText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($email)) continue;
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'name' => $fullName,
|
||||||
|
'email' => strtolower($email),
|
||||||
|
'affiliation' => $this->cleanAffiliation($affiliation),
|
||||||
|
'article_title' => $title,
|
||||||
|
'article_id' => $pmid,
|
||||||
|
'journal' => $journal,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PMC XML Parsing ====================
|
||||||
|
|
||||||
|
private function parsePMCXml($xmlString)
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$xml = simplexml_load_string($xmlString);
|
||||||
|
if ($xml === false) {
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
$articles = $xml->article ?? $xml->children();
|
||||||
|
|
||||||
|
foreach ($articles as $article) {
|
||||||
|
if ($article->getName() !== 'article') continue;
|
||||||
|
|
||||||
|
$front = $article->front;
|
||||||
|
if (!$front) continue;
|
||||||
|
$articleMeta = $front->{'article-meta'};
|
||||||
|
if (!$articleMeta) continue;
|
||||||
|
|
||||||
|
$title = $this->xmlNodeToString($articleMeta->{'title-group'}->{'article-title'} ?? null);
|
||||||
|
$pmcId = '';
|
||||||
|
if (isset($articleMeta->{'article-id'})) {
|
||||||
|
foreach ($articleMeta->{'article-id'} as $idNode) {
|
||||||
|
if ((string) $idNode['pub-id-type'] === 'pmc') {
|
||||||
|
$pmcId = (string) $idNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$journal = '';
|
||||||
|
if (isset($front->{'journal-meta'}->{'journal-title'})) {
|
||||||
|
$journal = (string) $front->{'journal-meta'}->{'journal-title'};
|
||||||
|
} elseif (isset($front->{'journal-meta'}->{'journal-title-group'}->{'journal-title'})) {
|
||||||
|
$journal = (string) $front->{'journal-meta'}->{'journal-title-group'}->{'journal-title'};
|
||||||
|
}
|
||||||
|
|
||||||
|
$correspEmails = [];
|
||||||
|
if (isset($articleMeta->{'author-notes'})) {
|
||||||
|
$this->extractEmailsFromNode($articleMeta->{'author-notes'}, $correspEmails);
|
||||||
|
}
|
||||||
|
|
||||||
|
$affiliationMap = [];
|
||||||
|
if (isset($articleMeta->{'contrib-group'})) {
|
||||||
|
foreach ($articleMeta->{'contrib-group'}->children() as $child) {
|
||||||
|
if ($child->getName() === 'aff') {
|
||||||
|
$affId = (string) ($child['id'] ?? '');
|
||||||
|
$affText = $this->xmlNodeToString($child);
|
||||||
|
if ($affId) $affiliationMap[$affId] = $affText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($front->{'article-meta'}->{'aff'})) {
|
||||||
|
foreach ($front->{'article-meta'}->{'aff'} as $aff) {
|
||||||
|
$affId = (string) ($aff['id'] ?? '');
|
||||||
|
$affText = $this->xmlNodeToString($aff);
|
||||||
|
if ($affId) $affiliationMap[$affId] = $affText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($articleMeta->{'contrib-group'})) continue;
|
||||||
|
|
||||||
|
foreach ($articleMeta->{'contrib-group'}->contrib as $contrib) {
|
||||||
|
if ((string) ($contrib['contrib-type'] ?? '') !== 'author') continue;
|
||||||
|
$nameNode = $contrib->name;
|
||||||
|
if (!$nameNode) continue;
|
||||||
|
|
||||||
|
$surname = (string) ($nameNode->surname ?? '');
|
||||||
|
$givenNames = (string) ($nameNode->{'given-names'} ?? '');
|
||||||
|
$fullName = trim($givenNames . ' ' . $surname);
|
||||||
|
if (empty($fullName)) continue;
|
||||||
|
|
||||||
|
$email = '';
|
||||||
|
if (isset($contrib->email)) {
|
||||||
|
$email = strtolower(trim((string) $contrib->email));
|
||||||
|
}
|
||||||
|
|
||||||
|
$affiliation = '';
|
||||||
|
if (isset($contrib->xref)) {
|
||||||
|
foreach ($contrib->xref as $xref) {
|
||||||
|
if ((string) $xref['ref-type'] === 'aff') {
|
||||||
|
$rid = (string) $xref['rid'];
|
||||||
|
if (isset($affiliationMap[$rid])) {
|
||||||
|
$affiliation = $affiliationMap[$rid];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($affiliation) && isset($contrib->aff)) {
|
||||||
|
$affiliation = $this->xmlNodeToString($contrib->aff);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isCorresponding = false;
|
||||||
|
if (isset($contrib->xref)) {
|
||||||
|
foreach ($contrib->xref as $xref) {
|
||||||
|
if ((string) $xref['ref-type'] === 'corresp') $isCorresponding = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((string) ($contrib['corresp'] ?? '') === 'yes') $isCorresponding = true;
|
||||||
|
|
||||||
|
if (empty($email) && $isCorresponding && !empty($correspEmails)) {
|
||||||
|
$email = $correspEmails[0];
|
||||||
|
}
|
||||||
|
if (empty($email)) {
|
||||||
|
$extracted = $this->extractEmailFromText($affiliation);
|
||||||
|
if ($extracted) $email = $extracted;
|
||||||
|
}
|
||||||
|
if (empty($email)) continue;
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'name' => $fullName,
|
||||||
|
'email' => strtolower($email),
|
||||||
|
'affiliation' => $this->cleanAffiliation($affiliation),
|
||||||
|
'article_title' => $title,
|
||||||
|
'article_id' => $pmcId,
|
||||||
|
'journal' => $journal,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Aggregation / Pagination ====================
|
||||||
|
|
||||||
|
private function aggregateExperts($authorRecords)
|
||||||
|
{
|
||||||
|
$map = [];
|
||||||
|
foreach ($authorRecords as $record) {
|
||||||
|
$key = strtolower(trim($record['email']));
|
||||||
|
if (empty($key)) continue;
|
||||||
|
|
||||||
|
if (!isset($map[$key])) {
|
||||||
|
$map[$key] = [
|
||||||
|
'name' => $record['name'],
|
||||||
|
'email' => $record['email'],
|
||||||
|
'affiliation' => $record['affiliation'],
|
||||||
|
'paper_count' => 0,
|
||||||
|
'papers' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$map[$key]['paper_count']++;
|
||||||
|
if (count($map[$key]['papers']) < 10) {
|
||||||
|
$map[$key]['papers'][] = [
|
||||||
|
'title' => $record['article_title'],
|
||||||
|
'article_id' => $record['article_id'],
|
||||||
|
'journal' => $record['journal'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (empty($map[$key]['affiliation']) && !empty($record['affiliation'])) {
|
||||||
|
$map[$key]['affiliation'] = $record['affiliation'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$experts = array_values($map);
|
||||||
|
usort($experts, function ($a, $b) {
|
||||||
|
return $b['paper_count'] - $a['paper_count'];
|
||||||
|
});
|
||||||
|
return $experts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPagedResult($experts, $expertCount, $articlesScanned, $totalArticles, $page, $perPage, $source)
|
||||||
|
{
|
||||||
|
$totalPages = $totalArticles > 0 ? ceil($totalArticles / $perPage) : 0;
|
||||||
|
return [
|
||||||
|
'experts' => $experts,
|
||||||
|
'total' => $expertCount,
|
||||||
|
'articles_scanned' => $articlesScanned,
|
||||||
|
'total_articles' => $totalArticles,
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'total_pages' => $totalPages,
|
||||||
|
'has_more' => $page < $totalPages,
|
||||||
|
'source' => $source,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DB Helpers ====================
|
||||||
|
|
||||||
|
private function enrichExpertField($expertId, $field)
|
||||||
|
{
|
||||||
|
$field = trim($field);
|
||||||
|
if (empty($field)) return 0;
|
||||||
|
|
||||||
|
$exists = Db::name('expert_field')
|
||||||
|
->where('expert_id', $expertId)
|
||||||
|
->where('field', $field)
|
||||||
|
->where('state', 0)
|
||||||
|
->find();
|
||||||
|
if ($exists) return 0;
|
||||||
|
Db::name('expert_field')->insert([
|
||||||
|
'expert_id' => $expertId,
|
||||||
|
'field' => mb_substr($field, 0, 128),
|
||||||
|
'state' => 0,
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Text Helpers ====================
|
||||||
|
|
||||||
|
private function extractEmailFromText($text)
|
||||||
|
{
|
||||||
|
if (empty($text)) return '';
|
||||||
|
if (preg_match('/[Ee]lectronic address:\s*([^\s;,]+@[^\s;,]+)/', $text, $m)) {
|
||||||
|
return strtolower(trim($m[1], '.'));
|
||||||
|
}
|
||||||
|
if (preg_match('/[Ee]-?mail:\s*([^\s;,]+@[^\s;,]+)/', $text, $m)) {
|
||||||
|
return strtolower(trim($m[1], '.'));
|
||||||
|
}
|
||||||
|
if (preg_match('/\b([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})\b/', $text, $m)) {
|
||||||
|
return strtolower(trim($m[1], '.'));
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractEmailsFromNode($node, &$emails)
|
||||||
|
{
|
||||||
|
if ($node === null) return;
|
||||||
|
foreach ($node->children() as $child) {
|
||||||
|
if ($child->getName() === 'email') {
|
||||||
|
$email = strtolower(trim((string) $child));
|
||||||
|
if (!empty($email) && !in_array($email, $emails)) $emails[] = $email;
|
||||||
|
}
|
||||||
|
$this->extractEmailsFromNode($child, $emails);
|
||||||
|
}
|
||||||
|
$text = (string) $node;
|
||||||
|
if (preg_match_all('/\b([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})\b/', $text, $matches)) {
|
||||||
|
foreach ($matches[1] as $email) {
|
||||||
|
$email = strtolower(trim($email, '.'));
|
||||||
|
if (!in_array($email, $emails)) $emails[] = $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanAffiliation($text)
|
||||||
|
{
|
||||||
|
$text = preg_replace('/\s*[Ee]lectronic address:\s*[^\s;,]+@[^\s;,]+/', '', $text);
|
||||||
|
$text = preg_replace('/\s*[Ee]-?mail:\s*[^\s;,]+@[^\s;,]+/', '', $text);
|
||||||
|
$text = preg_replace('/\s*\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/', '', $text);
|
||||||
|
return trim($text, " \t\n\r\0\x0B.,;");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function xmlNodeToString($node)
|
||||||
|
{
|
||||||
|
if ($node === null) return '';
|
||||||
|
$xml = $node->asXML();
|
||||||
|
$text = strip_tags($xml);
|
||||||
|
$text = html_entity_decode($text, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||||
|
return trim(preg_replace('/\s+/', ' ', $text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Logging ====================
|
||||||
|
|
||||||
|
public function log($msg)
|
||||||
|
{
|
||||||
|
$line = date('Y-m-d H:i:s') . ' ' . $msg . PHP_EOL;
|
||||||
|
@file_put_contents($this->logFile, $line, FILE_APPEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -700,19 +700,22 @@ page={{stage_page}},%号
|
|||||||
$latexLines[] = " \\centering";
|
$latexLines[] = " \\centering";
|
||||||
$latexLines[] = " \\includegraphics[width=0.9\\textwidth]{" . $sImageUrl . "}";
|
$latexLines[] = " \\includegraphics[width=0.9\\textwidth]{" . $sImageUrl . "}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(!empty($aImageInfo['title']) && !empty($aImageInfo['note'])){
|
if(!empty($aImageInfo['title']) && !empty($aImageInfo['note'])){
|
||||||
$escapedTitle = '{\fontspec{Calibri}\footnotesize\bfseries\color{figerTitleColor} '.$this->dealContent(preg_replace('/^Figure\s+\d+\s*/i', '', $aImageInfo['title']),[]).'}\\\\';
|
$escapedTitle = '{\fontspec{Calibri}\footnotesize\bfseries\color{figerTitleColor} '.$this->dealContent(preg_replace('/^Figure\s+\d+\s*/i', '', $aImageInfo['title']),[]).'}\\\\';
|
||||||
$escapedTitle .= '{\vspace{0.5em}\raggedright\small {'.$this->dealContent($aImageInfo['note'],[]).'}}';
|
$escapedTitle .= '{\vspace{0.5em}\raggedright\small {'.$this->dealContent($aImageInfo['note'],[]).'}}';
|
||||||
$latexLines[] = " \\caption{" . $escapedTitle . "}";
|
$latexLines[] = " \\caption{" . $escapedTitle . "}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($ami_info['title'])) {
|
if (!empty($aImageInfo['title']) && empty($aImageInfo['note'])) {
|
||||||
$escapedTitle = $this->dealContent(preg_replace('/^Figure\s+\d+\s*/i', '', $ami_info['title']),[]);
|
$escapedTitle = $this->dealContent(preg_replace('/^Figure\s+\d+\s*/i', '', $aImageInfo['title']),[]);
|
||||||
$latexLines[] = " \\caption{" . $escapedTitle . "}";
|
$latexLines[] = " \\caption{" . $escapedTitle . "}";
|
||||||
}
|
}
|
||||||
|
//添加图片的label
|
||||||
|
$ami_id = empty($aImageInfo['ami_id']) ? 0 : $aImageInfo['ami_id'];
|
||||||
|
if(!empty($ami_id)){
|
||||||
|
$latexLines[] = " \\label{fig:" . $ami_id . "}";
|
||||||
|
}
|
||||||
$latexLines[] = "\\end{figure" . ($isWideImage ? "*" : "") . "}";
|
$latexLines[] = "\\end{figure" . ($isWideImage ? "*" : "") . "}";
|
||||||
|
|
||||||
return ['status' => 1,'msg' => 'success','data' => implode("\n", $latexLines)];
|
return ['status' => 1,'msg' => 'success','data' => implode("\n", $latexLines)];
|
||||||
|
|||||||
184
application/common/Messages.php
Normal file
184
application/common/Messages.php
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\common;
|
||||||
|
use think\Db;
|
||||||
|
class Messages
|
||||||
|
{
|
||||||
|
//消息类型
|
||||||
|
private $aMessagesType = [
|
||||||
|
1 => [
|
||||||
|
'title' => 'Submission Received',
|
||||||
|
'content' => '<b>Your manuscript(ID: {#accept_sn#}) has been successfully submitted.</b><br/>
|
||||||
|
The editorial office is conducting an initial assessment. You will be notified once the review process begins.'
|
||||||
|
],//'投稿成功',
|
||||||
|
2 => [
|
||||||
|
'title' => 'Manuscript Under Review',
|
||||||
|
'content' => 'Your manuscript is now under review.<br>
|
||||||
|
Manuscript (ID: {#accept_sn#}) has been sent to peer reviewers for evaluation. You will be notified once a review decision becomes available.'
|
||||||
|
],//'稿件进入审稿',
|
||||||
|
3 => [
|
||||||
|
'title' => 'Declined for Publication',
|
||||||
|
'content' => '<b>Editorial decision issued.</b><br/>
|
||||||
|
After evaluation, your manuscript(ID: {#accept_sn#}) has not been accepted for publication. Please refer to the decision letter for details and reviewer comments.'
|
||||||
|
],//'退稿通知',
|
||||||
|
4 => [
|
||||||
|
'title' => 'Revision Requested',
|
||||||
|
'content' => '<b>Revision requested.</b><br/>
|
||||||
|
Editors/reviewers have requested revisions to your manuscript(ID: {#accept_sn#}). Please review the comments and submit your revised version within the required timeframe.'
|
||||||
|
],//'退修通知',
|
||||||
|
5 => [
|
||||||
|
'title' => 'Accepted for Publication',
|
||||||
|
'content' => '<b>Revision requested.</b><br/>
|
||||||
|
Editors/reviewers have requested revisions to your manuscript(ID: {#accept_sn#}). Please review the comments and submit your revised version within the required timeframe.'
|
||||||
|
],//'录用通知',
|
||||||
|
6 => [
|
||||||
|
'title' => 'APC Payment Required',
|
||||||
|
'content' => '<b>APC payment required.</b><br/>
|
||||||
|
Your manuscript(ID: {#accept_sn#}) has reached the payment stage. Please complete the Article Processing Charge (APC) to proceed with publication processing.'
|
||||||
|
],//'缴费提醒',
|
||||||
|
7 => [
|
||||||
|
'title' => 'Proofreading Confirmation Required',
|
||||||
|
'content' => '<b>Proofs ready for review.</b><br/>
|
||||||
|
Your manuscript(ID: {#accept_sn#}) proof is available. Please check and confirm carefully within 48 hours.'
|
||||||
|
],//'校对通知',
|
||||||
|
8 => [
|
||||||
|
'title' => 'Manuscript Published Online',
|
||||||
|
'content' => '<b>Your article is now published online!</b><br/>
|
||||||
|
The final version of your manuscript(ID: {#accept_sn#}) is available on the journal website. Thank you for publishing with us.'
|
||||||
|
],//'发表通知',
|
||||||
|
9 => [
|
||||||
|
'title' => 'APC Update',
|
||||||
|
'content' => '<b>APC update applied.</b><br/>
|
||||||
|
The Article Processing Charge for your manuscript(ID: {#accept_sn#}) has been adjusted. Please check the updated payment details in your submission dashboard.'
|
||||||
|
],//'稿费修改',
|
||||||
|
10 => [
|
||||||
|
'title' => 'Review Invitation(ID: {#accept_sn#})',
|
||||||
|
'content' => '<b>Review invitation received.</b><br/>
|
||||||
|
You are invited to review a manuscript (ID: {#accept_sn#}). Please accept or decline the invitation via your reviewer dashboard.'
|
||||||
|
],//'审稿通知',
|
||||||
|
11 => [
|
||||||
|
'title' => 'Review Invitation Reminder',
|
||||||
|
'content' => '<b>Reminder: Review invitation pending.</b><br/>
|
||||||
|
You have a pending review invitation (ID: {#accept_sn#}). Kindly respond at your earliest convenience.'
|
||||||
|
],//'审稿提醒',
|
||||||
|
12 => [
|
||||||
|
'title' => 'Review Report Reminder',
|
||||||
|
'content' => '<b>Reminder: Review report due.</b><br/>
|
||||||
|
Thank you for accepting the review (ID: {#accept_sn#}). Please submit your review report to help us proceed with the editorial decision.'
|
||||||
|
],//'审稿报告提交',
|
||||||
|
13 => [
|
||||||
|
'title' => 'Re-review Invitation',
|
||||||
|
'content' => '<b>Re-review request received.</b><br/>
|
||||||
|
A revised version of manuscript (ID: {#accept_sn#}) is available for your follow-up assessment. Please access the reviewer dashboard to proceed.'
|
||||||
|
],//'复审邀请',
|
||||||
|
14 => [
|
||||||
|
'title' => 'Re-review Reminder',
|
||||||
|
'content' => '<b>Reminder: Re-review pending.</b><br/>
|
||||||
|
Your follow-up review for manuscript (ID: {#accept_sn#}) is still pending. We would appreciate your evaluation at your earliest convenience.'
|
||||||
|
],//'复审提醒',
|
||||||
|
15 => [
|
||||||
|
'title' => 'Review Closed',
|
||||||
|
'content' => '<b>Review assignment closed.</b><br/>
|
||||||
|
Your review for manuscript (ID: {#accept_sn#}) is no longer required. Thank you for your support and contribution to the journal.'
|
||||||
|
],//审稿关闭提醒
|
||||||
|
];
|
||||||
|
|
||||||
|
private $aField = ['article_id','user_id','type','title','content','is_read','create_time','update_user_id','update_time'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站内信息添加
|
||||||
|
*/
|
||||||
|
public function add($aParam = []){
|
||||||
|
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? [] : $aParam;
|
||||||
|
//必填参数验证
|
||||||
|
$iType = empty($aParam['type']) ? 0 : $aParam['type'];
|
||||||
|
$iArticleId = empty($aParam['article_id']) ? 0 : $aParam['article_id'];
|
||||||
|
if(empty($iArticleId) || empty($iType) || empty($aParam['user_id'])){
|
||||||
|
return ['status' => 2, 'msg' => 'Parameter is empty article title/message type/user_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if($iType == 2){//验证是否插入
|
||||||
|
$aMessages = $this->get($aParam);
|
||||||
|
if(!empty($aMessages['data'])){
|
||||||
|
return ['status' => 3, 'msg' => 'The review reminder has been sent'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//根据类型获取消息内容
|
||||||
|
$aMessagesType = $this->aMessagesType;
|
||||||
|
$aMessagesTypeInfo = empty($aMessagesType[$iType]) ? [] : $aMessagesType[$iType];
|
||||||
|
if(empty($aMessagesTypeInfo)){
|
||||||
|
return ['status' => 4, 'msg' => 'Message content not found'];
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取表字段 进行字段赋值
|
||||||
|
$aField = $this->aField;
|
||||||
|
$aInsert = [];
|
||||||
|
foreach ($aField as $key => $value) {
|
||||||
|
if(empty($aParam[$value])){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$aInsert[$value] = $aParam[$value];
|
||||||
|
}
|
||||||
|
//获取稿件号
|
||||||
|
$sAcceptSn = empty($aParam['accept_sn']) ? '' : $aParam['accept_sn'];
|
||||||
|
$aInsert['title'] = str_replace('{#accept_sn#}', $sAcceptSn, $aMessagesTypeInfo['title']);
|
||||||
|
$aInsert['content'] = str_replace('{#accept_sn#}', $sAcceptSn, $aMessagesTypeInfo['content']);
|
||||||
|
$aInsert['create_time'] = time();
|
||||||
|
|
||||||
|
//拒稿和退修 查询是否有同意审稿但未审稿的审稿人
|
||||||
|
if(in_array($iType, [3,4])){
|
||||||
|
$aWhere = ['article_id' => $iArticleId,'state' => 0];
|
||||||
|
$aReviewer = Db::name('article_reviewer')->where($aWhere)->column('reviewer_id');
|
||||||
|
$aBatchInsert = [];
|
||||||
|
if(!empty($aReviewer)){
|
||||||
|
$aMessagesTypeInfo = $aMessagesType[15];
|
||||||
|
$sTitle = str_replace('{#accept_sn#}', $sAcceptSn, $aMessagesTypeInfo['title']);
|
||||||
|
$sContent = str_replace('{#accept_sn#}', $sAcceptSn, $aMessagesTypeInfo['content']);
|
||||||
|
$aReviewer =array_unique($aReviewer);
|
||||||
|
foreach ($aReviewer as $key => $value) {
|
||||||
|
if(empty($value)){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$aBatchInsert[] = ['title' => $sTitle,'content' => $sContent,'type' => 15,'create_time' => time(),'article_id' => $iArticleId,'user_id' => $value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$aBatchInsert[] = ['title' => $aInsert['title'],'content' => $aInsert['content'],'type' => $aInsert['type'],'create_time' => time(),'article_id' => $iArticleId,'user_id' => $aInsert['user_id']];
|
||||||
|
|
||||||
|
$result = Db::name('messages')->insertAll($aBatchInsert);
|
||||||
|
if(empty($result)){
|
||||||
|
return ['status' => 6, 'msg' => '数据插入失败'.Db::getLastSql()."\n数据内容:",'data' => $aParam];
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$result = Db::name('messages')->insertGetId($aInsert);
|
||||||
|
if(empty($result)){
|
||||||
|
return ['status' => 5, 'msg' => '数据插入失败'.Db::getLastSql()."\n数据内容:",'data' => $aParam];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status' => 1, 'msg' => '数据插入成功'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取信息
|
||||||
|
*/
|
||||||
|
public function get($aParam = []){
|
||||||
|
//获取参数
|
||||||
|
$aParam = empty($aParam) ? [] : $aParam;
|
||||||
|
//必填参数验证
|
||||||
|
if(empty($aParam['article_id']) || empty($aParam['type']) || empty($aParam['user_id'])){
|
||||||
|
return ['status' => 2, 'msg' => '非法操作'];
|
||||||
|
}
|
||||||
|
//获取表字段 进行字段赋值
|
||||||
|
$aField = $this->aField;
|
||||||
|
$aWhere = [];
|
||||||
|
foreach ($aField as $key => $value) {
|
||||||
|
if(empty($aParam[$value])){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$aWhere[$value] = $aParam[$value];
|
||||||
|
}
|
||||||
|
$aMessages = Db::name('messages')->where($aWhere)->find();
|
||||||
|
return ['status' => 1, 'msg' => '获取数据成功','data' => $aMessages];
|
||||||
|
}
|
||||||
|
}
|
||||||
506
application/common/PromotionService.php
Normal file
506
application/common/PromotionService.php
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
use think\Cache;
|
||||||
|
use think\Queue;
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
|
||||||
|
class PromotionService
|
||||||
|
{
|
||||||
|
private $logFile;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->logFile = ROOT_PATH . 'runtime' . DS . 'promotion_task.log';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the next email in a promotion task (called by queue job)
|
||||||
|
*/
|
||||||
|
public function processNextEmail($taskId)
|
||||||
|
{
|
||||||
|
$task = Db::name('promotion_task')->where('task_id', $taskId)->find();
|
||||||
|
if (!$task) {
|
||||||
|
return ['done' => true, 'reason' => 'task_not_found'];
|
||||||
|
}
|
||||||
|
if ($task['state'] != 1) {
|
||||||
|
return ['done' => true, 'reason' => 'task_not_running', 'state' => $task['state']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentHour = intval(date('G'));
|
||||||
|
if ($currentHour < $task['send_start_hour'] || $currentHour >= $task['send_end_hour']) {
|
||||||
|
$this->enqueueNextEmail($taskId, 300);
|
||||||
|
return ['done' => false, 'reason' => 'outside_send_window', 'retry_in' => 300];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($task['sent_count'] > 0 && $task['max_bounce_rate'] > 0) {
|
||||||
|
$bounceRate = ($task['bounce_count'] / $task['sent_count']) * 100;
|
||||||
|
if ($bounceRate >= $task['max_bounce_rate']) {
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->update([
|
||||||
|
'state' => 2,
|
||||||
|
'utime' => time(),
|
||||||
|
]);
|
||||||
|
$this->log("Task {$taskId} auto-paused: bounce rate {$bounceRate}% >= {$task['max_bounce_rate']}%");
|
||||||
|
return ['done' => true, 'reason' => 'auto_paused_bounce_rate', 'bounce_rate' => $bounceRate];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$logEntry = Db::name('promotion_email_log')
|
||||||
|
->where('task_id', $taskId)
|
||||||
|
->where('state', 0)
|
||||||
|
->order('log_id asc')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$logEntry) {
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->update([
|
||||||
|
'state' => 3,
|
||||||
|
'utime' => time(),
|
||||||
|
]);
|
||||||
|
return ['done' => true, 'reason' => 'all_emails_processed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$expert = Db::name('expert')->where('expert_id', $logEntry['expert_id'])->find();
|
||||||
|
if (!$expert || $expert['state'] == 4 || $expert['state'] == 5) {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([
|
||||||
|
'state' => 2,
|
||||||
|
'error_msg' => 'Expert invalid or deleted (state=' . (isset($expert['state']) ? $expert['state'] : 'null') . ')',
|
||||||
|
'send_time' => time(),
|
||||||
|
]);
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->setInc('fail_count');
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->update(['utime' => time()]);
|
||||||
|
$this->enqueueNextEmail($taskId, 2);
|
||||||
|
return ['done' => false, 'skipped' => $logEntry['email_to'], 'reason' => 'expert_invalid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->pickSmtpAccountForTask($task);
|
||||||
|
if (!$account) {
|
||||||
|
$this->enqueueNextEmail($taskId, 600);
|
||||||
|
return ['done' => false, 'reason' => 'no_smtp_quota', 'retry_in' => 600];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用预生成内容;无则现场渲染
|
||||||
|
$subject = '';
|
||||||
|
$body = '';
|
||||||
|
$hasPrepared = !empty($logEntry['subject_prepared']) && !empty($logEntry['body_prepared']);
|
||||||
|
|
||||||
|
if ($hasPrepared) {
|
||||||
|
$subject = $logEntry['subject_prepared'];
|
||||||
|
$body = $logEntry['body_prepared'];
|
||||||
|
} else {
|
||||||
|
$journal = Db::name('journal')->where('journal_id', $task['journal_id'])->find();
|
||||||
|
$expertVars = $this->buildExpertVars($expert);
|
||||||
|
$journalVars = $this->buildJournalVars($journal);
|
||||||
|
$vars = array_merge($journalVars, $expertVars);
|
||||||
|
|
||||||
|
$rendered = $this->renderFromTemplate(
|
||||||
|
$task['template_id'],
|
||||||
|
$task['journal_id'],
|
||||||
|
json_encode($vars, JSON_UNESCAPED_UNICODE),
|
||||||
|
$task['style_id']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($rendered['code'] !== 0) {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([
|
||||||
|
'state' => 2,
|
||||||
|
'error_msg' => 'Template render failed: ' . $rendered['msg'],
|
||||||
|
'send_time' => time(),
|
||||||
|
]);
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->setInc('fail_count');
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->update(['utime' => time()]);
|
||||||
|
$this->enqueueNextEmail($taskId, 2);
|
||||||
|
return ['done' => false, 'failed' => $logEntry['email_to'], 'reason' => 'template_error'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = $rendered['data']['subject'];
|
||||||
|
$body = $rendered['data']['body'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->doSendEmail($account, $logEntry['email_to'], $subject, $body);
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
if ($result['status'] === 1) {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([
|
||||||
|
'j_email_id' => $account['j_email_id'],
|
||||||
|
'subject' => mb_substr($subject, 0, 512),
|
||||||
|
'state' => 1,
|
||||||
|
'send_time' => $now,
|
||||||
|
]);
|
||||||
|
Db::name('journal_email')->where('j_email_id', $account['j_email_id'])->setInc('today_sent');
|
||||||
|
Db::name('expert')->where('expert_id', $expert['expert_id'])->update(['state' => 1, 'ltime' => $now]);
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->setInc('sent_count');
|
||||||
|
} else {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([
|
||||||
|
'j_email_id' => $account['j_email_id'],
|
||||||
|
'subject' => mb_substr($subject, 0, 512),
|
||||||
|
'state' => 2,
|
||||||
|
'error_msg' => mb_substr($result['data'], 0, 512),
|
||||||
|
'send_time' => $now,
|
||||||
|
]);
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->setInc('fail_count');
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->update(['utime' => $now]);
|
||||||
|
|
||||||
|
$delay = rand(max(5, $task['min_interval']), max($task['min_interval'], $task['max_interval']));
|
||||||
|
$this->enqueueNextEmail($taskId, $delay);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'done' => false,
|
||||||
|
'sent' => $result['status'] === 1,
|
||||||
|
'email' => $logEntry['email_to'],
|
||||||
|
'next_in' => $delay,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 准备与触发(今日准备、明日发送) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为指定任务预生成所有待发邮件的 subject/body,写入 log;完成后将任务置为 state=5(已准备)
|
||||||
|
* @param int $taskId
|
||||||
|
* @return array ['prepared' => int, 'failed' => int, 'error' => string|null]
|
||||||
|
*/
|
||||||
|
public function prepareTask($taskId)
|
||||||
|
{
|
||||||
|
$task = Db::name('promotion_task')->where('task_id', $taskId)->find();
|
||||||
|
if (!$task) {
|
||||||
|
return ['prepared' => 0, 'failed' => 0, 'error' => 'task_not_found'];
|
||||||
|
}
|
||||||
|
if ($task['state'] != 0) {
|
||||||
|
return ['prepared' => 0, 'failed' => 0, 'error' => 'task_state_not_draft'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$journal = Db::name('journal')->where('journal_id', $task['journal_id'])->find();
|
||||||
|
$logs = Db::name('promotion_email_log')
|
||||||
|
->where('task_id', $taskId)
|
||||||
|
->where('state', 0)
|
||||||
|
->where('prepared_at', 0)
|
||||||
|
->order('log_id asc')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$prepared = 0;
|
||||||
|
$failed = 0;
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$journalVars = $this->buildJournalVars($journal);
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
$expert = Db::name('expert')->where('expert_id', $log['expert_id'])->find();
|
||||||
|
if (!$expert) {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $log['log_id'])->update([
|
||||||
|
'state' => 2,
|
||||||
|
'error_msg' => 'Expert not found',
|
||||||
|
'send_time' => $now,
|
||||||
|
]);
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expertVars = $this->buildExpertVars($expert);
|
||||||
|
$vars = array_merge($journalVars, $expertVars);
|
||||||
|
$rendered = $this->renderFromTemplate(
|
||||||
|
$task['template_id'],
|
||||||
|
$task['journal_id'],
|
||||||
|
json_encode($vars, JSON_UNESCAPED_UNICODE),
|
||||||
|
$task['style_id']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($rendered['code'] !== 0) {
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $log['log_id'])->update([
|
||||||
|
'state' => 2,
|
||||||
|
'error_msg' => 'Prepare failed: ' . $rendered['msg'],
|
||||||
|
'send_time' => $now,
|
||||||
|
]);
|
||||||
|
$failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::name('promotion_email_log')->where('log_id', $log['log_id'])->update([
|
||||||
|
'subject_prepared' => mb_substr($rendered['data']['subject'], 0, 512),
|
||||||
|
'body_prepared' => $rendered['data']['body'],
|
||||||
|
'prepared_at' => $now,
|
||||||
|
]);
|
||||||
|
$prepared++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::name('promotion_task')->where('task_id', $taskId)->update([
|
||||||
|
'state' => 5,
|
||||||
|
'utime' => $now,
|
||||||
|
]);
|
||||||
|
$this->log("prepareTask task_id={$taskId} prepared={$prepared} failed={$failed}");
|
||||||
|
|
||||||
|
return ['prepared' => $prepared, 'failed' => $failed, 'error' => null];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为指定日期的任务批量预生成邮件(供定时任务调用,如每天 22:00 准备明天的)
|
||||||
|
* @param string $date Y-m-d,如 2026-03-12
|
||||||
|
* @return array ['tasks' => int, 'prepared' => int, 'failed' => int, 'details' => []]
|
||||||
|
*/
|
||||||
|
public function prepareTasksForDate($date)
|
||||||
|
{
|
||||||
|
$tasks = Db::name('promotion_task')
|
||||||
|
->where('send_date', $date)
|
||||||
|
->where('state', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$totalPrepared = 0;
|
||||||
|
$totalFailed = 0;
|
||||||
|
$details = [];
|
||||||
|
|
||||||
|
foreach ($tasks as $task) {
|
||||||
|
$ret = $this->prepareTask($task['task_id']);
|
||||||
|
$totalPrepared += $ret['prepared'];
|
||||||
|
$totalFailed += $ret['failed'];
|
||||||
|
$details[] = [
|
||||||
|
'task_id' => $task['task_id'],
|
||||||
|
'task_name' => $task['task_name'],
|
||||||
|
'prepared' => $ret['prepared'],
|
||||||
|
'failed' => $ret['failed'],
|
||||||
|
'error' => $ret['error'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log("prepareTasksForDate date={$date} tasks=" . count($tasks) . " prepared={$totalPrepared} failed={$totalFailed}");
|
||||||
|
return [
|
||||||
|
'tasks' => count($tasks),
|
||||||
|
'prepared' => $totalPrepared,
|
||||||
|
'failed' => $totalFailed,
|
||||||
|
'details' => $details,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发指定日期的已准备任务开始发送(供定时任务调用,如每天 8:00 触发今天的)
|
||||||
|
* 会先对 send_date=date 且 state=0 的任务做一次补准备,再启动所有 state=5 的任务
|
||||||
|
* @param string $date Y-m-d
|
||||||
|
* @return array ['prepared' => int, 'started' => int, 'task_ids' => []]
|
||||||
|
*/
|
||||||
|
public function startTasksForDate($date)
|
||||||
|
{
|
||||||
|
// 补准备:当天日期但尚未准备的任务(如 22:00 后创建)
|
||||||
|
$catchUpTasks = Db::name('promotion_task')
|
||||||
|
->where('send_date', $date)
|
||||||
|
->where('state', 0)
|
||||||
|
->select();
|
||||||
|
foreach ($catchUpTasks as $t) {
|
||||||
|
$this->prepareTask($t['task_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks = Db::name('promotion_task')
|
||||||
|
->where('send_date', $date)
|
||||||
|
->where('state', 5)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$started = 0;
|
||||||
|
$taskIds = [];
|
||||||
|
|
||||||
|
foreach ($tasks as $task) {
|
||||||
|
Db::name('promotion_task')->where('task_id', $task['task_id'])->update([
|
||||||
|
'state' => 1,
|
||||||
|
'utime' => time(),
|
||||||
|
]);
|
||||||
|
$this->enqueueNextEmail($task['task_id'], 0);
|
||||||
|
$started++;
|
||||||
|
$taskIds[] = $task['task_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log("startTasksForDate date={$date} started={$started} task_ids=" . implode(',', $taskIds));
|
||||||
|
return [
|
||||||
|
'prepared' => count($catchUpTasks),
|
||||||
|
'started' => $started,
|
||||||
|
'task_ids' => $taskIds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Queue ====================
|
||||||
|
|
||||||
|
public function enqueueNextEmail($taskId, $delay = 0)
|
||||||
|
{
|
||||||
|
$jobClass = 'app\api\job\PromotionSend@fire';
|
||||||
|
$data = ['task_id' => $taskId];
|
||||||
|
|
||||||
|
if ($delay > 0) {
|
||||||
|
Queue::later($delay, $jobClass, $data, 'promotion');
|
||||||
|
} else {
|
||||||
|
Queue::push($jobClass, $data, 'promotion');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SMTP ====================
|
||||||
|
|
||||||
|
public function pickSmtpAccountForTask($task)
|
||||||
|
{
|
||||||
|
$journalId = $task['journal_id'];
|
||||||
|
$smtpIds = $task['smtp_ids'] ? array_map('intval', explode(',', $task['smtp_ids'])) : [];
|
||||||
|
|
||||||
|
$query = Db::name('journal_email')
|
||||||
|
->where('journal_id', $journalId)
|
||||||
|
->where('state', 0);
|
||||||
|
|
||||||
|
if (!empty($smtpIds)) {
|
||||||
|
$query->where('j_email_id', 'in', $smtpIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
$accounts = $query->select();
|
||||||
|
if (empty($accounts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$best = null;
|
||||||
|
$bestRemaining = -1;
|
||||||
|
|
||||||
|
foreach ($accounts as $acc) {
|
||||||
|
$this->resetDailyCountIfNeeded($acc);
|
||||||
|
$remaining = $acc['daily_limit'] - $acc['today_sent'];
|
||||||
|
if ($remaining > 0 && $remaining > $bestRemaining) {
|
||||||
|
$best = $acc;
|
||||||
|
$bestRemaining = $remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $best;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetDailyCountIfNeeded(&$account)
|
||||||
|
{
|
||||||
|
$todayDate = date('Y-m-d');
|
||||||
|
$cacheKey = 'smtp_reset_' . $account['j_email_id'];
|
||||||
|
$lastReset = Cache::get($cacheKey);
|
||||||
|
|
||||||
|
if ($lastReset !== $todayDate) {
|
||||||
|
Db::name('journal_email')
|
||||||
|
->where('j_email_id', $account['j_email_id'])
|
||||||
|
->update(['today_sent' => 0]);
|
||||||
|
$account['today_sent'] = 0;
|
||||||
|
Cache::set($cacheKey, $todayDate, 86400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function doSendEmail($account, $toEmail, $subject, $htmlContent)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->SMTPDebug = 0;
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->Host = $account['smtp_host'];
|
||||||
|
$mail->Port = intval($account['smtp_port']);
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $account['smtp_user'];
|
||||||
|
$mail->Password = $account['smtp_password'];
|
||||||
|
|
||||||
|
if ($account['smtp_encryption'] === 'ssl') {
|
||||||
|
$mail->SMTPSecure = 'ssl';
|
||||||
|
} elseif ($account['smtp_encryption'] === 'tls') {
|
||||||
|
$mail->SMTPSecure = 'tls';
|
||||||
|
} else {
|
||||||
|
$mail->SMTPSecure = false;
|
||||||
|
$mail->SMTPAutoTLS = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fromName = !empty($account['smtp_from_name']) ? $account['smtp_from_name'] : $account['smtp_user'];
|
||||||
|
$mail->setFrom($account['smtp_user'], $fromName);
|
||||||
|
$mail->addReplyTo($account['smtp_user'], $fromName);
|
||||||
|
$mail->addAddress($toEmail);
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->Subject = $subject;
|
||||||
|
$mail->Body = $htmlContent;
|
||||||
|
$mail->AltBody = strip_tags($htmlContent);
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
|
||||||
|
return ['status' => 1, 'data' => 'success'];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['status' => 0, 'data' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Template Rendering ====================
|
||||||
|
|
||||||
|
public function renderFromTemplate($templateId, $journalId, $varsJson, $styleId = 0)
|
||||||
|
{
|
||||||
|
$tpl = Db::name('mail_template')->where('template_id', $templateId)->where('journal_id', $journalId)->where('state', 0)->find();
|
||||||
|
if (!$tpl) {
|
||||||
|
return ['code' => 1, 'msg' => 'Template not found'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$vars = [];
|
||||||
|
if ($varsJson) {
|
||||||
|
$decoded = json_decode($varsJson, true);
|
||||||
|
if (is_array($decoded)) $vars = $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = $this->renderVars($tpl['subject'], $vars);
|
||||||
|
$body = $this->renderVars($tpl['body_html'], $vars);
|
||||||
|
$finalBody = $body;
|
||||||
|
|
||||||
|
if ($styleId) {
|
||||||
|
$style = Db::name('mail_style')->where('style_id', $styleId)->where('state', 0)->find();
|
||||||
|
if ($style) {
|
||||||
|
$header = $style['header_html'] ? $this->renderVars($style['header_html'],$vars):'';
|
||||||
|
$footer = $style['footer_html'] ? $this->renderVars($style['footer_html'],$vars): '';
|
||||||
|
$finalBody = $header . $body . $footer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['code' => 0, 'msg' => 'success', 'data' => ['subject' => $subject, 'body' => $finalBody]];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildExpertVars($expert)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'expert_title' => "Ph.D",
|
||||||
|
'expert_name' => $expert['name'] ?? '',
|
||||||
|
'expert_email' => $expert['email'] ?? '',
|
||||||
|
'expert_affiliation' => $expert['affiliation'] ?? '',
|
||||||
|
'expert_field' => $expert['field'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildJournalVars($journal)
|
||||||
|
{
|
||||||
|
if (!$journal) return [];
|
||||||
|
$zb = Db::name("board_to_journal")
|
||||||
|
->where("journal_id",$journal['journal_id'])
|
||||||
|
->where("state",0)
|
||||||
|
->where('type',0)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'journal_name' => $journal['title'] ?? '',
|
||||||
|
'journal_abbr' => $journal['jabbr'] ?? '',
|
||||||
|
'journal_url' => $journal['website'] ?? '',
|
||||||
|
'journal_email' => $journal['email'] ?? '',
|
||||||
|
'indexing_databases' => $journal['databases'] ?? '',
|
||||||
|
'submission_url' => "https://submission.tmrjournals.com/",
|
||||||
|
'eic_name' => $zb['realname'] ?? '',
|
||||||
|
'editor_name' => $journal['editor_name'],
|
||||||
|
'special_support_deadline'=>date("Y-m-d",strtotime("+30 days"))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderVars($tpl, $vars)
|
||||||
|
{
|
||||||
|
if (!is_string($tpl) || $tpl === '') return '';
|
||||||
|
if (!is_array($vars) || empty($vars)) return $tpl;
|
||||||
|
|
||||||
|
$replace = [];
|
||||||
|
foreach ($vars as $k => $v) {
|
||||||
|
$key = trim((string)$k);
|
||||||
|
if ($key === '') continue;
|
||||||
|
$replace['{{' . $key . '}}'] = (string)$v;
|
||||||
|
$replace['{' . $key . '}'] = (string)$v;
|
||||||
|
}
|
||||||
|
return str_replace(array_keys($replace), array_values($replace), $tpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Logging ====================
|
||||||
|
|
||||||
|
public function log($msg)
|
||||||
|
{
|
||||||
|
$line = date('Y-m-d H:i:s') . ' ' . $msg . PHP_EOL;
|
||||||
|
@file_put_contents($this->logFile, $line, FILE_APPEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -172,8 +172,11 @@ return [
|
|||||||
// 日志保存目录
|
// 日志保存目录
|
||||||
'path' => LOG_PATH,
|
'path' => LOG_PATH,
|
||||||
// 日志记录级别
|
// 日志记录级别
|
||||||
'level' => ['log'],
|
'level' => ['error', 'warning', 'info', 'notice', 'debug'],
|
||||||
"max_files"=>3,
|
'file_size' => 1024 * 1024 * 2, // 2MB
|
||||||
|
'max_files'=>30,
|
||||||
|
'apart_day' => true,
|
||||||
|
'format' => '[%s][%s] %s',
|
||||||
],
|
],
|
||||||
|
|
||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user