Compare commits

..

3 Commits

Author SHA1 Message Date
wangjinlei
20a68ddc8a 自动推广 2026-03-23 09:57:45 +08:00
wangjinlei
d1e0f43992 Merge remote-tracking branch 'origin/master' 2026-03-18 14:37:29 +08:00
wangjinlei
e3ec1b0ca1 自动推广 2026-03-18 14:37:23 +08:00
7 changed files with 1353 additions and 64 deletions

View File

@@ -225,6 +225,9 @@ class EmailClient extends Base
$this->resetDailyCountIfNeeded($account); $this->resetDailyCountIfNeeded($account);
if ($account['today_sent'] >= $account['daily_limit']) { if ($account['today_sent'] >= $account['daily_limit']) {
$account = $this->pickSmtpAccount($journalId); $account = $this->pickSmtpAccount($journalId);
if (!$account) { if (!$account) {
@@ -247,6 +250,132 @@ class EmailClient extends Base
} }
} }
/**
* 测试邮件:按指定 mail_template + mail_style 渲染后发送一封到指定邮箱。
* 渲染与正式推广任务一致(使用 PromotionService::renderFromTemplate变量含 expert_* / journal_*)。
*
* Params:
* journal_id (必填)
* template_id (必填)
* to_email (必填)
* style_id (选填,默认 0 表示不套 style 包裹)
* expert_id (选填,有则从库取专家填充变量;无则使用占位假数据)
* vars_json (选填JSON 对象,合并覆盖上述变量)
* j_email_id (选填,指定 SMTP否则自动选有余额的账号)
* skip_quota_count (选填,传 1 发送成功时不增加 today_sent避免测试占额度)
*/
public function sendTemplateStyleTest()
{
$journalId = intval($this->request->param('journal_id', 0));
$templateId = intval($this->request->param('template_id', 0));
$styleId = intval($this->request->param('style_id', 0));
$toEmail = trim($this->request->param('to_email', ''));
$expertId = intval($this->request->param('expert_id', 0));
$varsJson = $this->request->param('vars_json', '');
$accountId = intval($this->request->param('j_email_id', 0));
$skipQuota = intval($this->request->param('skip_quota_count', 0));
if (!$journalId || !$templateId || $toEmail === '') {
return jsonError('journal_id, template_id and to_email are required');
}
$journal = Db::name('journal')->where('journal_id', $journalId)->find();
if (!$journal) {
return jsonError('Journal not found');
}
$service = new PromotionService();
$journalVars = $service->buildJournalVars($journal);
if ($expertId > 0) {
$expert = Db::name('expert')->where('expert_id', $expertId)->find();
if (!$expert) {
return jsonError('expert not found');
}
if (empty($expert['field'])) {
$fieldRow = Db::name('expert_field')
->where('expert_id', $expertId)
->where('state', 0)
->order('expert_field_id asc')
->find();
if ($fieldRow) {
$expert['field'] = $fieldRow['field'];
}
}
$expertVars = $service->buildExpertVars($expert);
} else {
$expertVars = [
'expert_title' => 'Dr.',
'expert_name' => 'Test Recipient',
'expert_email' => $toEmail,
'expert_affiliation' => 'Example University / Hospital',
'expert_field' => 'Clinical Research',
];
}
$vars = array_merge($journalVars, $expertVars);
if ($varsJson !== '' && $varsJson !== null) {
$extra = json_decode($varsJson, true);
if (is_array($extra)) {
$vars = array_merge($vars, $extra);
}
}
$rendered = $service->renderFromTemplate(
$templateId,
$journalId,
json_encode($vars, JSON_UNESCAPED_UNICODE),
$styleId
);
if ($rendered['code'] !== 0) {
return jsonError($rendered['msg']);
}
$subject = $rendered['data']['subject'];
$content = $rendered['data']['body'];
if ($accountId) {
$account = Db::name('journal_email')
->where('j_email_id', $accountId)
->where('journal_id', $journalId)
->where('state', 0)
->find();
} else {
$account = $this->pickSmtpAccount($journalId);
}
if (!$account) {
return jsonError('No available SMTP account (all disabled or daily limit reached)');
}
$this->resetDailyCountIfNeeded($account);
if (!$skipQuota && $account['today_sent'] >= $account['daily_limit']) {
$account = $this->pickSmtpAccount($journalId);
if (!$account) {
return jsonError('All SMTP accounts have reached daily limit');
}
}
$result = $this->doSendEmail($account, $toEmail, $subject, $content);
if ($result['status'] === 1) {
if (!$skipQuota) {
Db::name('journal_email')->where('j_email_id', $account['j_email_id'])->setInc('today_sent');
}
return jsonSuccess([
'msg' => 'Test email sent',
'from' => $account['smtp_user'],
'to' => $toEmail,
'j_email_id' => $account['j_email_id'],
'subject_sent' => $subject,
]);
}
return jsonError('Send failed: ' . $result['data']);
}
/** /**
* Send batch emails to multiple experts * Send batch emails to multiple experts
* Params: journal_id, expert_ids (comma separated), * Params: journal_id, expert_ids (comma separated),
@@ -1082,6 +1211,7 @@ class EmailClient extends Base
* - min_interval, max_interval (seconds between emails) * - min_interval, max_interval (seconds between emails)
* - max_bounce_rate (%), no_repeat_days * - max_bounce_rate (%), no_repeat_days
* - send_start_hour, send_end_hour (UTC, default 8-22) * - send_start_hour, send_end_hour (UTC, default 8-22)
* - send_date (Y-m-d计划发送日期有则「今日准备明日发」由定时任务处理无则创建后需手动 startTask)
*/ */
public function createTask() public function createTask()
{ {
@@ -1097,17 +1227,23 @@ class EmailClient extends Base
$minInterval = intval($this->request->param('min_interval', 30)); $minInterval = intval($this->request->param('min_interval', 30));
$maxInterval = intval($this->request->param('max_interval', 60)); $maxInterval = intval($this->request->param('max_interval', 60));
$maxBounceRate = intval($this->request->param('max_bounce_rate', 5)); $maxBounceRate = intval($this->request->param('max_bounce_rate', 5));
$noRepeatDays = intval($this->request->param('no_repeat_days', 30)); $noRepeatDays = intval($this->request->param('no_repeat_days', 7));
$sendStartHour = intval($this->request->param('send_start_hour', 8)); $sendStartHour = intval($this->request->param('send_start_hour', 8));
$sendEndHour = intval($this->request->param('send_end_hour', 22)); $sendEndHour = intval($this->request->param('send_end_hour', 22));
$sendDate = trim($this->request->param('send_date', date("Y-m-d",strtotime('+1 day'))));
if (!$journalId || !$templateId) { if (!$journalId || !$templateId) {
return jsonError('journal_id and template_id are required'); return jsonError('journal_id and template_id are required');
} }
if (empty($expertIds) && empty($field)) { $journal_info = $this->journal_obj->where("journal_id",$journalId)->find();
return jsonError('expert_ids or field is required'); // if (empty($expertIds) && empty($field)) {
// return jsonError('expert_ids or field is required');
// }
$templateId = ($templateId == 0) ? intval($journal_info['default_template_id']) : $templateId;
$styleId = ($styleId == 0) ? intval($journal_info['default_style_id']) : $styleId;
if($templateId==0){
return jsonError("template is not set!");
} }
$tpl = Db::name('mail_template') $tpl = Db::name('mail_template')
->where('template_id', $templateId) ->where('template_id', $templateId)
->where('journal_id', $journalId) ->where('journal_id', $journalId)
@@ -1123,36 +1259,88 @@ class EmailClient extends Base
$experts = []; $experts = [];
if (!empty($expertIds)) { if (!empty($expertIds)) {
// 显式点名的专家,只按 state 过滤ltime 由外部自行控制
$ids = array_map('intval', explode(',', $expertIds)); $ids = array_map('intval', explode(',', $expertIds));
$experts = Db::name('expert')->where('expert_id', 'in', $ids)->where('state', 0)->select(); $experts = Db::name('expert')
->where('expert_id', 'in', $ids)
->where('state', 0)
->select();
} else { } else {
// 一般情况:不传 expert_ids根据期刊实际领域自动选择专家
$journal = Db::name('journal')->where('journal_id', $journalId)->find();
if (!$journal || empty($journal['issn'])) {
return jsonError('Journal or ISSN 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));
$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')
->where('e.state', 0) ->where('e.state', 0)
->where('ef.state', 0); ->where('ef.state', 0);
if ($field) { // 领域条件:
$query->where('ef.field', 'like', '%' . $field . '%'); // 1) 若前端显式传了 field/major_id则优先按传入条件过滤
} // 2) 否则按期刊绑定领域major_title在 ef.field 中模糊匹配
if ($majorId) { $query->where(function ($q) use ($field, $majorId, $majors) {
$query->where('ef.major_id', $majorId); if ($field !== '') {
} $q->where('ef.field', 'like', '%' . $field . '%');
} elseif ($majorId > 0) {
$experts = $query->field('e.*')->group('e.expert_id')->select(); $q->where('ef.major_id', $majorId);
} } elseif (!empty($majors)) {
$q->where(function ($qq) use ($majors) {
if ($noRepeatDays > 0) { foreach ($majors as $idx => $title) {
$cutoff = time() - ($noRepeatDays * 86400); $title = trim($title);
$experts = array_filter($experts, function ($e) use ($cutoff) { if ($title === '') {
return intval($e['ltime']) < $cutoff; continue;
}
if ($idx === 0) {
$qq->where('ef.field', 'like', '%' . $title . '%');
} else {
$qq->whereOr('ef.field', 'like', '%' . $title . '%');
}
}
});
}
}); });
$experts = array_values($experts);
// 不频繁发送:在 SQL 中直接使用 ltime + no_repeat_days 过滤
if ($noRepeatDays > 0) {
$cutoff = time() - ($noRepeatDays * 86400);
$query->where(function ($q) use ($cutoff) {
$q->where('e.ltime', 0)->whereOr('e.ltime', '<', $cutoff);
});
}
// 去重同一个专家
$experts = $query
->field('e.*')
->group('e.expert_id')
->limit(100)
->select();
} }
if (empty($experts)) { if (empty($experts)) {
return jsonError('No eligible experts found (all may have been promoted recently)'); return jsonError('No eligible experts found (all may have been promoted recently)');
} }
$sendDateVal = null;
if ($sendDate !== '') {
$ts = strtotime($sendDate);
if ($ts !== false) {
$sendDateVal = date('Y-m-d', $ts);
}
}
$now = time(); $now = time();
$taskId = Db::name('promotion_task')->insertGetId([ $taskId = Db::name('promotion_task')->insertGetId([
'journal_id' => $journalId, 'journal_id' => $journalId,
@@ -1172,6 +1360,7 @@ class EmailClient extends Base
'no_repeat_days' => $noRepeatDays, 'no_repeat_days' => $noRepeatDays,
'send_start_hour' => $sendStartHour, 'send_start_hour' => $sendStartHour,
'send_end_hour' => $sendEndHour, 'send_end_hour' => $sendEndHour,
'send_date' => $sendDateVal,
'ctime' => $now, 'ctime' => $now,
'utime' => $now, 'utime' => $now,
]); ]);
@@ -1192,14 +1381,159 @@ class EmailClient extends Base
} }
Db::name('promotion_email_log')->insertAll($logs); Db::name('promotion_email_log')->insertAll($logs);
$msg = 'Task created, call startTask to begin sending';
if ($sendDateVal) {
$msg = 'Task created for send_date=' . $sendDateVal . ', will be prepared by cron and triggered on that day';
}
return jsonSuccess([ return jsonSuccess([
'task_id' => $taskId, 'task_id' => $taskId,
'total_count' => count($experts), 'total_count' => count($experts),
'state' => 0, 'state' => 0,
'msg' => 'Task created, call startTask to begin sending', 'send_date' => $sendDateVal,
'msg' => $msg,
]); ]);
} }
/**设置期刊默认
* @return \think\response\Json
* @throws \think\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
* @throws \think\exception\PDOException
*/
public function setDefaultPromotion(){
$data = $this->request->post();
$rule = new Validate([
"journal_id"=>"require",
"default_template_id"=>"require",
"default_style_id"=>"require",
"start_promotion"=>"require"
]);
if(!$rule->check($data)){
return jsonError($rule->getError());
}
$update['default_template_id']=$data['default_template_id'];
$update['default_style_id'] = $data['default_style_id'];
$update['start_promotion'] = $data['start_promotion'];
$this->journal_obj->where("journal_id",$data['journal_id'])->update($update);
$info = $this->journal_obj->where("journal_id",$data['journal_id'])->find();
return jsonSuccess(['journal'=>$info]);
}
public function getPromotionJournalList(){
$data = $this->request->post();
$rule = new Validate([
"user_id"=>"require"
]);
if(!$rule->check($data)){
return jsonError($rule->getError());
}
$journals = $this->journal_obj
->field("t_journal.journal_id,t_journal.title,t_journal.issn,t_journal.default_template_id,t_journal.default_style_id,t_journal.start_promotion")
->where("editor_id",$data['user_id'])->where("state",0)->select();
//丰富期刊内容
return jsonSuccess(["list"=>$journals]);
}
public function getPromotionJournalDetail(){
$data = $this->request->post();
$rule = new Validate([
"journal_id"=>"require"
]);
if(!$rule->check($data)){
return jsonError($rule->getError());
}
$info = $this->journal_obj->where("journal_id",$data['journal_id'])->find();
$template_info = [];
if($info['default_style_id']!==0){
$template_info = Db::name("mail_template")->where("template_id",$info['default_template_id'])->find();
}
$style_info = [];
if($info['default_style_id']!==0){
$style_info = Db::name("mail_style")->where("style_id",$info['default_style_id'])->find();
}
$re['journal'] = $info;
$re['template'] = $template_info;
$re['style'] = $style_info;
return jsonSuccess($re);
}
/**
* 为单个任务预生成邮件(可手动或测试用)
* Params: task_id
*/
public function prepareTask()
{
$taskId = intval($this->request->param('task_id', 0));
if (!$taskId) {
return jsonError('task_id is required');
}
$service = new PromotionService();
$result = $service->prepareTask($taskId);
if ($result['error']) {
return jsonError($result['error']);
}
return jsonSuccess($result);
}
/**
* 定时任务:为指定日期的任务预生成邮件(默认明天)
* 建议每天 22:00 执行curl .../EmailClient/prepareTasksForDate 或 prepareTasksForDate?date=2026-03-12
*/
public function prepareTasksForDate()
{
$date = trim($this->request->param('date', ''));
if ($date === '') {
$date = date('Y-m-d', strtotime('+1 day'));
} else {
$ts = strtotime($date);
if ($ts === false) {
return jsonError('date invalid, use Y-m-d');
}
$date = date('Y-m-d', $ts);
}
$service = new PromotionService();
$result = $service->prepareTasksForDate($date);
return jsonSuccess($result);
}
/**
* 定时任务:触发指定日期的已准备任务开始发送(默认今天)
* 建议每天 8:00 执行curl .../EmailClient/triggerTasksForDate 或 triggerTasksForDate?date=2026-03-12
*/
public function triggerTasksForDate()
{
$date = trim($this->request->param('date', ''));
if ($date === '') {
$date = date('Y-m-d');
} else {
$ts = strtotime($date);
if ($ts === false) {
return jsonError('date invalid, use Y-m-d');
}
$date = date('Y-m-d', $ts);
}
$service = new PromotionService();
$result = $service->startTasksForDate($date);
return jsonSuccess($result);
}
/** /**
* Start or resume a promotion task * Start or resume a promotion task
*/ */
@@ -1223,6 +1557,10 @@ class EmailClient extends Base
if ($task['state'] == 1) { if ($task['state'] == 1) {
return jsonError('Task is already running'); return jsonError('Task is already running');
} }
// state=0 草稿 或 state=5 已准备 均可启动
if ($task['state'] != 0 && $task['state'] != 5) {
return jsonError('Task can only be started when state is draft(0) or prepared(5), current: ' . $task['state']);
}
Db::name('promotion_task')->where('task_id', $taskId)->update([ Db::name('promotion_task')->where('task_id', $taskId)->update([
'state' => 1, 'state' => 1,
@@ -1469,6 +1807,265 @@ class EmailClient extends Base
return jsonSuccess(['bounce_logs_updated' => $updated]); return jsonSuccess(['bounce_logs_updated' => $updated]);
} }
// ==================== Cron: Auto Generate Daily Tasks ====================
/**
* 每日自动生成推广任务(由 Linux crontab 调用)
*
* 逻辑:
* 1. 查询所有 start_promotion=1 且 state=0 的期刊
* 2. 对每个期刊,检查明天是否已存在任务,避免重复
* 3. 用 default_template_id / default_style_id 创建 promotion_task
* 4. 根据期刊绑定的领域自动筛选专家排除近7天已联系、排除非正常状态
* 5. 每个期刊默认生成 100 封邮件
*
* crontab 示例每天凌晨1点执行
* 0 1 * * * curl -s "https://your-domain.com/api/email_client/cronDailyCreateTasks" >> /var/log/promotion_cron.log 2>&1
*
* 配合已有定时任务:
* 22:00 prepareTasksForDate — 预渲染邮件内容
* 08:00 triggerTasksForDate — 开始发送
*/
public function cronDailyCreateTasks()
{
set_time_limit(120);
$sendDate = date('Y-m-d', strtotime('+1 day'));
$dailyLimit = 100;
$noRepeatDays = 7;
$journals = Db::name('journal')
->where('start_promotion', 1)
->where('state', 0)
->where('default_template_id', '>', 0)
->select();
if (empty($journals)) {
return jsonSuccess(['msg' => 'No journals with promotion enabled', 'created' => 0]);
}
$created = 0;
$skipped = 0;
$errors = [];
$details = [];
foreach ($journals as $journal) {
$journalId = $journal['journal_id'];
$templateId = intval($journal['default_template_id']);
$styleId = intval(isset($journal['default_style_id']) ? $journal['default_style_id'] : 0);
$existTask = Db::name('promotion_task')
->where('journal_id', $journalId)
->where('send_date', $sendDate)
->where('state', 'in', [0, 1, 5])
->find();
if ($existTask) {
$skipped++;
continue;
}
$tpl = Db::name('mail_template')
->where('template_id', $templateId)
->where('journal_id', $journalId)
->where('state', 0)
->find();
if (!$tpl) {
$errors[] = 'journal_id=' . $journalId . ': template_id=' . $templateId . ' not found';
continue;
}
$smtpCount = Db::name('journal_email')
->where('journal_id', $journalId)
->where('state', 0)
->count();
if ($smtpCount == 0) {
$errors[] = 'journal_id=' . $journalId . ': no active SMTP account';
continue;
}
$experts = $this->findEligibleExperts($journal, $noRepeatDays, $dailyLimit);
if (empty($experts)) {
$details[] = [
'journal_id' => $journalId,
'title' => $journal['title'],
'status' => 'no_experts',
];
continue;
}
$now = time();
$scene = $tpl['scene'] ?? 'promotion';
$taskId = Db::name('promotion_task')->insertGetId([
'journal_id' => $journalId,
'template_id' => $templateId,
'style_id' => $styleId,
'scene' => $scene,
'task_name' => 'Auto-' . ($journal['title'] ?? $journalId) . '-' . $sendDate,
'total_count' => count($experts),
'sent_count' => 0,
'fail_count' => 0,
'bounce_count' => 0,
'state' => 0,
'smtp_ids' => '',
'min_interval' => 30,
'max_interval' => 60,
'max_bounce_rate' => 5,
'no_repeat_days' => $noRepeatDays,
'send_start_hour' => 8,
'send_end_hour' => 22,
'send_date' => $sendDate,
'ctime' => $now,
'utime' => $now,
]);
$logs = [];
foreach ($experts as $expert) {
$logs[] = [
'task_id' => $taskId,
'expert_id' => intval($expert['expert_id']),
'j_email_id' => 0,
'email_to' => $expert['email'],
'subject' => '',
'state' => 0,
'error_msg' => '',
'send_time' => 0,
'ctime' => $now,
];
}
Db::name('promotion_email_log')->insertAll($logs);
$created++;
$details[] = [
'journal_id' => $journalId,
'title' => $journal['title'],
'task_id' => $taskId,
'expert_count' => count($experts),
'send_date' => $sendDate,
];
}
return jsonSuccess([
'send_date' => $sendDate,
'created' => $created,
'skipped' => $skipped,
'errors' => $errors,
'details' => $details,
]);
}
/**
* 每日自动 prepare预生成 subject/body供 crontab 调用
*
* 默认准备明天 send_date 的任务;可传 date=Y-m-d
* 建议每天 22:00 执行curl .../EmailClient/cronDailyPrepareTasks
*/
public function cronDailyPrepareTasks()
{
$date = trim($this->request->param('date', ''));
if ($date === '') {
$date = date('Y-m-d', strtotime('+1 day'));
} else {
$ts = strtotime($date);
if ($ts === false) {
return jsonError('date invalid, use Y-m-d');
}
$date = date('Y-m-d', $ts);
}
$service = new PromotionService();
$result = $service->prepareTasksForDate($date);
return jsonSuccess($result);
}
/**
* 每日自动触发发送(把 prepared 的任务启动),供 crontab 调用
*
* 默认触发今天 send_date 的任务;可传 date=Y-m-d
* 会先对当天 state=0 的任务做一次补 prepare再启动所有 state=5 的任务
* 建议每天 08:00 执行curl .../EmailClient/cronDailyTriggerTasks
*/
public function cronDailyTriggerTasks()
{
$date = trim($this->request->param('date', ''));
if ($date === '') {
$date = date('Y-m-d');
} else {
$ts = strtotime($date);
if ($ts === false) {
return jsonError('date invalid, use Y-m-d');
}
$date = date('Y-m-d', $ts);
}
$service = new PromotionService();
$result = $service->startTasksForDate($date);
return jsonSuccess($result);
}
public function act_task(){
$service = new PromotionService();
$res = $service->processNextEmail(2);
return jsonSuccess(['rr'=>$res]);
}
/**
* 根据期刊绑定的领域自动筛选合适的专家
*/
private function findEligibleExperts($journal, $noRepeatDays, $limit)
{
$issn = trim($journal['issn'] ?? '');
$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));
}
$query = Db::name('expert')->alias('e')
->join('t_expert_field ef', 'e.expert_id = ef.expert_id', 'inner')
->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 . '%');
}
}
});
}
if ($noRepeatDays > 0) {
$cutoff = time() - ($noRepeatDays * 86400);
$query->where(function ($q) use ($cutoff) {
$q->where('e.ltime', 0)->whereOr('e.ltime', '<', $cutoff);
});
}
return $query
->field('e.*')
->group('e.expert_id')
->limit($limit)
->select();
}
// ==================== Internal Methods ==================== // ==================== Internal Methods ====================
/** /**

View File

@@ -0,0 +1,493 @@
<?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'] : '');
$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);
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 ($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'] : '');
$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) {
$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 ($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,
]);
}
}
}

View File

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

View File

@@ -118,6 +118,14 @@ class MailTemplate extends Base
return jsonSuccess(['list' => $list]); 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 * Create or update a global mail style
* 当前 style 表字段: * 当前 style 表字段:

View File

@@ -13,22 +13,22 @@ class PromotionSend
$service = new PromotionService(); $service = new PromotionService();
if (!$taskId) { if (!$taskId) {
$service->log('[PromotionSend] missing task_id, job deleted'); // $service->log('[PromotionSend] missing task_id, job deleted');
$job->delete(); $job->delete();
return; return;
} }
try { // try {
$result = $service->processNextEmail($taskId); $result = $service->processNextEmail($taskId);
$service->log('[PromotionSend] task=' . $taskId . ' result=' . json_encode($result)); // $service->log('[PromotionSend] task=' . $taskId . ' result=' . json_encode($result));
if (!empty($result['done'])) { // if (!empty($result['done'])) {
$reason = isset($result['reason']) ? $result['reason'] : ''; // $reason = isset($result['reason']) ? $result['reason'] : '';
$service->log('[PromotionSend] task=' . $taskId . ' finished, reason=' . $reason); // $service->log('[PromotionSend] task=' . $taskId . ' finished, reason=' . $reason);
} // }
} catch (\Exception $e) { // } catch (\Exception $e) {
$service->log('[PromotionSend] task=' . $taskId . ' exception=' . $e->getMessage()); // $service->log('[PromotionSend] task=' . $taskId . ' exception=' . $e->getMessage());
} // }
$job->delete(); $job->delete();
} }

View File

@@ -519,10 +519,11 @@ 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' => 0, 'major_id' => $major_id,
'field' => mb_substr($field, 0, 128), 'field' => mb_substr($field, 0, 128),
'state' => 0, 'state' => 0,
]); ]);

View File

@@ -80,33 +80,43 @@ class PromotionService
return ['done' => false, 'reason' => 'no_smtp_quota', 'retry_in' => 600]; return ['done' => false, 'reason' => 'no_smtp_quota', 'retry_in' => 600];
} }
$journal = Db::name('journal')->where('journal_id', $task['journal_id'])->find(); // 优先使用预生成内容;无则现场渲染
$expertVars = $this->buildExpertVars($expert); $subject = '';
$journalVars = $this->buildJournalVars($journal); $body = '';
$vars = array_merge($journalVars, $expertVars); $hasPrepared = !empty($logEntry['subject_prepared']) && !empty($logEntry['body_prepared']);
$rendered = $this->renderFromTemplate( if ($hasPrepared) {
$task['template_id'], $subject = $logEntry['subject_prepared'];
$task['journal_id'], $body = $logEntry['body_prepared'];
json_encode($vars, JSON_UNESCAPED_UNICODE), } else {
$task['style_id'] $journal = Db::name('journal')->where('journal_id', $task['journal_id'])->find();
); $expertVars = $this->buildExpertVars($expert);
$journalVars = $this->buildJournalVars($journal);
$vars = array_merge($journalVars, $expertVars);
if ($rendered['code'] !== 0) { $rendered = $this->renderFromTemplate(
Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([ $task['template_id'],
'state' => 2, $task['journal_id'],
'error_msg' => 'Template render failed: ' . $rendered['msg'], json_encode($vars, JSON_UNESCAPED_UNICODE),
'send_time' => time(), $task['style_id']
]); );
Db::name('promotion_task')->where('task_id', $taskId)->setInc('fail_count');
Db::name('promotion_task')->where('task_id', $taskId)->update(['utime' => time()]); if ($rendered['code'] !== 0) {
$this->enqueueNextEmail($taskId, 2); Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([
return ['done' => false, 'failed' => $logEntry['email_to'], 'reason' => 'template_error']; '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'];
} }
$subject = $rendered['data']['subject'];
$body = $rendered['data']['body'];
$result = $this->doSendEmail($account, $logEntry['email_to'], $subject, $body); $result = $this->doSendEmail($account, $logEntry['email_to'], $subject, $body);
$now = time(); $now = time();
@@ -118,7 +128,7 @@ class PromotionService
'send_time' => $now, 'send_time' => $now,
]); ]);
Db::name('journal_email')->where('j_email_id', $account['j_email_id'])->setInc('today_sent'); 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(['ltime' => $now]); 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'); Db::name('promotion_task')->where('task_id', $taskId)->setInc('sent_count');
} else { } else {
Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([ Db::name('promotion_email_log')->where('log_id', $logEntry['log_id'])->update([
@@ -144,6 +154,165 @@ class PromotionService
]; ];
} }
// ==================== 准备与触发(今日准备、明日发送) ====================
/**
* 为指定任务预生成所有待发邮件的 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 ==================== // ==================== Queue ====================
public function enqueueNextEmail($taskId, $delay = 0) public function enqueueNextEmail($taskId, $delay = 0)
@@ -270,8 +439,8 @@ class PromotionService
if ($styleId) { if ($styleId) {
$style = Db::name('mail_style')->where('style_id', $styleId)->where('state', 0)->find(); $style = Db::name('mail_style')->where('style_id', $styleId)->where('state', 0)->find();
if ($style) { if ($style) {
$header = $style['header_html'] ?? ''; $header = $style['header_html'] ? $this->renderVars($style['header_html'],$vars):'';
$footer = $style['footer_html'] ?? ''; $footer = $style['footer_html'] ? $this->renderVars($style['footer_html'],$vars): '';
$finalBody = $header . $body . $footer; $finalBody = $header . $body . $footer;
} }
} }
@@ -282,20 +451,33 @@ class PromotionService
public function buildExpertVars($expert) public function buildExpertVars($expert)
{ {
return [ return [
'name' => $expert['name'] ?? '', 'expert_title' => "Ph.D",
'email' => $expert['email'] ?? '', 'expert_name' => $expert['name'] ?? '',
'affiliation' => $expert['affiliation'] ?? '', 'expert_email' => $expert['email'] ?? '',
'field' => $expert['field'] ?? '', 'expert_affiliation' => $expert['affiliation'] ?? '',
'expert_field' => $expert['field'] ?? '',
]; ];
} }
public function buildJournalVars($journal) public function buildJournalVars($journal)
{ {
if (!$journal) return []; if (!$journal) return [];
$zb = Db::name("board_to_journal")
->where("journal_id",$journal['journal_id'])
->where("state",0)
->where('type',0)
->find();
return [ return [
'journal_title' => $journal['title'] ?? '', 'journal_name' => $journal['title'] ?? '',
'journal_abbr' => $journal['jabbr'] ?? '', 'journal_abbr' => $journal['jabbr'] ?? '',
'journal_url' => $journal['website'] ?? '', '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"))
]; ];
} }