Tài liệu mô tả kiến trúc của Export Core
Tài liệu mô tả kiến trúc của Export Core — hạ tầng dùng chung để sinh file
export (XLSX / CSV) chạy ngầm qua queue, lưu lên S3, rồi gửi link tải về cho
người dùng qua Chat. Hiện tại đang phục vụ tính năng xuất excel service
config, nhưng được thiết kế để
mọi module khác tái sử dụng chỉ bằng cách thêm một Exporter mới.
Mục tiêu thiết kế
- Tách business logic khỏi cách ghi file. Mỗi loại export chỉ cần khai báo lấy dữ liệu gì (sheets + headers + generator); việc ghi XLSX/CSV và upload S3 do core lo.
- Memory phẳng (flat) với dataset lớn. Toàn bộ pipeline dùng PHP
\GeneratorlazyById(), ghi ra temp file theo stream, rồi stream-upload lên S3 — không bao giờ nạp toàn bộ dataset vào RAM. Chạy được dataset 100k+ dòng.
- Chạy ngầm, không block request. Người dùng bấm nút → tạo bản ghi
ExportRequest(PENDING) → đẩyGenericExportJobvào queueexport→ trả về ngay. Khi xong, link tải được gửi qua Chat. - Mở rộng bằng cách thêm class, không sửa core. Thêm 1 enum case + 1 class
Exporter+ 1 nhánh trong factory là đủ.
Sơ đồ luồng (end-to-end)

