Tính năng này trả lời được bốn câu hỏi:
Hệ thống hỗ trợ đúng hai cách ghi nhận:
| Chế độ | Cách hoạt động |
|---|---|
AUTO_OCS |
Hệ thống định kỳ đối chiếu target của Campaign với dữ liệu OCS. |
MANUAL_IMPORT |
Người dùng import danh sách khách đã phản hồi tại màn chi tiết Campaign. |
Một Campaign chỉ được chọn một trong hai chế độ. Không cho phép vừa tự động vừa import thủ công để tránh một khách hàng được ghi nhận từ hai nguồn khác nhau.
Phạm vi hiện tại chỉ tính một khách hàng chuyển đổi một lần trong một Campaign. Không phát triển attribution nhiều chạm, doanh thu, nhiều Goal trên một Campaign hoặc tự động chọn biến thể A/B thắng.
Luồng hiện tại đã có:
Campaign, CampaignRun và bản chụp cấu hình của từng Run.campaign_target_member trong ClickHouse chứa đúng tập khách đã chốt cho từng Run.campaign_delivery_event ghi SENT, FAILED và các kết quả gửi.campaign_click_event, campaign_open_event ghi tương tác.Phần còn thiếu:
Với target được gửi thông điệp:
campaign_delivery_event.event_type = SENT.SENT gần nhất nhưng không sau thời điểm conversion được phát hiện.campaignRunId, targetSnapshotId, variantCode, channel và actionIndex lấy từ lần gửi được gắn conversion.Với target thuộc nhóm A/B CONTROL:
SENT.campaign_target_snapshot.frozenAt.CONTROL vẫn được ghi nhận để so sánh tỷ lệ chuyển đổi tự nhiên với các nhóm được gửi.Target gửi thất bại hoàn toàn và không thuộc CONTROL không được tính conversion.
Các trường như login_superapp, data_package, vas_service, g_volume phản ánh trạng thái hiện tại. Chúng không cung cấp chính xác thời điểm trạng thái thay đổi.
Vì vậy chế độ AUTO_OCS áp dụng quy tắc:
SENT hoặc target CONTROL.detectedAt là thời điểm phát hiện.Như vậy hệ thống chứng minh được khách chưa đạt Goal ở thời điểm chốt target và đã đạt Goal ở lần kiểm tra sau khi gửi. Muốn có chính xác thời điểm nghiệp vụ, OCS phải bổ sung cột thời gian phát sinh sự kiện.
conversionId được tạo cố định:
SHA-256(companyId + ":" + campaignId + ":" + recipientKey)
Cùng một khách trong cùng Campaign luôn có cùng conversionId, kể cả khi:
Báo cáo luôn dùng uniqExact(conversion_id). Vì vậy bản ghi Kafka/ClickHouse bị phát lại không làm tăng số conversion.
Để cấu hình dễ hiểu và không cần biểu thức lồng phức tạp:
AND.OR.Quy tắc này đủ cho cả năm Campaign và tránh phải lưu thêm ruleMatchPolicy.
Tại bước thiết lập Campaign, FE thêm phần Ghi nhận chuyển đổi.
FE hiển thị:
Tự động từ OCS.OR.Payload tối thiểu:
{
"conversionGoal": {
"mode": "AUTO_OCS",
"rules": [
{
"datasetCode": "ALL_REPORT_STL_NEW",
"conditions": [
{
"fieldCode": "LOGIN_SUPERAPP",
"operator": "GREATER_EQUAL_THAN",
"valueSource": "FIXED",
"value": 1
}
]
}
]
}
}
Không yêu cầu FE gửi tên bảng, tên cột vật lý hoặc selectedFields. Backend tự resolve từ ocs_dataset_config và ocs_dataset_field_config.
Khi tạo Campaign, FE chỉ gửi:
{
"conversionGoal": {
"mode": "MANUAL_IMPORT"
}
}
Không upload file tại bước tạo Campaign.
Nút Import kết quả chuyển đổi chỉ xuất hiện tại màn chi tiết Campaign khi:
MANUAL_IMPORT.IN_PROGRESS, PAUSED hoặc FINISHED.Không cho import khi Campaign là DRAFT, SUBMITTED, REJECTED hoặc CANCELLED.
Dataset: ALL_REPORT_STL_NEW
LOGIN_SUPERAPP >= 1
Dataset: ALL_REPORT_STL_NEW
LOGIN_SUPERAPP >= 1
Dataset: ALL_REPORT_STL_NEW
LOGIN_SUPERAPP >= 1
Chỉ cần một trong hai rule khớp:
Rule 1
Dataset: ALL_REPORT_STL_NEW
DATA_PACKAGE = "50K"
OR
Rule 2
Dataset: F013_VAS_MONTH_STREAM
VAS_SERVICE = "50K"
Nếu nghiệp vụ cung cấp nhiều mã gói 50K, danh sách giá trị phải được quản lý trong cấu hình field/rule, không hard-code rải rác trong scheduler.
g_volume và num_usage_day_4g là số không âm, nên > 0 đồng thời loại null và 0.
Rule 1
Dataset: ALL_REPORT_STL_NEW
DATA_PACKAGE = "10K6S"
AND G_VOLUME > 0
OR
Rule 2
Dataset: ALL_REPORT_STL_NEW
DATA_PACKAGE = "10K6S"
AND NUM_USAGE_DAY_4G > 0
Các field sau cần được bổ sung vào cấu hình dataset ALL_REPORT_STL_NEW nếu chưa có:
LOGIN_SUPERAPP STRING
G_VOLUME NUMBER
NUM_USAGE_DAY_4G NUMBER
campaignKhông tạo collection Goal riêng vì mỗi Campaign hiện chỉ có một Goal.
Bổ sung object nhúng:
conversionGoal.mode
conversionGoal.rules[]
Model đề xuất:
CampaignConversionGoalModel
mode: AUTO_OCS | MANUAL_IMPORT
rules: List<SegmentQueryDefinitionModel>
Backend nhận datasetCode + conditions, sau đó dùng validator OCS hiện có để resolve thành SegmentQueryDefinitionModel. Bản lưu phải chứa schema, table, column và kiểu dữ liệu đã resolve; không phụ thuộc vào label FE.
Không bổ sung các field sau trong giai đoạn này:
goalCode
countingMode
attributionWindowDays
ruleMatchPolicy
Các giá trị đó đã có quy tắc cố định trong phạm vi năm Campaign.
campaign_runBổ sung conversionGoal vào CampaignRunDefinitionSnapshotModel.
Khi tạo Run, MapStruct copy Goal hiện tại của Campaign vào snapshot. Job conversion luôn đọc snapshot, không đọc cấu hình Campaign có thể đã bị thay đổi sau đó.
Sau khi Campaign đã có Run đầu tiên, không cho sửa conversionGoal. Muốn đổi Goal phải tạo Campaign mới để không làm báo cáo cũ thay đổi ý nghĩa.
campaign_conversion_scan_stateCollection này chỉ lưu trạng thái kỹ thuật của job tự động, không lưu từng khách hàng.
| Trường | Ý nghĩa |
|---|---|
companyId, campaignId |
Campaign đang được quét. |
cycleDate |
Ngày chạy hiện tại. |
status |
IDLE, RUNNING, RETRY_WAIT. |
cursorRecipientKey |
Điểm đã xử lý thành công gần nhất. |
scannedCount, matchedCount |
Số liệu theo dõi tiến độ. |
leaseOwner, leaseUntil |
Chống hai node xử lý cùng Campaign. |
nextScanAt |
Thời điểm được phép quét tiếp. |
lastSuccessAt, lastErrorCode |
Theo dõi vận hành. |
Index:
UNIQUE(companyId, campaignId)
INDEX(status, nextScanAt)
INDEX(leaseUntil)
Cursor chỉ được cập nhật sau khi batch conversion đã ghi thành công xuống ClickHouse.
campaign_conversion_import_jobCollection này phục vụ giao diện theo dõi import thủ công.
| Trường | Ý nghĩa |
|---|---|
companyId, campaignId |
Phạm vi import. |
fileAssetId |
File nguồn đã upload lên MinIO. |
status |
QUEUED, RUNNING, COMPLETED, FAILED. |
totalRows, successRows, invalidRows, duplicateRows |
Tiến độ xử lý. |
errorFileAssetId |
File lỗi để FE tải lại. |
createdBy, startedAt, completedAt |
Audit. |
lastErrorCode |
Mã lỗi tổng của job. |
Index:
INDEX(companyId, campaignId, createdAt DESC)
INDEX(status, createdAt)
Không lưu từng dòng import trong MongoDB.
campaign_conversion_eventĐây là bảng kết quả chuyển đổi theo khách hàng và là nguồn chính cho báo cáo.
CREATE TABLE campaign_conversion_event
(
event_date Date,
converted_at DateTime64(3),
detected_at DateTime64(3),
conversion_id String,
company_id String,
campaign_id String,
campaign_run_id String,
target_snapshot_id String,
recipient_key String,
variant_code String DEFAULT '',
channel LowCardinality(String) DEFAULT '',
action_index Int32 DEFAULT -1,
attributed_sent_at Nullable(DateTime64(3)),
source LowCardinality(String),
time_source LowCardinality(String),
matched_rule_no UInt16,
source_dataset LowCardinality(String) DEFAULT '',
source_reference String DEFAULT '',
import_job_id String DEFAULT '',
evidence String DEFAULT '{}',
created_at DateTime64(3)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (
company_id,
campaign_id,
campaign_run_id,
event_date,
recipient_key
);
Giải thích các trường dễ nhầm:
| Trường | Ý nghĩa |
|---|---|
converted_at |
Thời điểm conversion từ file hoặc thời điểm OCS được phát hiện. |
detected_at |
Thời điểm LaoAds ghi nhận được conversion. |
time_source |
SOURCE_TIME, DETECTED_TIME hoặc IMPORT_TIME. |
attributed_sent_at |
Lần gửi được gắn conversion; null với CONTROL. |
matched_rule_no |
Rule OCS đã khớp, bắt đầu từ 1. |
evidence |
JSON nhỏ chứa giá trị chứng minh rule khớp; không chứa số điện thoại gốc. |
Không dùng MongoDB để lưu conversion theo từng người vì dữ liệu có thể rất lớn.
Không tạo bảng tổng hợp mới trong giai đoạn đầu. ClickHouse query trực tiếp bằng uniqExact(conversion_id) trên một Campaign. Chỉ bổ sung aggregate/Flink khi đo thực tế cho thấy API báo cáo không đạt ngưỡng yêu cầu.
Không được coi UNKNOWN, NOT_FOUND hoặc lỗi OCS là “chưa đạt Goal”. Nếu không xác định được đầy đủ thì fail batch và retry để tránh gửi nhầm.
Nếu process chết sau khi insert ClickHouse nhưng trước khi cập nhật cursor, batch được chạy lại. conversionId cố định và báo cáo dùng uniqExact, nên không tăng sai số liệu.
Không gọi từng ISDN. Một request tối đa 1.000 ISDN:
POST /api/v1/internal/subscribers/match-goal
X-API-Key: <internal-api-key>
Request chứa:
{
"requestId": "scan-id",
"isdns": ["85620xxxxxxx"],
"rules": [
{
"tableName": "all_report_stl_new",
"identityColumn": "isdn",
"conditions": [
{
"columnName": "login_superapp",
"dataType": "STRING",
"operator": "GREATER_EQUAL_THAN",
"value": 1
}
]
}
]
}
API này chỉ nhận cấu hình đã resolve từ backend. FE không gọi trực tiếp. OCS service phải validate table/column/operator theo allow-list hiện có và bind giá trị bằng prepared statement.
Response phải trả kết quả cho toàn bộ ISDN đầu vào, gồm MATCHED, NOT_MATCHED, NOT_FOUND, UNKNOWN, INVALID. LaoAds chỉ ghi conversion cho MATCHED.
| Cột | Bắt buộc | Ý nghĩa |
|---|---|---|
phone |
Có | Số điện thoại khách hàng. |
converted_at |
Không | Thời điểm phản hồi; nếu trống dùng thời điểm import. |
reference |
Không | Mã giao dịch hoặc mã đối soát bên ngoài. |
INVALID_PHONE
DUPLICATE_IN_FILE
NOT_CAMPAIGN_TARGET
NO_SUCCESSFUL_SEND
CONVERTED_BEFORE_SEND
ALREADY_CONVERTED
INVALID_CONVERTED_AT
Với target CONTROL, thay NO_SUCCESSFUL_SEND bằng kiểm tra converted_at >= targetSnapshot.frozenAt.
Một file có dòng lỗi vẫn hoàn thành job với các dòng hợp lệ. Chỉ đánh FAILED khi không đọc được file, ClickHouse không ghi được hoặc lỗi hệ thống khiến job không thể tiếp tục.
Nguồn dữ liệu:
| Chỉ số | Bảng |
|---|---|
| Target | campaign_target_member |
| Sent/Failed/Delivered | campaign_delivery_event hoặc campaign_metric_1m |
| Open/Read/Click | Bảng tracking hiện có hoặc campaign_metric_1m |
| Conversion | campaign_conversion_event |
Quy tắc tính:
conversionCount = uniqExact(conversion_id)
conversionRate = conversionCount / distinctTargetCount * 100
Dùng target làm mẫu số để nhóm CONTROL vẫn so sánh được. Không dùng số action hoàn thành trong MongoDB làm conversion.
Các API cần bổ sung hoặc sửa:
conversionCount và conversionRate.conversions theo converted_at.variant_code.channel đã được gắn từ lần SENT.CampaignActionExecutionModel.Query báo cáo bắt buộc scope bằng company_id + campaign_id; khi xem một Run thì thêm campaign_run_id.
| Trường hợp | Xử lý |
|---|---|
| Khách đạt Goal trước khi chốt target | Loại khỏi Target Snapshot, không ghi conversion cho Campaign. |
| OCS lỗi khi chốt target | Snapshot fail và retry; không mặc định cho khách đi tiếp. |
| OCS lỗi khi quét conversion | Không cập nhật cursor; retry cùng batch. |
| OCS trả thiếu một ISDN | Batch lỗi; không đánh ISDN thiếu là NOT_MATCHED. |
| Khách có nhiều dòng OCS | OCS tổng hợp theo ISDN; chỉ trả một kết quả cuối. |
| Khách khớp nhiều rule | Chỉ một conversion; lưu rule đầu tiên theo thứ tự cấu hình và evidence các rule khớp nếu cần. |
| Khách thuộc nhiều Run | Gắn lần SENT gần nhất trước khi phát hiện; chỉ tính một lần trong Campaign. |
| Khách chỉ có event FAILED | Không conversion, trừ khi thuộc CONTROL. |
| Job chết giữa batch | Chạy lại; conversionId chống tăng số liệu. |
| Hai node cùng chạy | Mongo lease chỉ cho một node giữ Campaign. |
| Import lặp cùng file | Dòng đã conversion được tính ALREADY_CONVERTED, không tăng báo cáo. |
| Import khi Campaign AUTO | Từ chối toàn bộ request. |
| Import khi Campaign FINISHED | Cho phép. |
| Import khi Campaign CANCELLED | Từ chối. |
| Campaign đã có Run rồi sửa Goal | Từ chối; tạo Campaign mới. |
converted_at trống trong file |
Dùng thời điểm import và time_source=IMPORT_TIME. |
| Target CONTROL chuyển đổi | Ghi conversion, attributed_sent_at=null, channel=''. |
| Campaign không có ngày kết thúc | AUTO tiếp tục quét khi Campaign còn hoạt động; khi FINISHED thực hiện một lượt quét cuối. |
Log theo batch, không log từng người:
requestId, scanId, importJobId
companyId, campaignId, campaignRunId
cursor, batchSize, matchedCount, invalidCount
durationMs, errorCode, retryCount
Không log số điện thoại gốc, nội dung file, credential OCS hoặc evidence đầy đủ.
recipient_key, không dùng OFFSET.company_id + campaign_id.Các exception từ API phải chỉ dùng error code và message resource hiện có:
CAMPAIGN_GOAL0001 Goal mode không hợp lệ
CAMPAIGN_GOAL0002 AUTO_OCS phải có ít nhất một rule
CAMPAIGN_GOAL0003 MANUAL_IMPORT không được chứa rule
CAMPAIGN_GOAL0004 Dataset/field/operator Goal không hợp lệ
CAMPAIGN_GOAL0005 Không được sửa Goal sau khi Campaign đã có Run
CAMPAIGN_GOAL0006 Campaign không cho phép import conversion
CAMPAIGN_GOAL0007 File import không hợp lệ
CAMPAIGN_GOAL0008 Không tìm thấy Goal snapshot của Run
CAMPAIGN_GOAL0009 Không thể kiểm tra Goal từ OCS
CAMPAIGN_GOAL0010 Kết quả OCS không đầy đủ
Bổ sung message tương ứng vào đầy đủ file ngôn ngữ đang được LaoAds sử dụng. Không hard-code message tiếng Anh trong service.
Phần việc được tách thành chín task. Mỗi task tạo thành một merge request độc lập, có đầu ra rõ ràng và không gom toàn bộ trách nhiệm của một nhánh vào một merge request lớn.
Tạo contract thống nhất cho Goal để Campaign API, Run snapshot, OCS và import cùng sử dụng.
CampaignConversionModeEnum: AUTO_OCS, MANUAL_IMPORT.CampaignConversionGoal.AUTO_OCS bắt buộc có ít nhất một rule; mỗi rule có dataset và ít nhất một condition.MANUAL_IMPORT không được chứa rule.datasetCode, fieldCode thành schema/table/column/data type ở Backend.Không phụ thuộc task khác. Đây là một trong ba task có thể bắt đầu ngay.
SegmentValidationService sang Campaign.Lưu Goal khi tạo Campaign và cố định Goal theo từng Run để cấu hình về sau không làm sai báo cáo cũ.
conversionGoal vào CampaignModel, Campaign request/response và API chi tiết Campaign.conversionGoal vào CampaignRunDefinitionSnapshotModel.conversionGoal vẫn đọc được.Phụ thuộc Task 01.
Tạo một nơi duy nhất ghi conversion cho cả AUTO_OCS và MANUAL_IMPORT, đồng thời cung cấp các query nền tảng cho scheduler và report.
campaign_conversion_event theo schema tại mục 6.5.conversionId = SHA-256(companyId + ":" + campaignId + ":" + recipientKey) tại một hàm dùng chung.SENT gần nhất trước thời điểm conversion.recipient_key; không dùng OFFSET.company_id và campaign_id.CONTROL không cần SENT, attributed_sent_at để null.Không phụ thuộc task khác. Đây là một trong ba task có thể bắt đầu ngay.
conversionId không làm tăng kết quả uniqExact.Cho phép LaoAds kiểm tra tối đa 1.000 thuê bao theo một hoặc nhiều rule mà không gọi OCS từng số điện thoại.
POST /api/v1/internal/subscribers/match-goal.AND; nhiều rule là OR.MATCHED, NOT_MATCHED, NOT_FOUND, UNKNOWN, INVALID.Không phụ thuộc task khác. Đây là một trong ba task có thể bắt đầu ngay.
requestId, batch size, dataset, duration và error code; không log ISDN.Định kỳ tìm khách đã đạt Goal, ghi conversion theo batch và tiếp tục đúng vị trí sau retry hoặc restart.
campaign_conversion_scan_state và các index tại mục 6.3.AUTO_OCS đến hạn.SENT hoặc CONTROL, chưa có conversion, theo keyset.MATCHED; không coi UNKNOWN, INVALID hoặc response thiếu là chưa đạt Goal.UNKNOWN.Phụ thuộc Task 01, Task 03 và Task 04.
Không đưa vào Run mới những khách đã đạt Goal trước khi gửi hoặc đã được ghi conversion trong Campaign.
AUTO_OCS.campaign_conversion_event để loại khách đã conversion ở Run trước.MANUAL_IMPORT không gọi OCS ở bước này; vẫn loại conversion đã import trước đó nếu Campaign tạo Run tiếp.MANUAL_IMPORT không có rules OCS.Phụ thuộc Task 02, Task 03 và Task 04.
Cho phép FE tạo và theo dõi một lần import mà chưa xử lý dữ liệu file trong request HTTP.
campaign_conversion_import_job và index tại mục 6.4.fileAssetId đã upload bằng File Asset/MinIO hiện có.MANUAL_IMPORT khi Campaign IN_PROGRESS, PAUSED hoặc FINISHED.importJobId ngay.fileAssetId.Phụ thuộc Task 01. Có thể phát triển song song với Task 02 sau khi contract Task 01 được chốt.
Đọc file lớn an toàn, ghi các dòng hợp lệ và trả file lỗi mà không làm mất toàn bộ job vì một dòng sai.
QUEUED thành RUNNING theo cách atomic.phone, converted_at, reference.SENT gần nhất; xử lý riêng CONTROL.COMPLETED; chỉ dùng FAILED cho lỗi hệ thống không thể tiếp tục.converted_at trước thời điểm gửi/freeze.Phụ thuộc Task 03 và Task 07.
Đưa conversion thật vào báo cáo Campaign, Run, A/B, channel và báo cáo tổng hợp company.
conversionCount và conversionRate vào Campaign summary.converted_at.campaign_run_id, variant_code và channel.CONTROL.campaign_conversion_event.CampaignActionExecutionModel làm nguồn conversion.uniqExact(conversion_id) và mẫu số distinct target.company_id; API Campaign phải thêm campaign_id.CONTROL hoặc channel rỗng.Phụ thuộc Task 03 để chốt schema và query contract. Có thể làm song song với Task 05, Task 06 và Task 08.
Không cần chờ hoàn thành toàn bộ một nhánh mới bắt đầu nhánh khác. Thứ tự đề xuất:
| Giai đoạn | Nhánh A | Nhánh B | Nhánh C |
|---|---|---|---|
| Bắt đầu | Task 01 - Goal contract | Task 03 - ClickHouse conversion store | Task 04 - OCS Goal API |
| Sau khi nền tảng ổn định | Task 02 - Campaign/Run snapshot | Task 07 - Import job API | Task 05 - AUTO scheduler |
| Ghép luồng | Task 06 - Target pre-check | Task 08 - Import processor | Task 09 - Report |
Sơ đồ phụ thuộc:
Ba task phải được chốt contract sớm nhất là Task 01, Task 03 và Task 04. Sau khi ba contract này được merge hoặc thống nhất trên cùng branch tích hợp, các task còn lại có thể phát triển song song mà không tự định nghĩa lại model, schema hoặc response.
Mốc 1: Task 01 + Task 03 + Task 04
Mốc 2: Task 02 + Task 05 + Task 07 + Task 09
Mốc 3: Task 06 + Task 08
Mốc 4: kiểm tra xuyên suốt AUTO_OCS và MANUAL_IMPORT
Hai luồng bắt buộc chạy xuyên suốt trước khi đóng nhóm task:
AUTO_OCS:
create -> approve -> snapshot pre-check -> send -> daily scan -> conversion -> report
MANUAL_IMPORT:
create -> approve -> send -> upload file -> import job -> conversion -> report