自动推广

This commit is contained in:
wangjinlei
2026-03-18 14:37:23 +08:00
parent 65ba338a7d
commit e3ec1b0ca1
5 changed files with 847 additions and 46 deletions

View File

@@ -1082,6 +1082,7 @@ class EmailClient extends Base
* - min_interval, max_interval (seconds between emails)
* - max_bounce_rate (%), no_repeat_days
* - send_start_hour, send_end_hour (UTC, default 8-22)
* - send_date (Y-m-d计划发送日期有则「今日准备明日发」由定时任务处理无则创建后需手动 startTask)
*/
public function createTask()
{
@@ -1097,16 +1098,17 @@ class EmailClient extends Base
$minInterval = intval($this->request->param('min_interval', 30));
$maxInterval = intval($this->request->param('max_interval', 60));
$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));
$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) {
return jsonError('journal_id and template_id are required');
}
if (empty($expertIds) && empty($field)) {
return jsonError('expert_ids or field is required');
}
// if (empty($expertIds) && empty($field)) {
// return jsonError('expert_ids or field is required');
// }
$tpl = Db::name('mail_template')
->where('template_id', $templateId)
@@ -1123,36 +1125,88 @@ class EmailClient extends Base
$experts = [];
if (!empty($expertIds)) {
// 显式点名的专家,只按 state 过滤ltime 由外部自行控制
$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 {
// 一般情况:不传 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')
->join('t_expert_field ef', 'e.expert_id = ef.expert_id')
->where('e.state', 0)
->where('ef.state', 0);
if ($field) {
$query->where('ef.field', 'like', '%' . $field . '%');
}
if ($majorId) {
$query->where('ef.major_id', $majorId);
}
$experts = $query->field('e.*')->group('e.expert_id')->select();
}
if ($noRepeatDays > 0) {
$cutoff = time() - ($noRepeatDays * 86400);
$experts = array_filter($experts, function ($e) use ($cutoff) {
return intval($e['ltime']) < $cutoff;
// 领域条件:
// 1) 若前端显式传了 field/major_id则优先按传入条件过滤
// 2) 否则按期刊绑定领域major_title在 ef.field 中模糊匹配
$query->where(function ($q) use ($field, $majorId, $majors) {
if ($field !== '') {
$q->where('ef.field', 'like', '%' . $field . '%');
} elseif ($majorId > 0) {
$q->where('ef.major_id', $majorId);
} elseif (!empty($majors)) {
$q->where(function ($qq) use ($majors) {
foreach ($majors as $idx => $title) {
$title = trim($title);
if ($title === '') {
continue;
}
if ($idx === 0) {
$qq->where('ef.field', 'like', '%' . $title . '%');
} else {
$qq->whereOr('ef.field', 'like', '%' . $title . '%');
}
}
});
}
});
$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)) {
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();
$taskId = Db::name('promotion_task')->insertGetId([
'journal_id' => $journalId,
@@ -1172,6 +1226,7 @@ class EmailClient extends Base
'no_repeat_days' => $noRepeatDays,
'send_start_hour' => $sendStartHour,
'send_end_hour' => $sendEndHour,
'send_date' => $sendDateVal,
'ctime' => $now,
'utime' => $now,
]);
@@ -1192,14 +1247,85 @@ class EmailClient extends Base
}
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([
'task_id' => $taskId,
'total_count' => count($experts),
'state' => 0,
'msg' => 'Task created, call startTask to begin sending',
'send_date' => $sendDateVal,
'msg' => $msg,
]);
}
/**
* 为单个任务预生成邮件(可手动或测试用)
* 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
*/
@@ -1223,6 +1349,10 @@ class EmailClient extends Base
if ($task['state'] == 1) {
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([
'state' => 1,