448 lines
15 KiB
PHP
448 lines
15 KiB
PHP
<?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 every journal's fields via queue
|
|
*/
|
|
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));
|
|
|
|
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();
|
|
}
|
|
|
|
if (empty($journals)) {
|
|
return jsonSuccess(['msg' => 'No active journals 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');
|
|
|
|
$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,
|
|
];
|
|
}
|
|
}
|
|
|
|
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 mytest()
|
|
{
|
|
$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),
|
|
]);
|
|
}
|
|
}
|