| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- <?php
- namespace App\Services;
- use Carbon\CarbonImmutable;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Validator;
- use Illuminate\Validation\ValidationException;
- class TelegramChannelMessageService
- {
- private const TABLE = 'telegram_channel_messages';
- public function __construct(private TelegramChannelService $channels) {}
- public function list(int $page): array
- {
- $items = DB::table(self::TABLE)->orderByDesc('id')->paginate(20, ['*'], 'page', $page);
- return ['list' => array_map($this->present(...), $items->items()), 'total' => $items->total()];
- }
- public function create(array $input): array
- {
- $fields = Validator::make($input, [
- 'channel_id' => 'required|integer|min:1',
- 'scheduled_at' => 'nullable|date_format:Y-m-d\TH:i:sP',
- 'is_top' => 'sometimes|boolean',
- ])->validate();
- $message = TelegramChannelService::message($input);
- $channel = $this->channels->detail((int) $fields['channel_id']);
- if (!$channel['enabled']) {
- throw ValidationException::withMessages(['channel_id' => '请先启用频道']);
- }
- $scheduled = isset($fields['scheduled_at']) ? CarbonImmutable::parse($fields['scheduled_at'])->utc() : null;
- if ($scheduled !== null && $scheduled->lessThanOrEqualTo(now('UTC'))) {
- throw ValidationException::withMessages(['scheduled_at' => '定时发送时间必须晚于当前时间']);
- }
- $id = (int) DB::table(self::TABLE)->insertGetId([
- 'channel_id' => $channel['id'],
- 'channel_name' => $channel['name'],
- 'target' => $channel['telegram_chat_id'] ?? $channel['chatId'],
- 'message' => json_encode($message, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
- 'is_top' => $fields['is_top'] ?? false,
- 'status' => 'scheduled',
- 'scheduled_at' => $scheduled ?? now('UTC'),
- 'created_at' => now('UTC'),
- 'updated_at' => now('UTC'),
- ]);
- if ($scheduled === null) {
- $this->dispatch($id);
- }
- return $this->present(DB::table(self::TABLE)->find($id));
- }
- public function cancel(int $id): void
- {
- if (!DB::table(self::TABLE)->where('id', $id)->where('status', 'scheduled')->update([
- 'status' => 'cancelled',
- 'updated_at' => now('UTC'),
- ])) {
- throw ValidationException::withMessages(['id' => '只有待发送消息可以取消']);
- }
- }
- public function dispatchDue(): void
- {
- DB::table(self::TABLE)->where('status', 'sending')
- ->where('attempted_at', '<=', now('UTC')->subMinutes(10))->update([
- 'status' => 'unknown', 'error' => '发送中断,结果未知,请到频道核对', 'updated_at' => now('UTC'),
- ]);
- $ids = DB::table(self::TABLE)->where('status', 'scheduled')
- ->where('scheduled_at', '<=', now('UTC'))->orderBy('scheduled_at')->orderBy('id')->limit(20)->pluck('id');
- foreach ($ids as $id) {
- $this->dispatch((int) $id);
- }
- }
- private function dispatch(int $id): void
- {
- $claimed = DB::table(self::TABLE)->where('id', $id)->where('status', 'scheduled')
- ->where('scheduled_at', '<=', now('UTC'))->update([
- 'status' => 'sending', 'attempted_at' => now('UTC'), 'updated_at' => now('UTC'),
- ]);
- if (!$claimed) {
- return;
- }
- $row = DB::table(self::TABLE)->find($id);
- try {
- $result = $this->channels->publishMessage(
- (int) $row->channel_id,
- json_decode($row->message, true, 512, JSON_THROW_ON_ERROR),
- (bool) $row->is_top,
- $row->target,
- );
- } catch (ValidationException $e) {
- $result = ['sent' => false, 'pinned' => null, 'message_id' => null, 'error' => $e->validator->errors()->first()];
- }
- DB::table(self::TABLE)->where('id', $id)->where('status', 'sending')->update([
- 'status' => match ($result['sent']) { true => 'sent', false => 'failed', null => 'unknown' },
- 'telegram_message_id' => $result['message_id'],
- 'pinned' => $result['pinned'],
- 'error' => $result['error'],
- 'updated_at' => now('UTC'),
- ]);
- }
- private function present(object $row): array
- {
- $data = (array) $row;
- $data['message'] = json_decode($row->message, true);
- $data['is_top'] = (bool) $row->is_top;
- $data['pinned'] = $row->pinned === null ? null : (bool) $row->pinned;
- foreach (['scheduled_at', 'attempted_at', 'created_at', 'updated_at'] as $key) {
- $data[$key] = $row->$key === null ? null : CarbonImmutable::parse($row->$key, 'UTC')->toIso8601String();
- }
- return $data;
- }
- }
|