Repository navigation
Expand file tree
/
Copy pathMailService.php
More file actions
221 lines (189 loc) · 7.89 KB
/
Copy pathMailService.php
File metadata and controls
221 lines (189 loc) · 7.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
<?php
declare(strict_types=1);
namespace app\service;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
use support\Log;
use Throwable;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
class MailService
{
/** @var AMQPStreamConnection|null */
private static ?AMQPStreamConnection $connection = null;
/** @var AMQPChannel|null */
private static $channel = null;
/** @var bool */
private static bool $initialized = false;
/**
* 渲染:基于 view 模板名(走 TwigTemplateService 与多主题逻辑)
*/
public static function renderView(string $template, array $vars = [], ?string $app = null, ?string $plugin = null): string
{
return TwigTemplateService::render($template, $vars, $app, $plugin);
}
/**
* 渲染:基于 inline Twig 模板字符串
*/
public static function renderInline(string $templateString, array $vars = []): string
{
$options = config('view.options', []);
// inline 模板默认不落地缓存
$options['cache'] = false;
$env = new Environment(new ArrayLoader(['__inline__' => $templateString]), $options);
$extension = config('view.extension');
if ($extension) {
$extension($env);
}
return $env->render('__inline__', $vars);
}
/**
* 入队:构建并发送邮件任务到 MQ
* 支持 payload:
* - to: string|array 收件人或收件人数组
* - subject: string
* - html: string 已渲染 HTML(可选)
* - text: string 纯文本(可选)
* - headers: array 额外头(可选)
* - attachments: array [['path' => '/a/b.txt','name'=>'b.txt','encoding'=>null,'type'=>null], ...]
* - view: string Twig 视图名(如 'emails/welcome',自动追加后缀)
* - view_vars: array 视图变量
* - inline_template: string Twig 语法的内联模板
* - inline_vars: array 内联模板变量
*/
public static function enqueue(array $payload): bool
{
try {
self::initializeQueues();
$channel = self::getChannel();
// 如果未提供 html,但提供了 view 或 inline_template,则进行渲染
if (empty($payload['html'])) {
if (!empty($payload['view'])) {
$vars = (array) ($payload['view_vars'] ?? []);
$payload['html'] = self::renderView((string) $payload['view'], $vars);
} elseif (!empty($payload['inline_template'])) {
$vars = (array) ($payload['inline_vars'] ?? []);
$payload['html'] = self::renderInline((string) $payload['inline_template'], $vars);
}
}
$exchange = (string) blog_config('rabbitmq_mail_exchange', 'mail_exchange', true);
$routingKey = (string) blog_config('rabbitmq_mail_routing_key', 'mail_send', true);
// 计算优先级(0-9),支持字符串映射 high/normal/low
$priority = 0;
if (isset($payload['priority'])) {
$p = $payload['priority'];
if (is_string($p)) {
$map = [
'high' => 9,
'normal' => 5,
'low' => 1,
];
$priority = $map[strtolower($p)] ?? 0;
} elseif (is_numeric($p)) {
$priority = max(0, min(9, (int) $p));
}
}
$msg = new AMQPMessage(json_encode($payload, JSON_UNESCAPED_UNICODE), [
'content_type' => 'application/json',
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'priority' => $priority,
]);
$channel->basic_publish($msg, $exchange, $routingKey);
Log::debug('Mail enqueued: ' . json_encode([
'to' => $payload['to'] ?? null,
'subject' => $payload['subject'] ?? null,
'priority' => $priority,
], JSON_UNESCAPED_UNICODE));
return true;
} catch (Throwable $e) {
Log::error('Enqueue mail failed: ' . $e->getMessage());
return false;
}
}
private static function getConnection(): AMQPStreamConnection
{
if (!self::$connection) {
self::$connection = new AMQPStreamConnection(
(string) blog_config('rabbitmq_host', '127.0.0.1', true),
(int) blog_config('rabbitmq_port', 5672, true),
(string) blog_config('rabbitmq_user', 'guest', true),
(string) blog_config('rabbitmq_password', 'guest', true),
(string) blog_config('rabbitmq_vhost', '/', true),
);
}
return self::$connection;
}
private static function getChannel(): AMQPChannel
{
if (!self::$channel) {
self::$channel = self::getConnection()->channel();
self::$channel->basic_qos(0, 1, false);
}
return self::$channel;
}
private static function initializeQueues(): void
{
if (self::$initialized) {
return;
}
try {
self::ensureMailDefaults();
$ch = self::getChannel();
$exchange = (string) blog_config('rabbitmq_mail_exchange', 'mail_exchange', true);
$routingKey = (string) blog_config('rabbitmq_mail_routing_key', 'mail_send', true);
$queueName = (string) blog_config('rabbitmq_mail_queue', 'mail_queue', true);
$dlxExchange = (string) blog_config('rabbitmq_mail_dlx_exchange', 'mail_dlx_exchange', true);
$mailDlxQueue = (string) blog_config('rabbitmq_mail_dlx_queue', 'mail_dlx_queue', true);
// 声明 DLX 及 mail 专属 DLQ(使用 mail_dlx_exchange,routing key 绑定到 mail_dlx_queue)
$ch->exchange_declare($dlxExchange, 'direct', false, true, false);
$ch->queue_declare($mailDlxQueue, false, true, false, false);
$ch->queue_bind($mailDlxQueue, $dlxExchange, $mailDlxQueue);
// 主交换机 & 队列(带死信参数)
$ch->exchange_declare($exchange, 'direct', false, true, false);
$args = new AMQPTable([
'x-dead-letter-exchange' => $dlxExchange,
'x-dead-letter-routing-key' => $mailDlxQueue,
'x-max-priority' => 10, // 开启队列优先级(0-9)
]);
try {
$ch->queue_declare($queueName, false, true, false, false, false, $args);
} catch (Throwable $e) {
Log::warning('mail_queue 声明失败,尝试无参重建: ' . $e->getMessage());
$ch->queue_declare($queueName, false, true, false, false, false);
}
$ch->queue_bind($queueName, $exchange, $routingKey);
self::$initialized = true;
Log::debug('Mail queues initialized');
} catch (Throwable $e) {
Log::error('Initialize mail queues failed: ' . $e->getMessage());
}
}
public static function close(): void
{
try {
if (self::$channel) {
self::$channel->close();
self::$channel = null;
}
if (self::$connection) {
self::$connection->close();
self::$connection = null;
}
} catch (Throwable $e) {
Log::warning('Close mail MQ connection failed: ' . $e->getMessage());
}
}
public function __destruct()
{
self::close();
}
/**
* 初始化邮件队列默认配置(首次无记录时落库)
* 注:不再初始化旧版 mail_host 等单平台配置,现已使用 mail_providers 轮询机制
*/
protected static function ensureMailDefaults(): void
{
}
}