# Gửi tin hàng loạt theo thẻ hội thoại

Trang `/settings/campaigns` (tab **Gửi hàng loạt**) cho phép chọn nick Zalo cá nhân và/hoặc
OA, lọc hội thoại theo thẻ rồi gửi cùng một nội dung theo nhịp có kiểm soát.

## Ai được dùng

Chỉ **admin của đơn vị**. Middleware `admin` (`EnsureUserIsAdmin`) chỉ nhận role `admin` -
super admin không vào. Ranh giới giữa các đơn vị nằm ở tầng dữ liệu:

- `SpamCampaignService::accountsQuery()` chỉ trả về tài khoản Zalo mà chủ sở hữu **hoặc**
  nhân viên được phân quyền thuộc cùng đơn vị với admin đang thao tác. Đây KHÔNG phải
  `ZaloAccount::scopeVisibleTo` (ở đó admin thấy mọi tài khoản của mọi đơn vị).
- `SpamCampaign::scopeVisibleTo()` chặn admin đơn vị A đọc/dừng chiến dịch của đơn vị B.

## Luồng chạy

1. **Tạo chiến dịch** (nháp): chọn tài khoản, thẻ, loại hội thoại, "còn tương tác trong N
   ngày", "bỏ qua ai đã nhận trong N ngày", nội dung (`{{name}}` được thay bằng tên người
   nhận) và nhịp gửi.

   Bộ lọc thẻ là **một trong hai chế độ**, không phải hai danh sách song song:
   - `include` - chỉ gửi cho hội thoại mang thẻ đã chọn (kèm "có bất kỳ" / "có đủ mọi thẻ");
   - `exclude` - gửi cho tất cả, trừ hội thoại mang thẻ đã chọn.

   Danh sách thẻ có ô tìm và nút tích hàng loạt; nút này chỉ tác động lên các thẻ đang hiện
   sau khi lọc, và giữ nguyên những thẻ đã chọn đang bị ô tìm giấu đi.
2. **Xem trước**: đếm chính xác số người nhận theo từng tài khoản + vài tên mẫu. Dùng đúng
   một đường code với lúc chốt danh sách (`SpamTargetResolver`) nên con số không lệch.
3. **Bắt đầu gửi**: `DispatchSpamCampaignJob` chốt danh sách vào `spam_campaign_targets` rồi
   rải `SendSpamMessageJob` cho từng người, cách nhau `delay_seconds` (mặc định 5) với nhiễu
   ±20% để loạt tin không đều tăm tắp như máy.
4. **Theo dõi / dừng**: nút Tạm dừng và Huỷ có hiệu lực ngay với mọi tin chưa rời hàng đợi -
   mỗi job đọc lại trạng thái chiến dịch ngay trước khi gọi API Zalo.

Danh sách người nhận được **chốt một lần** lúc chạy đầu tiên. Bấm chạy tiếp sau khi tạm dừng
sẽ dùng lại đúng danh sách cũ, nên không ai nhận tin hai lần (còn có unique key
`(spam_campaign_id, zalo_account_id, thread_id)` và một update-có-điều-kiện lúc job nhận việc).

## Ràng buộc của Zalo

- **OA**: chỉ gửi được tin tự do trong **7 ngày** kể từ tin cuối của khách. Hệ thống tự kẹp bộ
  lọc về 7 ngày cho tài khoản OA, và kiểm lại lần nữa ngay trước khi gửi (chiến dịch dài có
  thể chạy qua ngày hôm sau); người ngoài cửa sổ bị đánh dấu **bỏ qua**, muốn gửi phải dùng ZNS.
- **Nick cá nhân**: rủi ro bị khoá nick. Nhịp gửi là con số DUY NHẤT điều tiết lưu lượng
  (không có trần theo giờ/ngày): 5 giây/tin ≈ tối đa 720 tin/giờ. Sau
  `FAILURE_STOP_THRESHOLD` (5) lỗi liên tiếp, chiến dịch tự dừng.

## Vận hành

- Job chạy trên hàng đợi `spam`, do container **`queue-spam`** xử lý (`queue` thường chỉ ăn
  hàng đợi `default`, không bốc job của chiến dịch).
- `php artisan spam:run-scheduled` - scheduler gọi mỗi phút, khởi động chiến dịch tới giờ hẹn.
- `php artisan spam:recount [id]` - đối soát bộ đếm trên `spam_campaigns` với bảng người nhận
  (scheduler gọi mỗi 15 phút). Dùng khi worker bị giết giữa chừng khiến số liệu lệch hoặc
  chiến dịch kẹt ở trạng thái "đang chạy".

## Dữ liệu

- `spam_campaigns`: cấu hình + bộ đếm tổng (`total/sent/failed/skipped_count`) - bản cache để
  màn danh sách không phải COUNT bảng con mỗi lượt poll.
- `spam_campaign_targets`: **nguồn sự thật** - mỗi người nhận một dòng kèm `status`, `msg_id`,
  `error`, `sent_at`.
- Tin thật vẫn nằm ở `zalo_messages` như mọi tin khác, và được ghi công cho người tạo chiến
  dịch trong `zalo_user_sent_messages` nên trang Báo cáo tự tính luôn.
