Thiết kế Goal và ghi nhận chuyển đổi cho Campaign

1. Mục tiêu

Tính năng này trả lời được bốn câu hỏi:

  1. Campaign muốn khách hàng thực hiện hành động gì sau khi nhận thông điệp?
  2. Khách hàng nào đã đạt Goal?
  3. Khách hàng đạt Goal sau lần gửi nào, thuộc Run và nhóm A/B nào?
  4. Báo cáo cần tính số chuyển đổi và tỷ lệ chuyển đổi từ dữ liệu nào?

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.


2. Hiện trạng và phần còn thiếu

Luồng hiện tại đã có:

Phần còn thiếu:


3. Quy tắc chung

3.1. Mốc bắt đầu tính chuyển đổi

Với target được gửi thông điệp:

Với target thuộc nhóm A/B CONTROL:

Target gửi thất bại hoàn toàn và không thuộc CONTROL không được tính conversion.

3.2. Hạn chế của dữ liệu OCS hiện tại

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:

  1. Trước khi gửi, Target Snapshot phải loại khách đã đạt Goal.
  2. Sau khi gửi, job chỉ kiểm tra lại target đã SENT hoặc target CONTROL.
  3. Khi OCS bắt đầu trả kết quả khớp, hệ thống ghi 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.

3.3. Chống ghi trùng

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.

3.4. Quy tắc kết hợp điều kiện

Để cấu hình dễ hiểu và không cần biểu thức lồng phức tạp:

Quy tắc này đủ cho cả năm Campaign và tránh phải lưu thêm ruleMatchPolicy.


4. Cấu hình Goal trên giao diện

Tại bước thiết lập Campaign, FE thêm phần Ghi nhận chuyển đổi.

4.1. Tự động từ OCS

FE hiển thị:

  1. Chọn Tự động từ OCS.
  2. Chọn dataset.
  3. Chọn field, operator và giá trị giống màn tạo Segment.
  4. Có thể thêm nhiều điều kiện trong một rule.
  5. Có thể thêm rule khác khi Goal nằm ở bảng khác hoặc cần điều kiện 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_configocs_dataset_field_config.

4.2. Import thủ công

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:

Không cho import khi Campaign là DRAFT, SUBMITTED, REJECTED hoặc CANCELLED.


5. Cấu hình Goal cho năm Campaign

5.1. CAMP 1 - Chuyển đổi chéo BUs

Dataset: ALL_REPORT_STL_NEW
LOGIN_SUPERAPP >= 1

5.2. CAMP 2 - Onboarding khách Online

Dataset: ALL_REPORT_STL_NEW
LOGIN_SUPERAPP >= 1

5.3. CAMP 3 - Tải app chưa kích hoạt

Dataset: ALL_REPORT_STL_NEW
LOGIN_SUPERAPP >= 1

5.4. CAMP 4 - Upgrade gói Data 50K

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.

5.5. CAMP 5 - Phát triển thuê bao 4G

g_volumenum_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

6. Thiết kế dữ liệu

6.1. MongoDB campaign

Khô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.

6.2. MongoDB campaign_run

Bổ 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.

6.3. MongoDB campaign_conversion_scan_state

Collection 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.

6.4. MongoDB campaign_conversion_import_job

Collection 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.

6.5. ClickHouse 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.


7. Luồng tự động từ OCS

7.1. Chốt target trước khi gửi

[Diagram]

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.

7.2. Quét conversion sau khi gửi

[Diagram]

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.

7.3. API OCS dùng cho Goal

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.


8. Luồng import thủ công

8.1. Định dạng file

Cột Bắt buộc Ý nghĩa
phone 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.

8.2. Luồng xử lý

[Diagram]

8.3. Lỗi theo từng dòng

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.


9. Luồng báo cáo

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:

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.


10. Các trường hợp phải xử lý

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.

11. Log, bảo mật và hiệu suất

11.1. Log bắt buộc

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 đủ.

11.2. Hiệu suất

11.3. Error code đa ngôn ngữ

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.


12. Danh sách task triển khai

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.

Task 01 - Khai báo Goal và validation dùng chung

Mục tiêu

Tạo contract thống nhất cho Goal để Campaign API, Run snapshot, OCS và import cùng sử dụng.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

Không phụ thuộc task khác. Đây là một trong ba task có thể bắt đầu ngay.

Hoàn thành khi


Task 02 - Lưu Goal vào Campaign và Campaign Run snapshot

Mục tiêu

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ũ.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

Phụ thuộc Task 01.

Hoàn thành khi


Task 03 - Tạo bảng ClickHouse và lớp lưu conversion dùng chung

Mục tiêu

Tạo một nơi duy nhất ghi conversion cho cả AUTO_OCSMANUAL_IMPORT, đồng thời cung cấp các query nền tảng cho scheduler và report.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

Không phụ thuộc task khác. Đây là một trong ba task có thể bắt đầu ngay.

Hoàn thành khi


Task 04 - API OCS kiểm tra Goal theo batch

Mục tiêu

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.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

Không phụ thuộc task khác. Đây là một trong ba task có thể bắt đầu ngay.

Hoàn thành khi


Task 05 - Scheduler quét Goal tự động và quản lý checkpoint

Mục tiêu

Đị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.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

Phụ thuộc Task 01, Task 03 và Task 04.

Hoàn thành khi


Task 06 - Lọc Goal khi chốt Target Snapshot và dừng gửi lại

Mục tiêu

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.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

Phụ thuộc Task 02, Task 03 và Task 04.

Hoàn thành khi


Task 07 - API và trạng thái job import conversion thủ công

Mục tiêu

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.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

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.

Hoàn thành khi


Task 08 - Xử lý file import và ghi conversion

Mục tiêu

Đọ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.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

Phụ thuộc Task 03 và Task 07.

Hoàn thành khi


Task 09 - Bổ sung conversion vào API báo cáo

Mục tiêu

Đư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.

Phạm vi thay đổi

Luồng xử lý

[Diagram]

Trường hợp phải xử lý

Phụ thuộc

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.

Hoàn thành khi


13. Cách chia việc cho ba nhánh song song

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:

[Diagram]

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.

14. Thứ tự tích hợp cuối

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