flowchart TD
Blade["Blade: <x-admin-base::buttons.export-ajax><br/>nút 'Xuất dữ liệu'"]
Ctrl["Controller::exportAdministrativeFormat<br/>modules/Admin/Utm/.../UtmServiceConfigController"]
DomainSvc["UtmServiceConfigService::exportAdministrativeFormat<br/>app(ExportService)->export($auth, ['component', 'filters'], $isSync)"]
ExportSvc["Export\\Services\\ExportService::export<br/>tạo ExportRequest (PENDING)"]
Job["Export\\Jobs\\GenericExportJob (queue: 'export')<br/>status → PROCESSING<br/>ExportServiceFactory::make(component)"]
Exporter["Exporter extends BaseExportService::export<br/>getSheets() → FastExcelSheet[] (mỗi tỉnh = 1 sheet)"]
Writer{"Chọn writer<br/>theo export_format"}
Csv["NativeCsvExport<br/>(csv)"]
FastXlsx["FastExcelXlsxExport<br/>(xlsx + FastExcelSheet)"]
Legacy["Maatwebsite/Excel<br/>(xlsx legacy, GenericSheetExportV2)"]
Storage["StorageService: temp file → stream put() lên S3<br/>(visibility: private)<br/>status → COMPLETED, files=[path], completed_at"]
Notify["notifyUser: gửi pre-signed URL (hết hạn 24h)<br/>qua send_chat()"]
Blade -->|"AJAX POST (params từ URL filter + export_format + _token)"| Ctrl
Ctrl -->|"gọi service domain"| DomainSvc
DomainSvc --> ExportSvc
ExportSvc -->|"dispatch / dispatchSync"| Job
Job --> Exporter
Exporter --> Writer
Writer -->|csv| Csv
Writer -->|xlsx| FastXlsx
Writer -->|xlsx legacy| Legacy
Csv --> Storage
FastXlsx --> Storage
Legacy --> Storage
Storage --> Notify
Với chế độ đồng bộ (isSync = true), dispatchSync chạy ngay trong request
và controller trả thẳng files_url về cho front-end (front-end tự window.open
từng link). Với chế độ bất đồng bộ (mặc định cho UTM), controller trả message
"Quá trình tải về sẽ mất vài phút… file sẽ được gửi về Chat của bạn."
Thành phần
Services/ExportService.php — điểm vào (entry point)
API public mà các module domain gọi:
app(ExportService::class)->export($auth, [
'component' => ExportRequestComponent::UTM_SERVICE_CONFIG_ADMINISTRATIVE,
'filters' => [...], // params lọc, gồm cả 'export_format'
], $isSync = false);
- Tạo bản ghi
ExportRequest(statusPENDING) quaExportRequestInterface(repository), gắncauserpolymorphic là user đang đăng nhập. isSync = true→GenericExportJob::dispatchSync()rồi trả vềExportRequestđãrefresh().isSync = false→GenericExportJob::dispatch()trả vềPendingDispatch.
Controller dựa vào kiểu trả về (PendingDispatch vs ExportRequest) để quyết
định trả message "chạy ngầm" hay trả thẳng files_url.
Jobs/GenericExportJob.php — worker chạy ngầm
- Queue cố định:
export.tries = 1(không retry tự động),timeout = 0+set_time_limit(0)để job dài không bị kill; nếu lỡ bị requeue thìretryAfter = 7200s. handle(): bỏ qua nếu request không tồn tại hoặc đãCOMPLETED(idempotent); setPROCESSING+started_at; lấy Exporter từ factory rồi gọiexport().failed(): setFAILED+ ghierror_reason(gồm request context từLogContext, message/file/line của exception) + log lỗi. Cũng bỏ qua nếu đãCOMPLETED.
Services/ExportServiceFactory.php — bộ định tuyến
match từ ExportRequestComponent sang class Exporter cụ thể. Đăng ký
singleton trong ExportServiceProvider. Đây là nơi duy nhất phải sửa khi thêm
loại export mới (ngoài việc tạo class Exporter và thêm enum case).
match ($component) {
UTM_SERVICE_CONFIG_ADMINISTRATIVE => app(UtmServiceConfigAdministrativeExport::class),
UTM_SERVICE_CONFIG_UTM => app(UtmServiceConfigUtmExport::class),
default => throw new \Exception(...),
};
Services/Contracts/ExportServiceInterface.php + BaseExportService.php
BaseExportService là template method chứa toàn bộ logic ghi file & thông
báo; mỗi Exporter con chỉ override 3 hook:
| Hook abstract | Trách nhiệm |
|---|---|
getSheets($request) |
Trả về FastExcelSheet[] (hoặc GenericSheetExportV2[] legacy) |
getFilePrefix() |
Tiền tố tên file, vd cau_hinh_shop_theo_dia_gioi_hanh_chinh |
getNotificationTitle() |
Tiêu đề thông báo Chat |
export() của base làm các việc:
- Đọc
export_formattừfilters(mặc địnhcsv). - Suy ra extension (
csv/xlsx) và writer phù hợp. - Sinh tên file:
{prefix}_{timestamp}_{uniqid}.{ext}; object pathexports/{prefix}/{Y-m-d}quaStorageService::generateStorageMeta(). - Dispatch tới đúng writer:
- CSV →
NativeCsvExport(nhận cảFastExcelSheetlẫnGenericSheetExportV2). - XLSX + FastExcelSheet →
FastExcelXlsxExport(đường nhanh, streaming). - XLSX + sheet legacy →
Maatwebsite\ExcelquaMultiSheetExport(GenericSheetExportV2, có styling nhưng nặng RAM/CPU).
- CSV →
- Nếu lưu thành công: cập nhật
COMPLETED,files,completed_atrồi gọinotifyUser(). Nếu thất bại: ném exception (job sẽ vàofailed()).
notifyUser() lấy pre-signed URL với hạn 24h (withFilesUrlExpiration), gom
thành message dạng [link]...[/link] và gửi qua send_chat(). Lưu ý:
ngoài production thì email nhận luôn là chuongtd@hasaki.vn (hard-code để
test) — chỉ production mới gửi đúng $user->email.
Các writer
| Class | Format | Cơ chế | Khi dùng |
|---|---|---|---|
NativeCsvExport |
CSV | fputcsv thuần + BOM UTF-8, ghi temp file rồi stream lên S3 |
format csv. Nhanh 10–50× so với PHPSpreadsheet CSV |
FastExcelXlsxExport |
XLSX | rap2hpoutre/fast-excel (OpenSpout) — ZIP streaming, không style |
format xlsx với FastExcelSheet. Nhanh 5–20× với 100k+ dòng |
Maatwebsite\Excel |
XLSX | PHPSpreadsheet qua MultiSheetExport (legacy) |
XLSX với GenericSheetExportV2 — khi cần styling |
Cả ba đều stream temp file → S3 với visibility: private, và luôn unlink
temp file trong finally. CSV chỉ ghi 1 sheet (CSV không có khái niệm multi-sheet);
XLSX gom nhiều sheet, mỗi sheet 1 tab (key của SheetCollection = tên tab).
Services/FastExcelSheet.php — DTO mô tả 1 sheet
Thay thế GenericSheetExportV2 (vốn mang theo concern của PHPSpreadsheet) cho
pipeline FastExcel, để không đụng tới GenericSheetExportV2 của các consumer cũ.
new FastExcelSheet(
title: $sheetTitle, // ≤ 31 ký tự (giới hạn tab name của XLSX)
headers: ['Nhãn cột' => 'row_key', ...], // map nhãn hiển thị → key dữ liệu
data: $generator, // \Generator, mỗi yield = mảng assoc theo row_key
);
headings()→ mảng nhãn cột (hàng tiêu đề).mappedGenerator()→ yield row đã map đúng thứ tự cột theoheaders(dùng cho CSV để đảm bảo cột khớp tiêu đề).FastExcelXlsxExporttự build header row bằng nhãn cột làm key của row — KHÔNG inject thêm một hàng heading riêng (xem ghi chú trongrowsWithHeading()).
Exporter mẫu (Services/Exporter/*)
UtmServiceConfigAdministrativeExport— xuất cấu hình theo địa giới hành chính. Mỗi tỉnh = 1 sheet. Duyệt cây tỉnh → quận/huyện → phường/xã bằnglazyById(1000), tính trạng thái bật/tắt từng service type và kế thừa disable từ cấp cha (calculateStatuses).UtmServiceConfigUtmExport— xuất cấu hình theo toạ độ UTM (grid cell). Có thêm cột UTM/toạ độ/shop; chỉ tạo sheet cho các tỉnh thực sự có dữ liệu grid-cell (1 query joingrid_cells_ward → wards → districtsthay cho vòng lặp EXISTS N+1 per-tỉnh).
Cả hai dùng PickupLocationServiceType::allNow() để sinh động các cột service.
Front-end: nút export AJAX
Component dùng chung: modules/Admin/Base/.../components/buttons/export-ajax.blade.php.
Cách dùng (xem modules/Admin/Utm/.../service-configs/index-administrative-format.blade.php):
<x-admin-base::buttons.export-ajax
url="{{ route('admin.utm.service-config.export-administrative-format') }}"
permission="now-utm-service-config-administrative-export"
exportFormat="xlsx"
/>
Props chính: url, method (mặc định POST), permission (disable nút nếu
user thiếu quyền), buttonText, exportFormat (xlsx/csv), dataCallback
(tên hàm JS trả về payload), exportData (payload tĩnh).
Hành vi JS (đăng ký @once, dùng event-delegation trên .btn-ajax-export-action):
- Thu thập params theo thứ tự ưu tiên:
dataCallback()→exportDatatĩnh → mặc định: copy toàn bộ query-string của URL hiện tại (giữ nguyên bộ lọc đang xem; tự gom keyfoo[]thành mảng). - Gắn thêm
export_format(từ prop) và_token(CSRF) cho POST/PUT/DELETE. - Block UI, gọi
$.ajax. - Xử lý response chuẩn của hệ thống (
status.error_code === 0):- Nếu
data.files_url(mảng) hoặcdata.file_url(legacy) có giá trị →window.opentừng link (giãn 500ms để trình duyệt không chặn popup) — đây là nhánh đồng bộ, file có sẵn ngay. - Nếu không có URL → báo "Tiến trình xuất dữ liệu đang chạy ngầm thành công!" — nhánh bất đồng bộ, link sẽ về qua Chat.
error_code === 403→ "Bạn không có quyền…".
- Nếu
Khớp với contract back-end: controller trả files_url khi sync, hoặc chỉ trả
message khi async.
Dữ liệu & trạng thái: ExportRequest
Bảng export_requests (model Modules\Entity\Models\ExportRequest):
| Cột | Ý nghĩa |
|---|---|
causer_type/id |
Polymorphic — ai yêu cầu export |
component |
enum ExportRequestComponent (loại export → factory map) |
status |
enum ExportRequestStatus: PENDING(1) → PROCESSING(2) → COMPLETED(4) hoặc FAILED(3) |
filters |
JSON params lọc, gồm export_format |
files |
mảng đường dẫn file đã lưu trên S3 |
error_reason |
JSON chi tiết lỗi (khi FAILED) |
started_at / completed_at |
mốc thời gian |
files_url(accessor) sinh pre-signed URL từfilesquaurl_media(); hạn mặc địnhStorageService::URL_EXPIRATION_REPORT.withFilesUrlExpiration($sec)đặt hạn một lần cho lần đọcfiles_urlkế tiếp (tự reset sau 1 lần, không lộ quatoArray()/toJson()).
Thêm một loại export mới — checklist
- Thêm case vào enum
Modules\Entity\Enums\ExportRequest\ExportRequestComponent. - Tạo class
Modules\Export\Services\Exporter\{Name}Export extends BaseExportService, overridegetSheets()/getFilePrefix()/getNotificationTitle(). TronggetSheets()ưu tiên trảFastExcelSheet[]với generator +lazyById(). - Thêm nhánh
matchtrongExportServiceFactory::make(). - Ở service domain tương ứng, gọi
app(ExportService::class)->export($auth, [...]). - Thêm route + controller action trả về tuỳ kiểu (
PendingDispatch→ message,ExportRequest→files_url). - Đặt nút
<x-admin-base::buttons.export-ajax>trên view vớipermissionvàexportFormatphù hợp.
Lưu ý đặt sheet title ≤ 31 ký tự (giới hạn XLSX) và giữ nguyên chuỗi tiếng Việt trong header/label theo đúng yêu cầu nghiệp vụ.
All rights reserved