自动推广

This commit is contained in:
wangjinlei
2026-03-27 17:57:25 +08:00
parent 20a68ddc8a
commit 22947a56a4
5 changed files with 625 additions and 145 deletions

View File

@@ -31,13 +31,21 @@ class EmailClient extends Base
->select(); ->select();
foreach ($list as &$item) { foreach ($list as &$item) {
$item['smtp_password'] = '******'; // $item['smtp_password'] = '******';
$item['remaining_today'] = max(0, $item['daily_limit'] - $item['today_sent']); $item['remaining_today'] = max(0, $item['daily_limit'] - $item['today_sent']);
} }
return jsonSuccess($list); 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 * Add SMTP account
*/ */
@@ -546,8 +554,33 @@ class EmailClient extends Base
} }
$report = []; $report = [];
$accountUpdates = [];
foreach ($accounts as $account) { 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]); return jsonSuccess(['report' => $report]);
@@ -628,7 +661,7 @@ class EmailClient extends Base
'content_text' => $body['text'], 'content_text' => $body['text'],
'email_date' => $emailDate, 'email_date' => $emailDate,
'has_attachment' => $hasAttachment ? 1 : 0, 'has_attachment' => $hasAttachment ? 1 : 0,
'is_read' => 0, 'is_read' => mb_substr($subject, 0, 512)=="failure notice"?1:0,
'is_starred' => 0, 'is_starred' => 0,
'is_bounce' => $isBounce ? 1 : 0, 'is_bounce' => $isBounce ? 1 : 0,
'bounce_email' => mb_substr($bounceEmail, 0, 128), 'bounce_email' => mb_substr($bounceEmail, 0, 128),
@@ -668,6 +701,8 @@ class EmailClient extends Base
]; ];
} }
// ==================== Inbox - Read ==================== // ==================== Inbox - Read ====================
/** /**
@@ -739,6 +774,7 @@ class EmailClient extends Base
* Get single email detail * Get single email detail
*/ */
public function getEmailDetail() public function getEmailDetail()
{ {
$inboxId = intval($this->request->param('inbox_id', 0)); $inboxId = intval($this->request->param('inbox_id', 0));
if (!$inboxId) { 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 ==================== // ==================== Inbox - Actions ====================
/** /**
@@ -1266,54 +1422,41 @@ class EmailClient extends Base
->where('state', 0) ->where('state', 0)
->select(); ->select();
} else { } else {
// 一般情况:不传 expert_ids根据期刊实际领域自动选择专家 // 不传 expert_ids根据期刊选定的推广领域journal_promotion_field → expert_fetch自动选择专家
$journal = Db::name('journal')->where('journal_id', $journalId)->find(); $journal = Db::name('journal')->where('journal_id', $journalId)->find();
if (!$journal || empty($journal['issn'])) { if (!$journal) {
return jsonError('Journal or ISSN not found'); return jsonError('Journal not found');
} }
// 期刊绑定的领域major_title与 dailyFetchAll 保持一致 $promotionFields = Db::name('journal_promotion_field')
$majors = Db::name('major_to_journal') ->alias('jpf')
->alias('mtj') ->join('t_expert_fetch ef_fetch', 'ef_fetch.expert_fetch_id = jpf.expert_fetch_id', 'inner')
->join('t_major m', 'm.major_id = mtj.major_id', 'left') ->where('jpf.journal_id', $journalId)
->where('mtj.journal_issn', $journal['issn']) ->where('jpf.state', 0)
->where('mtj.mtj_state', 0) ->where('ef_fetch.state', 0)
->where('m.major_state', 0) ->column('ef_fetch.field');
->column('m.major_title'); $promotionFields = array_unique(array_filter(array_map('trim', $promotionFields)));
$majors = array_unique(array_filter($majors));
$query = Db::name('expert')->alias('e') $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('e.state', 0)
->where('ef.state', 0); ->where('ef.state', 0);
// 领域条件: // 前端显式传了 field 则优先,否则走期刊选定的推广领域
// 1) 若前端显式传了 field/major_id则优先按传入条件过滤 if ($field !== '') {
// 2) 否则按期刊绑定领域major_title在 ef.field 中模糊匹配 $query->where('ef.field', 'like', '%' . $field . '%');
$query->where(function ($q) use ($field, $majorId, $majors) { } elseif (!empty($promotionFields)) {
if ($field !== '') { $query->where(function ($q) use ($promotionFields) {
$q->where('ef.field', 'like', '%' . $field . '%'); foreach ($promotionFields as $idx => $fieldName) {
} elseif ($majorId > 0) { if ($idx === 0) {
$q->where('ef.major_id', $majorId); $q->where('ef.field', 'like', '%' . $fieldName . '%');
} elseif (!empty($majors)) { } else {
$q->where(function ($qq) use ($majors) { $q->whereOr('ef.field', 'like', '%' . $fieldName . '%');
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 . '%');
}
} }
}); }
} });
}); }
// 不频繁发送:在 SQL 中直接使用 ltime + no_repeat_days 过滤
if ($noRepeatDays > 0) { if ($noRepeatDays > 0) {
$cutoff = time() - ($noRepeatDays * 86400); $cutoff = time() - ($noRepeatDays * 86400);
$query->where(function ($q) use ($cutoff) { $query->where(function ($q) use ($cutoff) {
@@ -1321,7 +1464,6 @@ class EmailClient extends Base
}); });
} }
// 去重同一个专家
$experts = $query $experts = $query
->field('e.*') ->field('e.*')
->group('e.expert_id') ->group('e.expert_id')
@@ -1467,6 +1609,125 @@ class EmailClient extends Base
return jsonSuccess($re); 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 * Cron: check bounce emails and update promotion logs accordingly
* Should be called periodically after syncInbox runs. * 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) private function findEligibleExperts($journal, $noRepeatDays, $limit)
{ {
$issn = trim($journal['issn'] ?? ''); $journalId = intval($journal['journal_id']);
$majors = []; $fields = Db::name('journal_promotion_field')
if (!empty($issn)) { ->alias('jpf')
$majors = Db::name('major_to_journal') ->join('t_expert_fetch ef_fetch', 'ef_fetch.expert_fetch_id = jpf.expert_fetch_id', 'inner')
->alias('mtj') ->where('jpf.journal_id', $journalId)
->join('t_major m', 'm.major_id = mtj.major_id', 'left') ->where('jpf.state', 0)
->where('mtj.journal_issn', $issn) ->where('ef_fetch.state', 0)
->where('mtj.mtj_state', 0) ->column('ef_fetch.field');
->where('m.major_state', 0) $fields = array_unique(array_filter(array_map('trim', $fields)));
->where('m.pid', '<>', 0)
->column('m.major_title'); if (empty($fields)) {
$majors = array_unique(array_filter($majors)); return [];
} }
$query = Db::name('expert')->alias('e') $query = Db::name('expert')->alias('e')
@@ -2038,19 +2346,15 @@ class EmailClient extends Base
->where('e.state', 0) ->where('e.state', 0)
->where('ef.state', 0); ->where('ef.state', 0);
if (!empty($majors)) { $query->where(function ($q) use ($fields) {
$query->where(function ($q) use ($majors) { foreach ($fields as $idx => $fieldName) {
foreach ($majors as $idx => $title) { if ($idx === 0) {
$title = trim($title); $q->where('ef.field', 'like', '%' . $fieldName . '%');
if ($title === '') continue; } else {
if ($idx === 0) { $q->whereOr('ef.field', 'like', '%' . $fieldName . '%');
$q->where('ef.field', 'like', '%' . $title . '%');
} else {
$q->whereOr('ef.field', 'like', '%' . $title . '%');
}
} }
}); }
} });
if ($noRepeatDays > 0) { if ($noRepeatDays > 0) {
$cutoff = time() - ($noRepeatDays * 86400); $cutoff = time() - ($noRepeatDays * 86400);
@@ -2066,6 +2370,21 @@ class EmailClient extends Base
->select(); ->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 ==================== // ==================== Internal Methods ====================
/** /**

View File

@@ -242,70 +242,54 @@ class ExpertFinder extends Base
// ==================== Cron / Auto Fetch ==================== // ==================== 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() public function dailyFetchAll()
{ {
$journalId = intval($this->request->param('journal_id', 0)); $perPage = max(10, intval($this->request->param('per_page', 200)));
$perPage = max(10, intval($this->request->param('per_page', 200))); $source = $this->request->param('source', 'pubmed');
$source = $this->request->param('source', 'pubmed'); $minYear = intval($this->request->param('min_year', date('Y') - 3));
$minYear = intval($this->request->param('min_year', date('Y') - 3));
if ($journalId) { $fetchList = Db::name('expert_fetch')
$journals = Db::name('journal')->field("journal_id,issn,title")->where('journal_id', $journalId)->select(); ->where('state', 0)
} else { ->select();
$journals = Db::name('journal')->field("journal_id,issn,title")->where('state', 0)->select();
}
if (empty($journals)) { if (empty($fetchList)) {
return jsonSuccess(['msg' => 'No active journals found', 'queued' => 0]); return jsonSuccess(['msg' => 'No active fetch fields found', 'queued' => 0]);
} }
$queued = 0; $queued = 0;
$skipped = 0; $skipped = 0;
$details = []; $details = [];
$todayStart = strtotime(date('Y-m-d')); $todayStart = strtotime(date('Y-m-d'));
foreach ($journals as $journal) {
$issn = trim($journal['issn'] ?? '');
if (empty($issn)) continue;
$majors = Db::name('major_to_journal') foreach ($fetchList as $item) {
->alias('mtj') $keyword = trim($item['field']);
->join('t_major m', 'm.major_id = mtj.major_id', 'left') $itemSource = trim($item['source'] ?: $source);
->where('mtj.journal_issn', $issn) if ($keyword === '') continue;
->where('mtj.mtj_state', 0)
->where("m.pid", "<>", 0)
->where('m.major_state', 0)
->column('m.major_title');
$majors = array_unique(array_filter($majors)); $fetchLog = $this->service->getFetchLog($keyword, $itemSource);
if (empty($majors)) continue; if ($fetchLog['last_time'] >= $todayStart) {
foreach ($majors as $keyword) { $skipped++;
$keyword = trim($keyword); continue;
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,
];
} }
$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([ return jsonSuccess([
@@ -371,7 +355,7 @@ class ExpertFinder extends Base
], 'FetchExperts'); ], 'FetchExperts');
} }
public function mytest() public function fetchOneField()
{ {
$data = $this->request->post(); $data = $this->request->post();
$rule = new Validate([ $rule = new Validate([

View File

@@ -37,23 +37,19 @@ class ExpertManage extends Base
$data = $this->request->param(); $data = $this->request->param();
$keyword = trim(isset($data['keyword']) ? $data['keyword'] : ''); $keyword = trim(isset($data['keyword']) ? $data['keyword'] : '');
$field = trim(isset($data['field']) ? $data['field'] : ''); $field = trim(isset($data['field']) ? $data['field'] : '');
$majorId = intval(isset($data['major_id']) ? $data['major_id'] : 0);
$state = isset($data['state']) ? $data['state'] : '-1'; $state = isset($data['state']) ? $data['state'] : '-1';
$source = trim(isset($data['source']) ? $data['source'] : ''); $source = trim(isset($data['source']) ? $data['source'] : '');
$page = max(1, intval(isset($data['pageIndex']) ? $data['pageIndex'] : 1)); $page = max(1, intval(isset($data['pageIndex']) ? $data['pageIndex'] : 1));
$pageSize = max(1, intval(isset($data['pageSize']) ? $data['pageSize'] : 20)); $pageSize = max(1, intval(isset($data['pageSize']) ? $data['pageSize'] : 20));
$query = Db::name('expert')->alias('e'); $query = Db::name('expert')->alias('e');
$needJoin = ($field !== '' || $majorId > 0); $needJoin = ($field !== '');
if ($needJoin) { if ($needJoin) {
$query->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner'); $query->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner');
if ($field !== '') { if ($field !== '') {
$query->where('ef.field', 'like', '%' . $field . '%'); $query->where('ef.field', 'like', '%' . $field . '%');
} }
if ($majorId > 0) {
$query->where('ef.major_id', $majorId);
}
$query->group('e.expert_id'); $query->group('e.expert_id');
} }
@@ -338,21 +334,17 @@ class ExpertManage extends Base
{ {
$data = $this->request->param(); $data = $this->request->param();
$field = trim(isset($data['field']) ? $data['field'] : ''); $field = trim(isset($data['field']) ? $data['field'] : '');
$majorId = intval(isset($data['major_id']) ? $data['major_id'] : 0);
$state = isset($data['state']) ? $data['state'] : '-1'; $state = isset($data['state']) ? $data['state'] : '-1';
$keyword = trim(isset($data['keyword']) ? $data['keyword'] : ''); $keyword = trim(isset($data['keyword']) ? $data['keyword'] : '');
$source = trim(isset($data['source']) ? $data['source'] : ''); $source = trim(isset($data['source']) ? $data['source'] : '');
$query = Db::name('expert')->alias('e'); $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'); $query->join('t_expert_field ef', 'ef.expert_id = e.expert_id AND ef.state = 0', 'inner');
if ($field !== '') { if ($field !== '') {
$query->where('ef.field', 'like', '%' . $field . '%'); $query->where('ef.field', 'like', '%' . $field . '%');
} }
if ($majorId > 0) {
$query->where('ef.major_id', $majorId);
}
$query->group('e.expert_id'); $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([]);
}
} }

View File

@@ -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

View File

@@ -36,10 +36,15 @@ class ExpertFinderService
$result = $this->searchViaPubMed($field, $perPage, $minYear, $page); $result = $this->searchViaPubMed($field, $perPage, $minYear, $page);
} }
if(!isset($result['total'])){
return [
"has_more"=>"no"
];
}
$saveResult = $this->saveExperts($result['experts'], $field, $source); $saveResult = $this->saveExperts($result['experts'], $field, $source);
$nextPage = $result['has_more'] ? $page : 0; $nextPage = $result['has_more'] ? $page : $fetchLog['last_page'];
$totalPages = isset($result['total_pages']) ? $result['total_pages'] : 0; $totalPages = $result['total_pages'] ?? $fetchLog['total_pages'];
$this->updateFetchLog($field, $source, $nextPage, $totalPages); $this->updateFetchLog($field, $source, $nextPage, $totalPages);
return [ return [
@@ -48,6 +53,7 @@ class ExpertFinderService
'experts_found' => $result['total'], 'experts_found' => $result['total'],
'saved_new' => $saveResult['inserted'], 'saved_new' => $saveResult['inserted'],
'saved_exist' => $saveResult['existing'], 'saved_exist' => $saveResult['existing'],
'list' => $result['experts'],
'field_enriched' => $saveResult['field_enriched'], 'field_enriched' => $saveResult['field_enriched'],
'has_more' => $result['has_more'], 'has_more' => $result['has_more'],
]; ];
@@ -68,6 +74,8 @@ class ExpertFinderService
$fieldEnrich = 0; $fieldEnrich = 0;
foreach ($experts as $expert) { foreach ($experts as $expert) {
$email = strtolower(trim($expert['email'])); $email = strtolower(trim($expert['email']));
if (empty($email)) { if (empty($email)) {
continue; continue;
@@ -94,6 +102,9 @@ class ExpertFinderService
try { try {
$expertId = Db::name('expert')->insertGetId($insert); $expertId = Db::name('expert')->insertGetId($insert);
$this->enrichExpertField($expertId, $field); $this->enrichExpertField($expertId, $field);
if(isset($expert['papers'])&&is_array($expert['papers'])){
$this->savePaper($expertId, $expert['papers']);
}
$inserted++; $inserted++;
} catch (\Exception $e) { } catch (\Exception $e) {
$existing++; $existing++;
@@ -103,6 +114,25 @@ class ExpertFinderService
return ['inserted' => $inserted, 'existing' => $existing, 'field_enriched' => $fieldEnrich]; 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) public function getFetchLog($field, $source)
{ {
$log = Db::name('expert_fetch') $log = Db::name('expert_fetch')
@@ -519,11 +549,8 @@ class ExpertFinderService
->where('state', 0) ->where('state', 0)
->find(); ->find();
if ($exists) return 0; 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([ Db::name('expert_field')->insert([
'expert_id' => $expertId, 'expert_id' => $expertId,
'major_id' => $major_id,
'field' => mb_substr($field, 0, 128), 'field' => mb_substr($field, 0, 128),
'state' => 0, 'state' => 0,
]); ]);