自动推广
This commit is contained in:
@@ -31,13 +31,21 @@ class EmailClient extends Base
|
||||
->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['smtp_password'] = '******';
|
||||
// $item['smtp_password'] = '******';
|
||||
$item['remaining_today'] = max(0, $item['daily_limit'] - $item['today_sent']);
|
||||
}
|
||||
|
||||
return jsonSuccess($list);
|
||||
}
|
||||
|
||||
public function getAccountsAll(){
|
||||
$list = Db::name('journal_email')
|
||||
->where("state",0)
|
||||
->order('state asc, j_email_id asc')
|
||||
->select();
|
||||
return jsonSuccess($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add SMTP account
|
||||
*/
|
||||
@@ -546,8 +554,33 @@ class EmailClient extends Base
|
||||
}
|
||||
|
||||
$report = [];
|
||||
$accountUpdates = [];
|
||||
foreach ($accounts as $account) {
|
||||
$report[] = $this->doSyncAccount($account);
|
||||
$one = $this->doSyncAccount($account);
|
||||
$report[] = $one;
|
||||
|
||||
$aid = intval($account['j_email_id']);
|
||||
if (!isset($accountUpdates[$aid])) {
|
||||
$accountUpdates[$aid] = [
|
||||
'synced' => 0,
|
||||
'bounced' => 0,
|
||||
'journal_id' => intval($account['journal_id']),
|
||||
];
|
||||
}
|
||||
$accountUpdates[$aid]['synced'] += intval(isset($one['synced']) ? $one['synced'] : 0);
|
||||
$accountUpdates[$aid]['bounced'] += intval(isset($one['bounced']) ? $one['bounced'] : 0);
|
||||
}
|
||||
|
||||
// 发布 SSE 事件:按 j_email_id 维度通知;前端收到后自行刷新列表
|
||||
foreach ($accountUpdates as $aid => $sum) {
|
||||
$this->publishInboxUpdatedEvent($aid, [
|
||||
'type' => 'inbox_updated',
|
||||
'j_email_id' => intval($aid),
|
||||
'journal_id' => intval($sum['journal_id']),
|
||||
'synced' => intval($sum['synced']),
|
||||
'bounced' => intval($sum['bounced']),
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
return jsonSuccess(['report' => $report]);
|
||||
@@ -628,7 +661,7 @@ class EmailClient extends Base
|
||||
'content_text' => $body['text'],
|
||||
'email_date' => $emailDate,
|
||||
'has_attachment' => $hasAttachment ? 1 : 0,
|
||||
'is_read' => 0,
|
||||
'is_read' => mb_substr($subject, 0, 512)=="failure notice"?1:0,
|
||||
'is_starred' => 0,
|
||||
'is_bounce' => $isBounce ? 1 : 0,
|
||||
'bounce_email' => mb_substr($bounceEmail, 0, 128),
|
||||
@@ -668,6 +701,8 @@ class EmailClient extends Base
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================== Inbox - Read ====================
|
||||
|
||||
/**
|
||||
@@ -739,6 +774,7 @@ class EmailClient extends Base
|
||||
* Get single email detail
|
||||
*/
|
||||
public function getEmailDetail()
|
||||
|
||||
{
|
||||
$inboxId = intval($this->request->param('inbox_id', 0));
|
||||
if (!$inboxId) {
|
||||
@@ -785,6 +821,126 @@ class EmailClient extends Base
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE: 订阅收件箱更新事件(按 j_email_id 维度)。
|
||||
* 前端建议按 j_email_id 建立连接,收到 inbox_updated 后自行请求 getInboxList/getUnreadCount。
|
||||
*
|
||||
* URL: /api/email_client/inboxSse?j_email_id=3
|
||||
*/
|
||||
public function inboxSse()
|
||||
{
|
||||
$accountId = intval($this->request->param('j_email_id', 0));
|
||||
|
||||
if (!$accountId) {
|
||||
return jsonError('j_email_id is required');
|
||||
}
|
||||
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_flush();
|
||||
}
|
||||
@ob_implicit_flush(1);
|
||||
@set_time_limit(0);
|
||||
|
||||
$eventFile = $this->getInboxSseEventFile($accountId);
|
||||
$lastMtime = is_file($eventFile) ? filemtime($eventFile) : 0;//记录初始时间
|
||||
|
||||
echo "retry: 3000\n";
|
||||
echo "event: connected\n";
|
||||
echo "data: " . json_encode([
|
||||
'msg' => 'SSE connected',
|
||||
'j_email_id' => $accountId,
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
], JSON_UNESCAPED_UNICODE) . "\n\n";
|
||||
flush();
|
||||
|
||||
// 单连接保持 1.5 分钟,随后由前端自动重连
|
||||
$maxLoop = 90;
|
||||
for ($i = 0; $i < $maxLoop; $i++) {
|
||||
if (connection_aborted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
clearstatcache(true, $eventFile);
|
||||
$mtime = is_file($eventFile) ? filemtime($eventFile) : 0;//新时间
|
||||
if ($mtime > $lastMtime) {
|
||||
$lastMtime = $mtime;
|
||||
$content = @file_get_contents($eventFile);
|
||||
$payload = json_decode($content, true);
|
||||
if (!is_array($payload)) {
|
||||
$payload = [
|
||||
'type' => 'inbox_updated',
|
||||
'j_email_id' => $accountId,
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
echo "event: inbox_updated\n";
|
||||
echo "id: " . $mtime . "\n";
|
||||
echo "data: " . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n\n";
|
||||
flush();
|
||||
} else {
|
||||
// 心跳,避免中间层断开空闲 SSE 连接
|
||||
echo ": ping " . time() . "\n\n";
|
||||
flush();
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
echo "event: disconnected\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE demo: fixed message stream for Vue EventSource testing.
|
||||
* URL: /api/email_client/sseDemo
|
||||
*/
|
||||
public function sseDemo()
|
||||
{
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_flush();
|
||||
}
|
||||
@ob_implicit_flush(1);
|
||||
|
||||
echo "retry: 3000\n";
|
||||
echo "event: connected\n";
|
||||
echo "data: " . json_encode([
|
||||
'msg' => 'SSE connected',
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
], JSON_UNESCAPED_UNICODE) . "\n\n";
|
||||
flush();
|
||||
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
if (connection_aborted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
echo "event: message\n";
|
||||
echo "id: " . $i . "\n";
|
||||
echo "data: " . json_encode([
|
||||
'msg' => 'This is fixed SSE demo content',
|
||||
'index' => $i,
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
], JSON_UNESCAPED_UNICODE) . "\n\n";
|
||||
flush();
|
||||
sleep(2);
|
||||
}
|
||||
|
||||
echo "event: done\n";
|
||||
echo "data: " . json_encode(['msg' => 'SSE demo finished'], JSON_UNESCAPED_UNICODE) . "\n\n";
|
||||
flush();
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==================== Inbox - Actions ====================
|
||||
|
||||
/**
|
||||
@@ -1266,54 +1422,41 @@ class EmailClient extends Base
|
||||
->where('state', 0)
|
||||
->select();
|
||||
} else {
|
||||
// 一般情况:不传 expert_ids,根据期刊实际领域自动选择专家
|
||||
// 不传 expert_ids:根据期刊选定的推广领域(journal_promotion_field → expert_fetch)自动选择专家
|
||||
$journal = Db::name('journal')->where('journal_id', $journalId)->find();
|
||||
if (!$journal || empty($journal['issn'])) {
|
||||
return jsonError('Journal or ISSN not found');
|
||||
if (!$journal) {
|
||||
return jsonError('Journal not found');
|
||||
}
|
||||
|
||||
// 期刊绑定的领域(major_title),与 dailyFetchAll 保持一致
|
||||
$majors = Db::name('major_to_journal')
|
||||
->alias('mtj')
|
||||
->join('t_major m', 'm.major_id = mtj.major_id', 'left')
|
||||
->where('mtj.journal_issn', $journal['issn'])
|
||||
->where('mtj.mtj_state', 0)
|
||||
->where('m.major_state', 0)
|
||||
->column('m.major_title');
|
||||
|
||||
$majors = array_unique(array_filter($majors));
|
||||
$promotionFields = Db::name('journal_promotion_field')
|
||||
->alias('jpf')
|
||||
->join('t_expert_fetch ef_fetch', 'ef_fetch.expert_fetch_id = jpf.expert_fetch_id', 'inner')
|
||||
->where('jpf.journal_id', $journalId)
|
||||
->where('jpf.state', 0)
|
||||
->where('ef_fetch.state', 0)
|
||||
->column('ef_fetch.field');
|
||||
$promotionFields = array_unique(array_filter(array_map('trim', $promotionFields)));
|
||||
|
||||
$query = Db::name('expert')->alias('e')
|
||||
->join('t_expert_field ef', 'e.expert_id = ef.expert_id')
|
||||
->join('t_expert_field ef', 'e.expert_id = ef.expert_id', 'inner')
|
||||
->where('e.state', 0)
|
||||
->where('ef.state', 0);
|
||||
|
||||
// 领域条件:
|
||||
// 1) 若前端显式传了 field/major_id,则优先按传入条件过滤
|
||||
// 2) 否则,按期刊绑定领域(major_title)在 ef.field 中模糊匹配
|
||||
$query->where(function ($q) use ($field, $majorId, $majors) {
|
||||
if ($field !== '') {
|
||||
$q->where('ef.field', 'like', '%' . $field . '%');
|
||||
} elseif ($majorId > 0) {
|
||||
$q->where('ef.major_id', $majorId);
|
||||
} elseif (!empty($majors)) {
|
||||
$q->where(function ($qq) use ($majors) {
|
||||
foreach ($majors as $idx => $title) {
|
||||
$title = trim($title);
|
||||
if ($title === '') {
|
||||
continue;
|
||||
}
|
||||
if ($idx === 0) {
|
||||
$qq->where('ef.field', 'like', '%' . $title . '%');
|
||||
} else {
|
||||
$qq->whereOr('ef.field', 'like', '%' . $title . '%');
|
||||
}
|
||||
// 前端显式传了 field 则优先,否则走期刊选定的推广领域
|
||||
if ($field !== '') {
|
||||
$query->where('ef.field', 'like', '%' . $field . '%');
|
||||
} elseif (!empty($promotionFields)) {
|
||||
$query->where(function ($q) use ($promotionFields) {
|
||||
foreach ($promotionFields as $idx => $fieldName) {
|
||||
if ($idx === 0) {
|
||||
$q->where('ef.field', 'like', '%' . $fieldName . '%');
|
||||
} else {
|
||||
$q->whereOr('ef.field', 'like', '%' . $fieldName . '%');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 不频繁发送:在 SQL 中直接使用 ltime + no_repeat_days 过滤
|
||||
if ($noRepeatDays > 0) {
|
||||
$cutoff = time() - ($noRepeatDays * 86400);
|
||||
$query->where(function ($q) use ($cutoff) {
|
||||
@@ -1321,7 +1464,6 @@ class EmailClient extends Base
|
||||
});
|
||||
}
|
||||
|
||||
// 去重同一个专家
|
||||
$experts = $query
|
||||
->field('e.*')
|
||||
->group('e.expert_id')
|
||||
@@ -1467,6 +1609,125 @@ class EmailClient extends Base
|
||||
return jsonSuccess($re);
|
||||
}
|
||||
|
||||
// ==================== Journal Promotion Field Mapping ====================
|
||||
|
||||
/**
|
||||
* 获取某期刊已选的推广领域
|
||||
* 参数: journal_id
|
||||
*/
|
||||
public function getJournalPromotionFields()
|
||||
{
|
||||
$journalId = intval($this->request->param('journal_id', 0));
|
||||
if (!$journalId) {
|
||||
return jsonError('journal_id is required');
|
||||
}
|
||||
|
||||
$list = Db::name('journal_promotion_field')
|
||||
->alias('jpf')
|
||||
->join('t_expert_fetch ef', 'ef.expert_fetch_id = jpf.expert_fetch_id', 'left')
|
||||
->where('jpf.journal_id', $journalId)
|
||||
->where('jpf.state', 0)
|
||||
->field('jpf.jpf_id, jpf.expert_fetch_id, ef.field, ef.source, ef.state as fetch_state, jpf.ctime')
|
||||
->order('jpf.jpf_id asc')
|
||||
->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['ctime_text'] = $item['ctime'] ? date('Y-m-d H:i:s', $item['ctime']) : '';
|
||||
}
|
||||
|
||||
return jsonSuccess($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置/更新期刊的推广领域(全量覆盖)
|
||||
* 参数: journal_id, fetch_ids (逗号分隔的 expert_fetch_id 列表,如 "1,3,5")
|
||||
*/
|
||||
public function setJournalPromotionFields()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$journalId = intval(isset($data['journal_id']) ? $data['journal_id'] : 0);
|
||||
$fetchIds = trim(isset($data['fetch_ids']) ? $data['fetch_ids'] : '');
|
||||
|
||||
if (!$journalId) {
|
||||
return jsonError('journal_id is required');
|
||||
}
|
||||
|
||||
$newIds = [];
|
||||
if ($fetchIds !== '') {
|
||||
$newIds = array_unique(array_map('intval', explode(',', $fetchIds)));
|
||||
$newIds = array_filter($newIds, function ($v) { return $v > 0; });
|
||||
}
|
||||
|
||||
$currentIds = Db::name('journal_promotion_field')
|
||||
->where('journal_id', $journalId)
|
||||
->where('state', 0)
|
||||
->column('expert_fetch_id');
|
||||
|
||||
$toAdd = array_diff($newIds, $currentIds);
|
||||
$toRemove = array_diff($currentIds, $newIds);
|
||||
|
||||
if (!empty($toRemove)) {
|
||||
Db::name('journal_promotion_field')
|
||||
->where('journal_id', $journalId)
|
||||
->where('expert_fetch_id', 'in', $toRemove)
|
||||
->where('state', 0)
|
||||
->update(['state' => 1]);
|
||||
}
|
||||
|
||||
$now = time();
|
||||
foreach ($toAdd as $fetchId) {
|
||||
$exists = Db::name('journal_promotion_field')
|
||||
->where('journal_id', $journalId)
|
||||
->where('expert_fetch_id', $fetchId)
|
||||
->find();
|
||||
if ($exists) {
|
||||
if ($exists['state'] == 1) {
|
||||
Db::name('journal_promotion_field')
|
||||
->where('jpf_id', $exists['jpf_id'])
|
||||
->update(['state' => 0]);
|
||||
}
|
||||
} else {
|
||||
Db::name('journal_promotion_field')->insert([
|
||||
'journal_id' => $journalId,
|
||||
'expert_fetch_id' => $fetchId,
|
||||
'state' => 0,
|
||||
'ctime' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$finalList = Db::name('journal_promotion_field')
|
||||
->alias('jpf')
|
||||
->join('t_expert_fetch ef', 'ef.expert_fetch_id = jpf.expert_fetch_id', 'left')
|
||||
->where('jpf.journal_id', $journalId)
|
||||
->where('jpf.state', 0)
|
||||
->field('jpf.jpf_id, jpf.expert_fetch_id, ef.field')
|
||||
->select();
|
||||
|
||||
return jsonSuccess([
|
||||
'added' => count($toAdd),
|
||||
'removed' => count($toRemove),
|
||||
'current' => $finalList,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可选的抓取领域(供期刊选择时的下拉列表)
|
||||
* 返回 t_expert_fetch 中 state=0 的所有记录
|
||||
*/
|
||||
public function getAvailableFields()
|
||||
{
|
||||
$list = Db::name('expert_fetch')
|
||||
->where('state', 0)
|
||||
->order('field asc')
|
||||
->select();
|
||||
|
||||
foreach ($list as &$item) {
|
||||
$item['last_time_text'] = $item['last_time'] ? date('Y-m-d H:i:s', $item['last_time']) : '';
|
||||
}
|
||||
|
||||
return jsonSuccess($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个任务预生成邮件(可手动或测试用)
|
||||
@@ -1758,6 +2019,53 @@ class EmailClient extends Base
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function getTaskLogDetail(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"log_id"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
$info = Db::name("promotion_email_log")->where("log_id",$data['log_id'])->find();
|
||||
$expert_info = Db::name("expert")->where("expert_id",$info['expert_id'])->find();
|
||||
$expert_info['fields'] = Db::name("expert_field")->where("expert_id",$info['expert_id'])->where("state",0)->select();
|
||||
|
||||
return jsonSuccess(['log'=>$info,'expert'=>$expert_info]);
|
||||
|
||||
}
|
||||
|
||||
public function cancelTaskLog(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"log_id"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
Db::name("promotion_email_log")->where("log_id",$data['log_id'])->update(['state'=>4]);
|
||||
return jsonSuccess();
|
||||
}
|
||||
|
||||
public function updateTaskLog(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"log_id"=>"require",
|
||||
"subject_prepared"=>"require",
|
||||
"body_prepared"=>"require"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
|
||||
$update['subject_prepared']=$data['subject_prepared'];
|
||||
$update['body_prepared']=$data['body_prepared'];
|
||||
Db::name("promotion_email_log")->where("log_id",$data['log_id'])->update($update);
|
||||
$info = Db::name("promotion_email_log")->where("log_id",$data['log_id'])->find();
|
||||
return jsonSuccess(['log'=>$info]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cron: check bounce emails and update promotion logs accordingly
|
||||
* Should be called periodically after syncInbox runs.
|
||||
@@ -2014,23 +2322,23 @@ class EmailClient extends Base
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据期刊绑定的领域自动筛选合适的专家
|
||||
* 根据期刊选定的推广领域(journal_promotion_field → expert_fetch)筛选合适的专家
|
||||
*/
|
||||
private function findEligibleExperts($journal, $noRepeatDays, $limit)
|
||||
{
|
||||
$issn = trim($journal['issn'] ?? '');
|
||||
$journalId = intval($journal['journal_id']);
|
||||
|
||||
$majors = [];
|
||||
if (!empty($issn)) {
|
||||
$majors = Db::name('major_to_journal')
|
||||
->alias('mtj')
|
||||
->join('t_major m', 'm.major_id = mtj.major_id', 'left')
|
||||
->where('mtj.journal_issn', $issn)
|
||||
->where('mtj.mtj_state', 0)
|
||||
->where('m.major_state', 0)
|
||||
->where('m.pid', '<>', 0)
|
||||
->column('m.major_title');
|
||||
$majors = array_unique(array_filter($majors));
|
||||
$fields = Db::name('journal_promotion_field')
|
||||
->alias('jpf')
|
||||
->join('t_expert_fetch ef_fetch', 'ef_fetch.expert_fetch_id = jpf.expert_fetch_id', 'inner')
|
||||
->where('jpf.journal_id', $journalId)
|
||||
->where('jpf.state', 0)
|
||||
->where('ef_fetch.state', 0)
|
||||
->column('ef_fetch.field');
|
||||
$fields = array_unique(array_filter(array_map('trim', $fields)));
|
||||
|
||||
if (empty($fields)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = Db::name('expert')->alias('e')
|
||||
@@ -2038,19 +2346,15 @@ class EmailClient extends Base
|
||||
->where('e.state', 0)
|
||||
->where('ef.state', 0);
|
||||
|
||||
if (!empty($majors)) {
|
||||
$query->where(function ($q) use ($majors) {
|
||||
foreach ($majors as $idx => $title) {
|
||||
$title = trim($title);
|
||||
if ($title === '') continue;
|
||||
if ($idx === 0) {
|
||||
$q->where('ef.field', 'like', '%' . $title . '%');
|
||||
} else {
|
||||
$q->whereOr('ef.field', 'like', '%' . $title . '%');
|
||||
}
|
||||
$query->where(function ($q) use ($fields) {
|
||||
foreach ($fields as $idx => $fieldName) {
|
||||
if ($idx === 0) {
|
||||
$q->where('ef.field', 'like', '%' . $fieldName . '%');
|
||||
} else {
|
||||
$q->whereOr('ef.field', 'like', '%' . $fieldName . '%');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($noRepeatDays > 0) {
|
||||
$cutoff = time() - ($noRepeatDays * 86400);
|
||||
@@ -2066,6 +2370,21 @@ class EmailClient extends Base
|
||||
->select();
|
||||
}
|
||||
|
||||
private function publishInboxUpdatedEvent($accountId, $payload)
|
||||
{
|
||||
$file = $this->getInboxSseEventFile($accountId);
|
||||
$dir = dirname($file);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0777, true);
|
||||
}
|
||||
@file_put_contents($file, json_encode($payload, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
private function getInboxSseEventFile($accountId)
|
||||
{
|
||||
return ROOT_PATH . 'runtime' . DS . 'sse' . DS . 'inbox_email_' . intval($accountId) . '.json';
|
||||
}
|
||||
|
||||
// ==================== Internal Methods ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,70 +242,54 @@ class ExpertFinder extends Base
|
||||
// ==================== Cron / Auto Fetch ====================
|
||||
|
||||
/**
|
||||
* Daily cron: auto-fetch experts for every journal's fields via queue
|
||||
* 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()
|
||||
{
|
||||
$journalId = intval($this->request->param('journal_id', 0));
|
||||
$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));
|
||||
$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));
|
||||
|
||||
if ($journalId) {
|
||||
$journals = Db::name('journal')->field("journal_id,issn,title")->where('journal_id', $journalId)->select();
|
||||
} else {
|
||||
$journals = Db::name('journal')->field("journal_id,issn,title")->where('state', 0)->select();
|
||||
}
|
||||
$fetchList = Db::name('expert_fetch')
|
||||
->where('state', 0)
|
||||
->select();
|
||||
|
||||
if (empty($journals)) {
|
||||
return jsonSuccess(['msg' => 'No active journals found', 'queued' => 0]);
|
||||
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 ($journals as $journal) {
|
||||
$issn = trim($journal['issn'] ?? '');
|
||||
if (empty($issn)) continue;
|
||||
|
||||
$majors = Db::name('major_to_journal')
|
||||
->alias('mtj')
|
||||
->join('t_major m', 'm.major_id = mtj.major_id', 'left')
|
||||
->where('mtj.journal_issn', $issn)
|
||||
->where('mtj.mtj_state', 0)
|
||||
->where("m.pid", "<>", 0)
|
||||
->where('m.major_state', 0)
|
||||
->column('m.major_title');
|
||||
foreach ($fetchList as $item) {
|
||||
$keyword = trim($item['field']);
|
||||
$itemSource = trim($item['source'] ?: $source);
|
||||
if ($keyword === '') continue;
|
||||
|
||||
$majors = array_unique(array_filter($majors));
|
||||
if (empty($majors)) continue;
|
||||
foreach ($majors as $keyword) {
|
||||
$keyword = trim($keyword);
|
||||
if (empty($keyword)) continue;
|
||||
|
||||
$fetchLog = $this->service->getFetchLog($keyword, $source);
|
||||
if ($fetchLog['last_time'] >= $todayStart) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$delay = $queued * 10;
|
||||
\think\Queue::later($delay, 'app\api\job\FetchExperts@fire', [
|
||||
'field' => $keyword,
|
||||
'source' => $source,
|
||||
'per_page' => $perPage,
|
||||
'min_year' => $minYear,
|
||||
'journal_id' => $journal['journal_id'],
|
||||
], 'FetchExperts');
|
||||
|
||||
$queued++;
|
||||
$details[] = [
|
||||
'journal' => $journal['title'] ?? $journal['journal_id'],
|
||||
'keyword' => $keyword,
|
||||
'delay_s' => $delay,
|
||||
];
|
||||
$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([
|
||||
@@ -371,7 +355,7 @@ class ExpertFinder extends Base
|
||||
], 'FetchExperts');
|
||||
}
|
||||
|
||||
public function mytest()
|
||||
public function fetchOneField()
|
||||
{
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
|
||||
@@ -37,23 +37,19 @@ class ExpertManage extends Base
|
||||
$data = $this->request->param();
|
||||
$keyword = trim(isset($data['keyword']) ? $data['keyword'] : '');
|
||||
$field = trim(isset($data['field']) ? $data['field'] : '');
|
||||
$majorId = intval(isset($data['major_id']) ? $data['major_id'] : 0);
|
||||
$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 !== '' || $majorId > 0);
|
||||
$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 . '%');
|
||||
}
|
||||
if ($majorId > 0) {
|
||||
$query->where('ef.major_id', $majorId);
|
||||
}
|
||||
$query->group('e.expert_id');
|
||||
}
|
||||
|
||||
@@ -338,21 +334,17 @@ class ExpertManage extends Base
|
||||
{
|
||||
$data = $this->request->param();
|
||||
$field = trim(isset($data['field']) ? $data['field'] : '');
|
||||
$majorId = intval(isset($data['major_id']) ? $data['major_id'] : 0);
|
||||
$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 !== '' || $majorId > 0) {
|
||||
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 . '%');
|
||||
}
|
||||
if ($majorId > 0) {
|
||||
$query->where('ef.major_id', $majorId);
|
||||
}
|
||||
$query->group('e.expert_id');
|
||||
}
|
||||
|
||||
@@ -490,4 +482,162 @@ class ExpertManage extends Base
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 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([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,14 +843,14 @@ class Preaccept extends Base
|
||||
}
|
||||
$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();
|
||||
if($p_list&&($p_list[0]['type']>0||$p_list[0]['content']!="")){
|
||||
$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();
|
||||
if($n_list[0]['type']>0||$n_list[0]['content']!=""){
|
||||
$this->addBRow($am_info['article_id'],$data['am_id']);
|
||||
}
|
||||
// $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']!="")){
|
||||
// $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();
|
||||
// if($n_list[0]['type']>0||$n_list[0]['content']!=""){
|
||||
// $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]);
|
||||
// return jsonSuccess([]);
|
||||
//返回数据 20260119 start
|
||||
@@ -872,10 +872,10 @@ class Preaccept extends Base
|
||||
}
|
||||
$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();
|
||||
if($p_list&&($p_list[0]['type']>0||$p_list[0]['content']!="")){
|
||||
$this->addBRow($am_info['article_id'],$p_list[0]['am_id']);
|
||||
}
|
||||
// $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']!="")){
|
||||
// $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]);
|
||||
// return jsonSuccess([]);
|
||||
//返回数据 20260119 start
|
||||
|
||||
@@ -36,10 +36,15 @@ class ExpertFinderService
|
||||
$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 : 0;
|
||||
$totalPages = isset($result['total_pages']) ? $result['total_pages'] : 0;
|
||||
$nextPage = $result['has_more'] ? $page : $fetchLog['last_page'];
|
||||
$totalPages = $result['total_pages'] ?? $fetchLog['total_pages'];
|
||||
$this->updateFetchLog($field, $source, $nextPage, $totalPages);
|
||||
|
||||
return [
|
||||
@@ -48,6 +53,7 @@ class ExpertFinderService
|
||||
'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'],
|
||||
];
|
||||
@@ -68,6 +74,8 @@ class ExpertFinderService
|
||||
$fieldEnrich = 0;
|
||||
|
||||
foreach ($experts as $expert) {
|
||||
|
||||
|
||||
$email = strtolower(trim($expert['email']));
|
||||
if (empty($email)) {
|
||||
continue;
|
||||
@@ -94,6 +102,9 @@ class ExpertFinderService
|
||||
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++;
|
||||
@@ -103,6 +114,25 @@ class ExpertFinderService
|
||||
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')
|
||||
@@ -519,11 +549,8 @@ class ExpertFinderService
|
||||
->where('state', 0)
|
||||
->find();
|
||||
if ($exists) return 0;
|
||||
$major = Db::name("major")->where("major_title",$field)->where("state",0)->find();
|
||||
$major_id = $major ? $major['major_id'] : 0;
|
||||
Db::name('expert_field')->insert([
|
||||
'expert_id' => $expertId,
|
||||
'major_id' => $major_id,
|
||||
'field' => mb_substr($field, 0, 128),
|
||||
'state' => 0,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user