| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- <?php
- namespace app\admin\controller;
- use app\admin\model\general\Lang as LangModel;
- use app\common\controller\Backend;
- use app\common\model\EmailBroadcast as EmailBroadcastModel;
- use app\common\model\EmailBroadcastItem as EmailBroadcastItemModel;
- use app\common\model\EmailSubscribe as EmailSubscribeModel;
- use app\common\model\EmailSubscribeLog as EmailSubscribeLogModel;
- use app\common\model\EmailTemplate as EmailTemplateModel;
- use think\facade\Db;
- use think\facade\Validate;
- class Emailbroadcast extends Backend
- {
- protected $modelClass = null;
- protected $modelValidate = false;
- protected function initialize()
- {
- parent::initialize();
- $this->modelClass = new EmailBroadcastModel();
- $this->assign('statusList', $this->modelClass->getStatusList());
- $this->assign('itemStatusList', (new EmailBroadcastItemModel())->getStatusList());
- $this->assign('langList', LangModel::getEnabledList());
- $this->assign('defaultLang', LangModel::getDefaultMark());
- $this->assign('templateList', EmailTemplateModel::where(['type' => 'marketing_broadcast', 'status' => 1])
- ->order('listorder', 'asc')
- ->order('id', 'asc')
- ->select());
- if (in_array(strtolower($this->request->action()), ['add', 'edit', 'items'], true)) {
- $config = $this->view->config ?: [];
- $config['jsname'] = 'backend/emailbroadcast/' . strtolower($this->request->action());
- $this->view->config = $config;
- $this->assign('config', $config);
- }
- }
- public function index()
- {
- if ($this->request->isAjax()) {
- $page = (int)$this->request->param('page/d', 1);
- $limit = (int)$this->request->param('limit/d', 20);
- $sort = (string)$this->request->param('sort/s', 'id');
- $order = (string)$this->request->param('order/s', 'desc');
- $name = trim((string)$this->request->param('name/s', ''));
- $lang = trim((string)$this->request->param('lang/s', ''));
- $status = $this->request->param('status/s', '');
- $templateId = (int)$this->request->param('template_id/d', 0);
- $query = $this->modelClass;
- if ($name !== '') {
- $query = $query->whereLike('name|subject', '%' . $name . '%');
- }
- if ($lang !== '') {
- $query = $query->where('lang', strtolower($lang));
- }
- if ($status !== '') {
- $query = $query->where('status', (int)$status);
- }
- if ($templateId > 0) {
- $query = $query->where('template_id', $templateId);
- }
- $count = $query->count();
- $data = $query->order($sort, $order)->page($page, $limit)->select();
- foreach ($data as $row) {
- $row->status_text = $this->modelClass->getStatusList()[(int)$row->status] ?? $row->status;
- }
- return json(['code' => 0, 'count' => $count, 'data' => $data]);
- }
- return $this->fetch();
- }
- public function add()
- {
- if ($this->request->isPost()) {
- $this->token();
- $params = $this->buildBroadcastParams();
- $recipientRows = $this->getSubscribersForBroadcast($params);
- if (empty($recipientRows)) {
- $this->error('未筛选到可发送的订阅用户');
- }
- Db::startTrans();
- try {
- $now = time();
- $params['recipient_count'] = count($recipientRows);
- $params['success_count'] = 0;
- $params['fail_count'] = 0;
- $params['status'] = 0;
- $params['createtime'] = $now;
- $params['updatetime'] = $now;
- $this->modelClass->save($params);
- $items = [];
- foreach ($recipientRows as $recipient) {
- $items[] = [
- 'broadcast_id' => $this->modelClass->id,
- 'subscribe_id' => (int)$recipient['id'],
- 'email' => (string)$recipient['email'],
- 'lang' => (string)$recipient['lang'],
- 'status' => 0,
- 'response_code' => '',
- 'response_msg' => '待发送',
- 'retry_count' => 0,
- 'send_time' => 0,
- 'createtime' => $now,
- 'updatetime' => $now,
- ];
- }
- (new EmailBroadcastItemModel())->saveAll($items);
- Db::commit();
- } catch (\Throwable $e) {
- Db::rollback();
- $this->error('创建群发任务失败:' . $e->getMessage());
- }
- $this->success('群发任务创建成功', url('Emailbroadcast/index'));
- }
- return $this->fetch();
- }
- public function edit()
- {
- $id = (int)$this->request->param('id/d', 0);
- $row = $this->modelClass->find($id);
- if (!$row) {
- $this->error('群发任务不存在');
- }
- if ($this->request->isPost()) {
- $this->token();
- if ((int)$row->status !== 0) {
- $this->error('仅待发送任务支持编辑');
- }
- $params = $this->buildBroadcastParams();
- $recipientRows = $this->getSubscribersForBroadcast($params);
- if (empty($recipientRows)) {
- $this->error('未筛选到可发送的订阅用户');
- }
- Db::startTrans();
- try {
- $now = time();
- $params['recipient_count'] = count($recipientRows);
- $params['success_count'] = 0;
- $params['fail_count'] = 0;
- $params['status'] = 0;
- $params['updatetime'] = $now;
- $row->save($params);
- Db::name('email_broadcast_item')->where('broadcast_id', $row->id)->delete();
- $items = [];
- foreach ($recipientRows as $recipient) {
- $items[] = [
- 'broadcast_id' => $row->id,
- 'subscribe_id' => (int)$recipient['id'],
- 'email' => (string)$recipient['email'],
- 'lang' => (string)$recipient['lang'],
- 'status' => 0,
- 'response_code' => '',
- 'response_msg' => '待发送',
- 'retry_count' => 0,
- 'send_time' => 0,
- 'createtime' => $now,
- 'updatetime' => $now,
- ];
- }
- (new EmailBroadcastItemModel())->saveAll($items);
- Db::commit();
- } catch (\Throwable $e) {
- Db::rollback();
- $this->error('编辑群发任务失败:' . $e->getMessage());
- }
- $this->success('群发任务更新成功', url('Emailbroadcast/index'));
- }
- $this->assign('data', $row);
- return $this->fetch();
- }
- public function items()
- {
- $id = (int)$this->request->param('id/d', 0);
- $broadcast = $this->modelClass->find($id);
- if (!$broadcast) {
- $this->error('群发任务不存在');
- }
- if ($this->request->isAjax()) {
- $page = (int)$this->request->param('page/d', 1);
- $limit = (int)$this->request->param('limit/d', 20);
- $sort = (string)$this->request->param('sort/s', 'id');
- $order = (string)$this->request->param('order/s', 'desc');
- $email = trim((string)$this->request->param('email/s', ''));
- $status = $this->request->param('status/s', '');
- $query = new EmailBroadcastItemModel();
- $query = $query->where('broadcast_id', $id);
- if ($email !== '') {
- $query = $query->where('email', 'like', '%' . $email . '%');
- }
- if ($status !== '') {
- $query = $query->where('status', (int)$status);
- }
- $count = $query->count();
- $data = $query->order($sort, $order)->page($page, $limit)->select();
- return json(['code' => 0, 'count' => $count, 'data' => $data]);
- }
- $this->assign('broadcast', $broadcast);
- return $this->fetch();
- }
- public function send()
- {
- if (!$this->request->isPost()) {
- $this->error('请求方式错误');
- }
- $id = (int)$this->request->post('id/d', 0);
- $batchSize = (int)$this->request->post('batch_size/d', 0);
- $batchSize = $batchSize > 0 ? min($batchSize, 500) : 100;
- $row = $this->modelClass->find($id);
- if (!$row) {
- $this->error('群发任务不存在');
- }
- if (!in_array((int)$row->status, [0, 3, 4], true)) {
- $this->error('当前状态不允许发送');
- }
- $template = EmailTemplateModel::where('id', (int)$row->template_id)->find();
- if (!$template || (int)$template->status !== 1) {
- $this->error('关联模板不存在或已禁用');
- }
- $now = time();
- $row->status = 1;
- if ((int)$row->start_time <= 0) {
- $row->start_time = $now;
- }
- $row->updatetime = $now;
- $row->save();
- $items = EmailBroadcastItemModel::where('broadcast_id', $row->id)
- ->where('status', 'in', [0, 2])
- ->order('id', 'asc')
- ->limit($batchSize)
- ->select();
- if ($items->isEmpty()) {
- $this->error('没有可发送的明细记录');
- }
- $success = 0;
- $fail = 0;
- $processed = 0;
- foreach ($items as $item) {
- $subscribe = EmailSubscribeModel::where('id', (int)$item->subscribe_id)->find();
- $context = $this->buildMailContext([
- 'email' => (string)$item->email,
- 'lang' => (string)$item->lang,
- 'broadcast_name' => (string)$row->name,
- 'broadcast_id' => (string)$row->id,
- 'template_name' => (string)$template->name,
- 'unsubscribe_url' => $subscribe && (string)$subscribe->unsubscribe_token !== ''
- ? url('index/emailsubscribe/unsubscribe', ['token' => (string)$subscribe->unsubscribe_token], true, true)
- : '',
- ]);
- $subject = $this->renderTemplateContent((string)$template->subject, $context);
- $content = $this->renderTemplateContent((string)$template->content, $context);
- [$sendResult, $responseCode, $responseMsg] = $this->sendEmail((string)$item->email, $subject, $content);
- $sendTime = $sendResult ? time() : 0;
- $item->status = $sendResult ? 1 : 2;
- $item->response_code = $responseCode;
- $item->response_msg = $responseMsg ?: ($sendResult ? '发送成功' : '发送失败');
- $item->retry_count = (int)$item->retry_count + ($sendResult ? 0 : 1);
- $item->send_time = $sendTime;
- $item->updatetime = time();
- $item->save();
- if ($subscribe) {
- $subscribe->last_send_status = $sendResult ? 1 : 2;
- if ($sendResult) {
- $subscribe->last_send_time = $sendTime;
- }
- $subscribe->updatetime = time();
- $subscribe->save();
- }
- EmailSubscribeLogModel::create([
- 'subscribe_id' => (int)$item->subscribe_id,
- 'email' => (string)$item->email,
- 'mail_type' => 'marketing_broadcast',
- 'subject' => $subject,
- 'content' => $content,
- 'status' => $sendResult ? 1 : 2,
- 'response_code' => $responseCode,
- 'response_msg' => $responseMsg ?: ($sendResult ? '营销群发发送成功' : '营销群发发送失败'),
- 'retry_count' => $sendResult ? 0 : 1,
- 'send_time' => $sendTime,
- 'createtime' => time(),
- 'updatetime' => time(),
- ]);
- $sendResult ? $success++ : $fail++;
- $processed++;
- }
- $row->success_count = (int)$row->success_count + $success;
- $row->fail_count = (int)$row->fail_count + $fail;
- $pendingCount = EmailBroadcastItemModel::where('broadcast_id', $row->id)
- ->where('status', 0)
- ->count();
- $failedCount = EmailBroadcastItemModel::where('broadcast_id', $row->id)
- ->where('status', 2)
- ->count();
- if ($pendingCount > 0) {
- $row->status = $row->fail_count > 0 ? 3 : 1;
- $row->finish_time = 0;
- $message = '本次发送 ' . $processed . ' 条,任务未完成,仍有 ' . $pendingCount . ' 条待发送';
- } else {
- $row->status = $failedCount > 0 ? ($row->success_count > 0 ? 3 : 4) : 2;
- $row->finish_time = time();
- $message = $failedCount > 0 ? '群发执行完成,存在失败记录' : '群发执行完成';
- }
- $row->updatetime = time();
- $row->save();
- $this->success($message, null, [
- 'processed' => $processed,
- 'success' => $success,
- 'fail' => $fail,
- 'pending' => $pendingCount,
- 'batch_size' => $batchSize,
- ]);
- }
- public function del()
- {
- if (!$this->request->isPost()) {
- $this->error('请求方式错误');
- }
- $ids = $this->request->post('ids/a', []);
- if (!$ids) {
- $this->error('请先选择群发任务');
- }
- Db::startTrans();
- try {
- Db::name('email_broadcast_item')->where('broadcast_id', 'in', $ids)->delete();
- $this->modelClass->where('id', 'in', $ids)->delete();
- Db::commit();
- } catch (\Throwable $e) {
- Db::rollback();
- $this->error('删除失败:' . $e->getMessage());
- }
- $this->success('删除成功');
- }
- protected function buildBroadcastParams(): array
- {
- $row = $this->request->post('row/a', [], 'trim');
- $lang = strtolower((string)($row['lang'] ?? LangModel::getDefaultMark()));
- $templateId = (int)($row['template_id'] ?? 0);
- $template = EmailTemplateModel::where('id', $templateId)->find();
- if (!$template) {
- $this->error('请选择有效的邮件模板');
- }
- if ((string)$template->type !== 'marketing_broadcast') {
- $this->error('仅支持选择营销群发邮件模板');
- }
- if ((int)$template->status !== 1) {
- $this->error('所选模板已禁用');
- }
- $statusFilter = array_values(array_unique(array_map('intval', (array)($row['status_filter'] ?? [1]))));
- $statusFilter = array_values(array_intersect($statusFilter, [0, 1, 9]));
- $allowUserUnsubscribed = !empty($row['allow_user_unsubscribed']);
- if (!$allowUserUnsubscribed) {
- $statusFilter = array_values(array_diff($statusFilter, [9]));
- }
- if (empty($statusFilter)) {
- $statusFilter = [1];
- }
- $confirmFilter = array_values(array_unique(array_map('intval', (array)($row['confirm_status_filter'] ?? [0, 1]))));
- $confirmFilter = array_values(array_intersect($confirmFilter, [0, 1]));
- if (empty($confirmFilter)) {
- $confirmFilter = [0, 1];
- }
- $lastSendFilter = array_values(array_unique(array_map('intval', (array)($row['last_send_status_filter'] ?? [0, 1, 2]))));
- $lastSendFilter = array_values(array_intersect($lastSendFilter, [0, 1, 2]));
- if (empty($lastSendFilter)) {
- $lastSendFilter = [0, 1, 2];
- }
- return [
- 'name' => (string)($row['name'] ?? ''),
- 'template_id' => $templateId,
- 'template_name' => (string)$template->name,
- 'lang' => $lang,
- 'subject' => (string)$template->subject,
- 'content' => (string)$template->content,
- 'remark' => (string)($row['remark'] ?? ''),
- 'source_key' => (string)($row['source_key'] ?? ''),
- 'status_filter' => implode(',', $statusFilter),
- 'confirm_status_filter' => implode(',', $confirmFilter),
- 'last_send_status_filter' => implode(',', $lastSendFilter),
- 'exclude_sent' => empty($row['exclude_sent']) ? 0 : 1,
- 'allow_user_unsubscribed' => $allowUserUnsubscribed ? 1 : 0,
- ];
- }
- protected function getSubscribersForBroadcast(array $params): array
- {
- $query = EmailSubscribeModel::where('lang', $params['lang'])
- ->whereIn('status', explode(',', $params['status_filter']))
- ->whereIn('confirm_status', explode(',', $params['confirm_status_filter']))
- ->whereIn('last_send_status', explode(',', $params['last_send_status_filter']));
- if ($params['source_key'] !== '') {
- $query = $query->where('source_key', $params['source_key']);
- }
- if (!empty($params['exclude_sent'])) {
- $query = $query->whereNotIn('id', function ($subQuery) {
- $subQuery->name('email_subscribe_log')->where('mail_type', 'marketing_broadcast')->field('subscribe_id');
- });
- }
- return $query->order('id', 'asc')->select()->toArray();
- }
- protected function sendEmail(string $email, string $subject, string $content): array
- {
- try {
- $config = get_addon_config('phpmailer');
- if (!$config) {
- return [false, 'NO_CONFIG', '未找到 phpmailer 插件配置'];
- }
- $mailer = new \addons\phpmailer\library\Mailer($config);
- $result = $mailer->to($email)->subject($subject)->message($content, true)->send();
- if ($result) {
- return [true, 'SUCCESS', '邮件发送成功'];
- }
- return [false, 'FAIL', $mailer->getError() ?: '邮件发送失败'];
- } catch (\Throwable $e) {
- return [false, 'EXCEPTION', $e->getMessage()];
- }
- }
- protected function buildMailContext(array $data): array
- {
- $context = [];
- foreach ($data as $key => $value) {
- $context[$key] = is_scalar($value) || $value === null ? (string)$value : json_encode($value, JSON_UNESCAPED_UNICODE);
- }
- return $context;
- }
- protected function renderTemplateContent(string $content, array $context): string
- {
- $replace = [];
- foreach ($context as $key => $value) {
- $replace['{{' . $key . '}}'] = (string)$value;
- $replace['{{ ' . $key . ' }}'] = (string)$value;
- }
- return strtr($content, $replace);
- }
- }
|