Khảo sát qua campaign và cộng thưởng tự động

1. Mục đích và phạm vi

Tài liệu thiết kế tính năng khảo sát cho LaoAds: quản lý bộ câu hỏi riêng, dùng campaign để gửi lời mời tới ứng dụng của SP, nhận câu trả lời và tự động cộng thưởng khi đủ điều kiện. Nội dung này là thiết kế cần triển khai; không có nghĩa toàn bộ chức năng đã tồn tại.

SP là đơn vị sử dụng LaoAds. Một SP có thể có nhiều ứng dụng. Hồ sơ khách hàng được quản lý riêng trong từng SP; một khách có thể cài ứng dụng trên nhiều thiết bị.

Các khái niệm dùng trong tài liệu:

Khái niệm Cách hiểu
Phiên bản khảo sát Bộ câu hỏi và cách rẽ nhánh đã phát hành, được giữ nguyên cho các lượt đang chạy
Lượt chạy campaign (run) Một lần campaign thực hiện gửi theo lịch
Lượt tham gia Lời mời và bài trả lời của một khách trong một lượt chạy
Giữ suất thưởng Tạm giữ một phần trong hạn mức để người dùng có thời gian làm bài
Quyền nhận thưởng Bản ghi xác định lượt tham gia được nhận gì, vào số nào và đã xử lý tới đâu
Lệnh thực hiện (command) Công việc giao cho worker, ví dụ cộng 500 MB vào một số điện thoại
Outbox Danh sách yêu cầu được lưu cùng dữ liệu nghiệp vụ, để chuyển tiếp qua Kafka
Phiên bản cập nhật (revision) Số tăng sau mỗi lần sửa, giúp tránh ghi đè hoặc xử lý lại dữ liệu cũ

Phần 4–5 mô tả luồng sử dụng và xử lý. Phần 6 là thiết kế dữ liệu. Phần 8 giải thích hành vi và báo cáo. Tên field trong bảng được giữ để dùng khi triển khai.

Phạm vi đầu tiên:

Hỗ trợ ngay bốn loại câu hỏi: chọn một đáp án, chọn nhiều đáp án, đánh giá 1–5 hoặc 1–10 bằng sao/số và nhập văn bản. Câu chọn đáp án có thể bật lựa chọn “Khác” kèm nội dung nhập. Hỗ trợ rẽ nhánh theo đáp án, chuyển tới câu hỏi khác hoặc kết thúc khảo sát. Chưa triển khai hiển thị native trong app, OTP hoặc link công khai không giới hạn người tham gia.

Tài liệu liên quan: Nền dispatcher, worker và phí, Khách hàng và danh tính theo SP. Quy tắc phí, chống trùng và đối soát trong các tài liệu này tiếp tục áp dụng.

2. Nguyên tắc nghiệp vụ

  1. Bộ câu hỏi được tạo riêng. Campaign chọn phiên bản khảo sát đã phát hành, ứng dụng, người nhận, lịch gửi và phần thưởng.
  2. Phiên bản đã phát hành không sửa tại chỗ. Campaign đã duyệt giữ nguyên phiên bản câu hỏi và chính sách thưởng đã chốt.
  3. Mỗi người được mời có một lượt tham gia trong một run. Nhiều thiết bị của cùng người dùng chung lượt tham gia.
  4. Link nhận diện lượt tham gia, không chứng minh người mở link là chủ tài khoản. Người được chuyển tiếp link có thể sử dụng lượt đó khi chưa yêu cầu đăng nhập.
  5. Số nhận thưởng là dữ liệu riêng, không tự tạo hoặc hợp nhất customer bằng số này. Chưa OTP thì lưu trạng thái chưa xác minh.
  6. Gửi push xong không kích hoạt thưởng. Chỉ bài nộp hợp lệ và đủ điều kiện mới tạo yêu cầu thưởng.
  7. Hoàn thành xử lý gửi, hoàn thành khảo sát và cộng thưởng thành công là ba trạng thái riêng.
  8. Không tự cộng thưởng hoặc thu phí lại khi hết thời gian chờ, mất phản hồi hoặc tiến trình dừng sau khi đã ghi bắt đầu gọi. Giữ trạng thái chưa rõ kết quả để đối soát.
  9. Ngân sách giữ trong LaoAds là phân bổ nội bộ, không phải tài nguyên hoặc số dư đã được provider giữ thật.
  10. Khảo sát không thưởng bỏ qua bước nhập số, giữ suất và cộng thưởng.

3. Thành phần đang có và phần cần hoàn thiện

Thành phần Hiện trạng và hướng xử lý
Campaign, run, target snapshot, dispatcher và Kafka Dùng lại nền hiện có; thêm nhánh khảo sát và sự kiện hoàn thành
sp_config Đã có Firebase project và service account; cần gắn rõ ứng dụng và môi trường
app_installation Đã có đăng ký, cập nhật token, identify/logout; cần kiểm đủ phạm vi SP/app/môi trường và liên kết khách
Push-worker Đã có đường gửi FCM; cần chọn Firebase theo ứng dụng và không dùng điều kiện loại khách từng đăng nhập cho mọi campaign
VCGW Đã có action tiền, data, SMS, phút gọi, khuyến mại
UPoint Đã có đường gọi Unipay; cần kiểm kết quả nghiệp vụ trước khi dùng cho khảo sát
Command, outbox, phí và đối soát Dùng lại, bổ sung liên kết nguồn khảo sát và kiểm tra các điểm chưa đáp ứng quy tắc
Quản lý khảo sát, lượt tham gia và quyền thưởng Bổ sung theo thiết kế dưới đây

VCGW có trường ProcessChargingRequest.contents. VcgwRewardProvider hiện lấy tên event, nếu eventName null thì lấy tên campaign. TelecomResourceData chưa có nội dung riêng. Cần bổ sung nội dung cấu hình và chốt vào request. Chưa xác nhận trường này tạo SMS cho thuê bao; không dùng nó thay thông báo kết quả. Độ dài, ký tự, đơn vị data và ý nghĩa trường phải xác nhận với bên tích hợp; code hiện nhân lượng data với 1024 trước khi gửi.

4. Luồng tổng thể

[Diagram]

5. Luồng chi tiết theo giai đoạn

5.1. Đăng ký ứng dụng và thiết bị

  1. Người quản trị tạo cấu hình app thuộc SP và môi trường, khai báo Firebase.
  2. Backend cấp khóa tích hợp để xác định đúng SP/app/môi trường; không tin các giá trị phạm vi do client tự khai.
  3. App khởi tạo installation, đăng ký thông tin thiết bị và quyền nhận push. Chưa có token vẫn có thể đăng ký installation.
  4. Firebase SDK lấy token. App gửi token về LaoAds khi có token mới và khi đồng bộ trạng thái định kỳ.
  5. Khi đăng nhập, app cung cấp bằng chứng từ backend ứng dụng để liên kết installation với customer. Khóa tích hợp trong app không là bằng chứng danh tính khách.
  6. Logout gỡ liên kết hiện tại. Yêu cầu cũ đến trễ không được xóa liên kết của tài khoản vừa đăng nhập; sử dụng phiên bản liên kết hoặc phiên đăng nhập.

Tra cứu installation bằng đủ SP/app/môi trường/installationId. Không dùng số điện thoại bắt buộc để nhận diện khách. ID nguồn có namespace riêng, ví dụ LAOTV, và được resolve sang customerId.

Các tình huống đăng ký thiết bị:

Tình huống Xử lý
Chưa cấp quyền hoặc chưa có token Giữ installation, không đủ điều kiện push; cập nhật khi người dùng cấp quyền
Đăng ký lại cùng installation Tạo mới hoặc cập nhật theo đúng khóa SP/app/môi trường/installation, không tạo thiết bị trùng
identifyKey của app khác Không cho cập nhật installation ngoài phạm vi
Đăng nhập tài khoản mới, logout cũ đến trễ Kiểm tra phiên bản liên kết; không xóa chủ hiện tại
Khóa tích hợp bị thu hồi Vô hiệu cache theo cấu hình và chặn request mới; không chờ cache dài hết hạn

5.2. Tạo khảo sát và campaign

  1. Tạo khảo sát nháp, câu hỏi có mã ổn định và các đáp án cho phép.
  2. Xem trước, kiểm tra giới hạn câu hỏi/kích thước rồi phát hành một phiên bản bất biến.
  3. Tạo campaign: chọn phiên bản khảo sát, app/môi trường, audience, lịch gửi, hạn trả lời và nội dung push.
  4. Nếu có thưởng, chọn loại, số lượng, thời hạn sử dụng, nội dung hiển thị, nội dung gửi provider, tổng suất và giới hạn theo người/số nhận.
  5. Duyệt campaign theo workflow hiện có. Snapshot phải chứa đủ cấu hình câu hỏi tham chiếu và phần thưởng; không dùng nội dung client gửi lại lúc thực thi.
  6. Kiểm tra cấu hình provider và giá/phí trước duyệt. Không coi thiếu giá là miễn phí.

Giới hạn phần thưởng: một phần thưởng cho một lượt hoàn thành. Có thể chọn không thưởng; chưa hỗ trợ người dùng tự chọn nhiều loại thưởng. Câu hỏi và rẽ nhánh thực hiện theo mục 5.2.1.

Các tình huống cấu hình campaign:

Tình huống Xử lý
Chọn khảo sát hoặc app khác SP Từ chối, không tiết lộ dữ liệu ngoài quyền
Phiên bản chưa phát hành Không duyệt
Firebase/provider thiếu cấu hình hoặc thiếu giá Báo lỗi đúng trường; không tự dùng cấu hình mặc định của SP/app khác
Hạn trả lời trước lịch gửi Từ chối cấu hình
Thay bộ câu hỏi sau duyệt Tạo version mới cho lần cấu hình mới; không đổi bản cấu hình đã duyệt

5.2.1. Loại câu hỏi và quy tắc rẽ nhánh

Loại Cấu hình Đáp án được chấp nhận
SINGLE_SELECT Danh sách optionId; required; có/không lựa chọn Khác Đúng một optionId hợp lệ nếu trả lời
MULTI_SELECT Danh sách optionId; required; minSelections/maxSelections; có/không Khác Tập optionId không trùng, trong giới hạn
RATING min=1; max=5 hoặc 10; display=STAR hoặc NUMBER; required Số nguyên trong khoảng đã cấu hình
TEXT required; minLength/maxLength Văn bản thuần, không thực thi HTML/script

Lựa chọn Khác là một option có ID ổn định và thuộc tính requiresText. Khi chọn, phải nhập nội dung không rỗng sau trim và trong giới hạn otherTextMaxLength. Với multi-select, Khác được tính là một lựa chọn và có thể đi cùng lựa chọn khác. Nếu bỏ chọn Khác thì nội dung đi kèm bị loại khỏi bài trả lời và thống kê cuối cùng. Required chỉ áp dụng cho câu thuộc đường đi thực tế.

Bộ câu hỏi có một câu bắt đầu. Mỗi câu gồm danh sách quy tắc có thứ tự và đường đi mặc định. Mỗi quy tắc gồm ruleId, priority, điều kiện và đích đến; đích là questionId hoặc END. END là yêu cầu kết thúc phần câu hỏi, vẫn phải qua API nộp và kiểm tra để hoàn thành.

Loại đáp án Điều kiện hỗ trợ
Chọn một EQUALS, IN
Chọn nhiều CONTAINS_ANY, CONTAINS_ALL, CONTAINS_NONE
Đánh giá EQ, LT, LTE, GT, GTE, BETWEEN với hai đầu bao gồm
Văn bản IS_EMPTY, IS_NOT_EMPTY
Mọi loại ANSWERED, UNANSWERED nếu phù hợp câu tùy chọn

Không nhận script, SQL, biểu thức tự do hoặc regex do quản trị viên nhập. Một quy tắc dùng nhóm ALL hoặc ANY với số điều kiện có giới hạn, không lồng vô hạn. Điều kiện tham chiếu câu hiện tại hoặc câu đã đi qua trên cùng đường đi. Điều kiện tham chiếu câu chưa trả lời không khớp, trừ UNANSWERED trên câu đã đi qua. Không dùng đáp án của nhánh đã bị loại.

Khi nhiều quy tắc cùng đúng, chọn priority nhỏ nhất; priority trong một câu không trùng. Nếu không có quy tắc đúng, chọn defaultNext. Giao diện hiển thị thứ tự ưu tiên để người quản trị hiểu kết quả, đặc biệt multi-select có thể khớp nhiều nhánh.

Cấu trúc lưu là đồ thị có hướng không vòng lặp: nhiều nhánh có thể gặp lại cùng một câu, không cần nhân bản câu hỏi. Câu hỏi có thứ tự trong phiên bản; đích chuyển tiếp phải đứng sau câu hiện tại. Cách giới hạn này hỗ trợ cây câu hỏi và ghép nhánh, đồng thời tránh vòng lặp và điều kiện không thể xác định. Khi mở rộng cho nhảy ngược cần thiết kế phiên bản quy tắc riêng; nút Quay lại của giao diện không phải nhánh nhảy ngược.

Ví dụ:

[Diagram]

Có thể cấu hình nhánh 4–5 đi thẳng END thay Q2b. Trang cảm ơn chỉ hiển thị hoàn thành sau khi server chốt bài; không đánh dấu hoàn thành chỉ vì frontend đã đi tới END.

Luồng quản trị:

  1. Thêm câu hỏi, đáp án, điều kiện, defaultNext và đường đi END.
  2. Backend kiểm ID hợp lệ, loại toán tử, giá trị, priority, thứ tự đích, tham chiếu điều kiện và các giới hạn kích thước.
  3. Kiểm không có câu không thể tới, câu không có lối ra, vòng lặp hoặc tham chiếu không tồn tại. Tham chiếu câu trước phải bảo đảm câu đó có mặt trên mọi đường tới quy tắc sử dụng nó; nếu không thì từ chối cấu hình.
  4. Cung cấp công cụ xem trước dùng cùng bộ xử lý quy tắc với lúc người dùng làm bài. Thử bộ đáp án và hiển thị đường đi, rule đã khớp.
  5. Phát hành bản cấu hình cố định gồm câu hỏi, quy tắc, schemaVersion và logicVersion. Phiên bản mới không đổi bài đang làm.

Luồng trả lời:

  1. Server đọc và cache version bất biến, trả câu hiện tại và dữ liệu hiển thị cần thiết.
  2. Người dùng gửi đáp án cùng revision. Server kiểm loại/giá trị, tính câu tiếp theo bằng bộ quy tắc chung.
  3. Khi quay lại sửa câu trước, server tính lại từ câu bắt đầu; giữ đáp án còn nằm trên đường mới, loại đáp án/cờ hoàn thành của nhánh không còn áp dụng. Trả danh sách câu bị loại cho frontend.
  4. Sau điểm chưa trả lời hợp lệ, chưa quyết định các câu phía sau. Bỏ qua câu tùy chọn là thao tác rõ ràng để đánh giá UNANSWERED và defaultNext.
  5. Khi nộp, server tính lại toàn bộ đường đi và câu bắt buộc; không tin danh sách câu đã hoàn thành hoặc đích END do client khai. Chỉ lưu kết quả cuối của đường hợp lệ.
Tình huống Xử lý
Multi-select khớp hai nhánh Chọn priority nhỏ nhất; lưu ruleId đã chọn
Không có điều kiện khớp Đi defaultNext bắt buộc
Câu bắt buộc nằm ở nhánh không đi qua Không yêu cầu trả lời; không tính thiếu đáp án
Chọn Khác nhưng thiếu văn bản Trả lỗi tại câu đó, không chuyển tiếp
Gửi optionId thuộc câu khác Từ chối đáp án
Sửa Q1 từ 2 thành 5 sao Loại đáp án Q2a, chuyển Q2b; thống kê cuối không tính Q2a
Nhánh đi thẳng END Kiểm bài và quyền thưởng trước chốt; không tự thưởng từ thao tác điều hướng
Hai cửa sổ lưu cùng revision Một cập nhật thắng, cửa sổ còn lại tải lại trạng thái
Gửi bài của version mới vào lượt cũ Từ chối; lượt cũ luôn dùng version đã ghim
Điều kiện/đích sai hoặc có vòng lặp Không cho phát hành

5.3. Tạo lời mời và gửi push

  1. Đến lịch, scheduler tạo run và chốt target theo nền campaign hiện có.
  2. Đọc target theo trang, tạo hoặc đọc lại lượt tham gia theo khóa duy nhất của run/người nhận.
  3. Tạo token ngẫu nhiên đủ mạnh, lưu hash để tra cứu. Bản token cần cho gửi lại được bảo vệ trong dữ liệu gửi có giới hạn thời gian hoặc dùng cơ chế tạo lại đã chốt; không chỉ lưu hash rồi giả định có thể lấy lại token. Replay cùng lời mời không được tự đổi link đã gửi. Link có hạn và không chứa số điện thoại hoặc dữ liệu cá nhân.
  4. Tìm thiết bị của khách theo đúng SP/app/môi trường, trạng thái hoạt động và quyền nhận push.
  5. Áp chính sách tất cả thiết bị hợp lệ hoặc thiết bị hoạt động gần nhất. Loại trùng registration trong phạm vi lần gửi.
  6. Tạo command ổn định theo lượt gửi/installation; dùng chung link lượt tham gia trên các thiết bị của một người.
  7. Worker kiểm tra lại chủ liên kết thiết bị, token và cấu hình app trước khi gửi. Token lỗi cũ không được làm vô hiệu token mới.
  8. Ghi kết quả theo từng thiết bị. Không có thiết bị là bỏ qua có lý do, không phải gửi thành công.

Firebase chấp nhận yêu cầu không đồng nghĩa người dùng đã nhận hoặc mở. Chỉ báo cáo delivered khi có nguồn xác nhận tương ứng. Run gửi không chờ người dùng trả lời khảo sát.

Các tình huống gửi lời mời:

Tình huống Xử lý
Process chết giữa batch Tiếp tục theo checkpoint sau ghi thành công, đọc lại invitation/command có sẵn
Không có thiết bị hợp lệ Ghi lý do không thể gửi; không tăng push accepted
Cùng token xuất hiện ở nhiều installation Loại trùng đúng phạm vi provider/app; lưu delivery liên quan để tra cứu
Đổi credentials khi đang gửi Tải cấu hình theo phiên bản/quy tắc đã chốt; không chuyển project ngoài app
Firebase lỗi hoặc timeout Áp chính sách command hiện có, phân biệt chưa rõ kết quả; không báo delivered

5.4. Mở link, nhập số và giữ suất

  1. Backend tra token, kiểm tra hạn, trạng thái campaign tiếp nhận và trạng thái lượt tham gia.
  2. Nếu đã hoàn thành thì trả kết quả đã lưu và trạng thái thưởng, không mở một lượt mới.
  3. Nếu có thưởng, hiển thị số lượng, đơn vị, điều kiện, thời hạn và ô nhập số nhận thưởng.
  4. Khi người dùng bấm kiểm tra: chuẩn hóa số, kiểm tra định dạng, giới hạn theo người/số, suất còn lại. Không gọi provider mỗi lần gõ.
  5. Chỉ kiểm điều kiện thuê bao từ xa nếu provider có API được xác nhận. Lỗi kết nối trả CHECK_UNAVAILABLE, không coi số là không đủ điều kiện.
  6. Sau kiểm tra, transaction tạo/cập nhật quyền thưởng HELD và giữ suất trong pool nếu còn hạn mức. Các kiểm tra ngoài hệ thống không nằm trong transaction.
  7. Trả thời điểm hết giữ suất cho giao diện. Khi hết hạn, yêu cầu kiểm tra và giữ lại trước khi tiếp tục nhận thưởng; không tự gia hạn vô hạn.

Đổi số nhận thưởng phải kiểm tra lại; chuyển khóa số và giữ suất trong transaction. Sau khi nộp thành công không được đổi số tự do. Không tự hứa chắc chắn provider sẽ cộng thành công chỉ vì đã giữ suất nội bộ.

Các tình huống kiểm tra số và giữ suất:

Tình huống Xử lý
Số sai định dạng hoặc không hỗ trợ Không giữ suất, trả lý do để sửa
Provider kiểm tra chậm/mất kết nối Trả chưa kiểm tra được; không coi đủ điều kiện hoặc không hợp lệ
Hai người cùng giữ một số Khóa duy nhất và thao tác ghi nguyên tử chỉ cho phép một quyền theo chính sách
Hai request tranh suất cuối Một request thành công, request còn lại báo hết suất
Request kiểm tra lặp Trả lại quyền HELD còn hạn, không giữ thêm
Đổi số khi đang giữ Kiểm tra lại và chuyển khóa nguyên tử; nếu thất bại giữ trạng thái cũ rõ ràng

5.5. Lưu nháp và nộp bài

  1. API lưu nháp chỉ ghi câu hỏi hợp lệ của đúng phiên bản và đường đi; giới hạn kích thước và revision để tránh ghi đè từ hai cửa sổ. Server tính lại nhánh khi sửa đáp án; trả đường đi mới và các đáp án bị loại theo mục 5.2.1.
  2. Khi nộp, tính lại đường đi từ câu bắt đầu, kiểm tra đáp án bắt buộc trên đường đó, giá trị, hạn nhận bài và trạng thái giữ suất nếu có thưởng. Dữ liệu client gửi cho nhánh không còn áp dụng không trở thành đáp án cuối hoặc dữ liệu thống kê.
  3. Trong một transaction: chốt COMPLETED, chuyển HELD sang QUEUED, chuyển lượng giữ sang chờ chi và ghi outbox yêu cầu thực thi thưởng. Khảo sát không thưởng chỉ chốt bài và sự kiện thống kê.
  4. Request lặp trả kết quả đã chốt. Không tạo thêm quyền thưởng, không cập nhật lại số hoặc đáp án đã nộp.
  5. Trả ngay trang cảm ơn và trạng thái thưởng đang xử lý; không chờ VCGW.

Nếu giữ suất hết hạn thì giữ bài nháp, thông báo rõ và cho kiểm tra lại; không âm thầm ghi hoàn thành có thưởng khi không còn quyền thưởng. Nếu không thể giữ lại, người dùng chỉ nộp không thưởng khi nghiệp vụ cho phép và họ xác nhận rõ; giai đoạn đầu mặc định không cho đổi như vậy.

Các tình huống nộp bài:

Tình huống Xử lý
Lưu nháp offline rồi gửi revision cũ Báo xung đột; không ghi đè đáp án mới
Hết giữ suất cùng lúc nộp Cập nhật có điều kiện trong cùng giao dịch xác định một kết quả; không vừa giải phóng vừa cấp thưởng
Bấm nộp hai lần hoặc mất response Đọc kết quả đã chốt, không tạo thêm event thưởng
Gửi thiếu câu bắt buộc trên đường đi Trả lỗi câu tương ứng, giữ bài nháp
Transaction rollback Không COMPLETED, không phát yêu cầu thưởng; cho phép nộp lại cùng công việc

5.6. Cộng thưởng và đối soát

[Diagram]

Các tình huống thực hiện thưởng:

Tình huống Xử lý
Hết quota provider Chờ có thời điểm kiểm lại, không drop command hoặc gọi vượt hạn mức
Thiếu số dư/đang chờ phí Giữ trạng thái chờ có lý do; chưa cho worker cộng thưởng
Thu phí xong nhưng action từ chối Giữ chứng từ phí, báo lỗi action; không tự hoàn phí
Chết trước/sau bước bắt đầu gọi Trước gọi phục hồi theo trạng thái; sau ghi bắt đầu mà chưa rõ thì đối soát, không gọi lại mù
Kết quả lặp hoặc đến ngược thứ tự Kiểm command/revision và chuyển trạng thái hợp lệ, không giảm/tăng pool hai lần
Provider không có API tra cứu Có danh sách cần xác minh thủ công và audit; không tự chuyển UNKNOWN thành FAILED

5.7. Hết hạn, đóng và dừng

Thao tác Xử lý
Hết hạn trả lời Không nhận bài mới; không hủy quyền thưởng đã chốt
Hết thời gian giữ suất Job giải phóng HELD bằng transaction, có kiểm tra trạng thái và thời gian
Dừng gửi campaign Ngừng lời mời chưa gửi theo quy tắc hiện có; không tự đóng link đã gửi
Đóng tiếp nhận khảo sát Thao tác riêng, chặn bắt đầu/nộp mới; bài và quyền thưởng cũ được giữ
Dừng khi có thưởng đang xử lý Không xóa giao dịch, không tự hoàn phí hoặc coi UNKNOWN thất bại
Thiết bị logout/đổi chủ Không gửi push cá nhân tới chủ mới; link cũ vẫn theo chính sách xác thực của lượt mời

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

Các collection dưới đây là thiết kế đề xuất cho MongoDB. ID và thời gian dùng quy ước hiện có của dự án; mọi truy vấn quản trị đều giới hạn theo spCode. Không nhúng danh sách người tham gia vào campaign/survey.

6.1. Các collection hiện có cần mở rộng

Collection Thay đổi
sp_config Gắn appId, environment, phiên bản cấu hình provider; giữ thông tin xác thực phía backend
app_installation Giới hạn đúng SP/app/môi trường; thêm quyền nhận push, thời điểm token cập nhật và phiên bản liên kết customer
campaign / snapshot run surveyVersionId, app, hạn trả lời, chính sách thiết bị, cấu hình thưởng và chính sách giới hạn
worker_command_state / execution Liên kết rewardId, participationId; lưu cố định người nhận, hành động, giá và nội dung gửi hệ thống thưởng
Outbox và chứng từ phí Thêm loại nguồn khảo sát; dùng chung cơ chế phát lại và đối soát

Index installation cần khớp truy vấn đăng ký và tìm thiết bị của khách. Migration kiểm dữ liệu trùng trước khi đổi unique index; không xóa dữ liệu để ép tạo index.

6.2. Thêm survey và survey_version

Collection Field chính Quy tắc
survey spCode, name, description, status, latestPublishedVersionId, revision, audit Quản lý nháp/phát hành/lưu trữ
survey_version spCode, surveyId, version, schemaVersion, logicVersion, startQuestionId, questions, displayConfig, publishedAt/By Bộ câu hỏi và quy tắc phát hành bất biến

Index survey: (spCode, status, createdAt DESC, _id DESC). Unique version: (spCode, surveyId, version).

Nhúng câu hỏi, option và quy tắc có giới hạn vào version; không tách collection mỗi câu hỏi ở giai đoạn đầu. Một câu có questionId, type, required, order, options, validation, rules và defaultNext. Một option có optionId, label, requiresText; rating có min/max/display; text có giới hạn ký tự. Quy tắc chứa ruleId, priority, nhóm điều kiện và target. Bộ quy tắc được kiểm tra và biên dịch thành cấu trúc tra cứu trong cache khi đọc version; không truy vấn Mongo cho từng điều kiện.

Giữ questionId/optionId khi ý nghĩa không đổi. Đổi ý nghĩa hoặc thang điểm phải dùng mã mới; mặc định báo cáo phân biệt version, không gộp nhầm thang 1–5 với 1–10. Mọi giới hạn câu hỏi, option, quy tắc, điều kiện và tổng kích thước được kiểm trước phát hành.

6.3. Thêm survey_participation

Một document cho một người trong một run; chứa cả lượt mời và bài trả lời.

Nhóm Field chính
Nguồn spCode, campaignId, runId, surveyVersionId, participantKey, customerId
Link tokenHash, expiresAt
Tiến độ status: INVITED / STARTED / COMPLETED / EXPIRED; revision
Đáp án answers có giới hạn, answerRevision, currentQuestionId, currentPath, appliedRuleIds, draftUpdatedAt, completedAt
Thưởng rewardEntitlementId; không sao chép lịch sử provider vào đây
Thời gian createdAt, firstOpenedAt, startedAt, updatedAt; customerIdAtInvitation và thông tin ứng dụng đã chốt

Index: unique (spCode, runId, participantKey), unique tokenHash, (spCode, campaignId, completedAt DESC, _id DESC).

Token trong link cấp quyền truy cập lượt tham gia, phải đủ ngẫu nhiên, không log token đầy đủ. Đáp án do backend kiểm theo version, không tin điểm/hoàn thành do client khai. Không lưu danh sách ngày càng dài các lần mở hoặc lưu nháp trong cùng bản ghi. Lượt trả lời hết hạn vẫn giữ theo retention, không TTL mặc định.

answers lưu theo questionId: optionIds, ratingValue hoặc textValue và otherText theo type; không nhận cả ba dạng cùng lúc. currentPath/appliedRuleIds là kết quả server tính có revision, không là nguồn cấu hình. Khi nộp lưu finalPath, đáp án cuối và thời gian hoàn thành bất biến. Không giữ mọi phiên bản đáp án nháp trong cùng document. Bản sửa nhánh phát sự kiện revision để thống kê biết trạng thái mới, không tăng số người trả lời hai lần.

6.4. Thêm reward_entitlement

Bản ghi giữ suất và quyền nhận thưởng, không phải chứng từ provider đã cộng.

Nhóm Field chính
Định danh rewardId, spCode, campaignId, runId, participationId, rewardDefinitionId
Giới hạn eligibilityScopeKey, participantKey, recipientPhoneNormalized, recipientVerificationStatus
Đã chốt rewardSnapshot: loại, lượng, đơn vị, thời hạn, nội dung, policyVersion
Kiểm tra eligibilityCheckedAt, eligibilityResultCode, holdUntil
Xử lý status, executionId, commandId, revision, thời gian và lý do kết quả

Trạng thái: HELD / EXPIRED / QUEUED / PROCESSING / SUCCEEDED / FAILED / UNKNOWN. Trường hợp bị từ chối trước giữ suất trả lý do, không tạo giao dịch cộng thưởng.

Unique (spCode, participationId, rewardDefinitionId) chống tạo lại quyền. Khi giữ lại suất hết hạn, cập nhật có điều kiện trên cùng bản ghi, trong một giao dịch; chưa có action mới chỉ vì giữ lại.

Mặc định giới hạn theo campaign: một người và một số nhận một phần thưởng một lần; run khác không tự mở lại quyền. eligibilityScopeKey ghi rõ phạm vi đã duyệt. Nếu đổi chính sách theo run phải chốt trước khi duyệt.

Unique có điều kiện theo (spCode, eligibilityScopeKey, rewardDefinitionId, participantKey) và khóa tương tự theo số chuẩn hóa, áp cho HELD/QUEUED/PROCESSING/SUCCEEDED/FAILED/UNKNOWN. EXPIRED đã giải phóng mới ra khỏi tập bị chặn. FAILED không tự mở quyền mới; xử lý tiếp dùng cùng hồ sơ và quy trình có audit.

Index job hết giữ suất: (status, holdUntil, _id) với điều kiện HELD. Index tra cứu: (spCode, campaignId, createdAt DESC, _id DESC). Index kiểm trạng thái chờ theo nextCheckAt nếu cần reconcile quyền thưởng. Index là thiết kế logic; kiểm tương thích partial filter với phiên bản MongoDB trước khi tạo.

6.5. Thêm campaign_reward_pool

Một document cho mỗi rewardDefinitionId trong campaign. Field: spCode, campaignId, rewardDefinitionId, unit, quantityPerReward, maxRewardCount, heldCount, committedCount, succeededCount, revision, audit.

Unique (spCode, campaignId, rewardDefinitionId). Giữ suất chỉ thành công khi heldCount + committedCount + succeededCount < maxRewardCount. Mỗi chuyển trạng thái entitlement cập nhật bộ đếm cùng transaction và chỉ một lần.

Giai đoạn đầu lượng thưởng cố định cho mỗi người; thống kê lượng bằng số suất nhân quantityPerReward. Phí tiền dùng ngân sách phí hiện có. UNKNOWN không giải phóng bằng timeout. Không tự xóa pool hoặc quyền thưởng theo thời gian để thay cho bước giải phóng suất.

6.6. Lịch sử thực thi và thống kê

Dùng worker_command_state hiện có cho trạng thái hiện tại, requestId, providerRequest, kết quả và đối soát. Khi triển khai, kiểm dữ liệu lịch sử hiện có. Nếu thiếu lịch sử nhiều lần kiểm tra/xử lý thì bổ sung collection dùng chung worker_command_attempt, không tạo bảng VCGW riêng cho khảo sát. Giới hạn kích thước request/response và loại secret.

ClickHouse bổ sung sự kiện khảo sát và quyền thưởng, có eventId, revision, spCode, campaignId, runId, participationId/rewardId, loại sự kiện và thời gian. Bộ nhận sự kiện phải xử lý trường hợp phát lại và cập nhật trạng thái, không cộng lại cùng chuyển đổi. Không dùng bảng thống kê để quyết định còn suất/ngân sách.

7. Hiệu suất và lưu trữ

Mục tiêu là xử lý nhiều người nhưng không để một yêu cầu tải toàn bộ dữ liệu hoặc làm quá tải hệ thống thưởng.

Công việc Cách thực hiện
Tạo lời mời, tìm thiết bị Đọc và ghi từng lô có giới hạn; lưu vị trí đã xử lý để tiếp tục sau khi khởi động lại
Danh sách và tra cứu Có index theo bộ lọc; phân trang bằng vị trí bản ghi cuối, không bỏ qua hàng triệu dòng để lấy trang sau
Đọc bộ câu hỏi Cache theo phiên bản đã phát hành; xử lý quy tắc trong bộ nhớ, không truy vấn database cho từng điều kiện
Firebase Dùng lại client theo app và phiên bản cấu hình, không khởi tạo cho mỗi thông báo
Lưu bài nháp Lưu khi chuyển câu/trang hoặc sau khoảng chờ; không ghi mỗi ký tự
Kiểm tra số Kiểm định dạng trước; chỉ gọi bên ngoài khi người dùng xác nhận; có thời gian chờ và giới hạn tốc độ
Cộng thưởng Nhiều worker xử lý song song nhưng dùng chung hạn mức provider; hàng chờ và số kết nối có giới hạn
Hết giữ suất, đối soát Lấy từng lô theo trạng thái và thời điểm cần xử lý; không quét toàn bộ dữ liệu mỗi lần chạy
Báo cáo Đọc số liệu tổng hợp đã chuẩn bị; xuất dữ liệu lớn bằng tác vụ nền

Pool thưởng là bản ghi dùng chung để kiểm soát hạn mức. Cần đo mức tranh chấp khi nhiều người giữ suất đồng thời. Chỉ chia hạn mức thành các phần khi cần thiết và vẫn bảo đảm tổng không vượt giới hạn. Redis không thay thế chứng từ giữ suất hoặc phí.

Không gọi hệ thống ngoài trong transaction MongoDB. Transaction chỉ ghi các thay đổi phải cùng thành công hoặc cùng thất bại, ví dụ chốt bài, chốt quyền thưởng và ghi yêu cầu gửi tiếp.

Thời gian lưu của bài trả lời, sự kiện hành vi và chứng từ thưởng/phí được quy định riêng. Không tự xóa trạng thái chưa rõ kết quả hoặc dùng chính sách lưu log để xóa dữ liệu khảo sát. Trước khi xóa phải xác định dữ liệu cần lưu trữ lâu dài và khả năng khôi phục báo cáo.

Dung lượng phụ thuộc số lượt mời, số thiết bị, số câu hỏi và thời gian lưu. Đo kích thước bản ghi cùng index trước khi quyết định cách chia dữ liệu giữa các máy chủ.

8. Ghi nhận hành vi và thống kê

8.1. Nguồn dữ liệu và luồng ghi nhận

Tách hai nhóm: hành vi giao diện cho biết người dùng tương tác thế nào; sự kiện nghiệp vụ xác nhận việc hệ thống đã chấp nhận. Hành vi client không được dùng để cấp thưởng hoặc làm chứng từ tiền.

[Diagram]

8.2. Danh mục sự kiện

Sự kiện Nơi phát Ý nghĩa và khóa chống trùng
INVITATION_CREATED Backend Một lượt mời; participationId
PUSH_ACCEPTED / PUSH_FAILED / PUSH_SKIPPED Backend worker Một kết quả delivery/command, có revision
PUSH_OPENED App hoặc link trung gian có xác thực Bấm thông báo; deliveryId và eventId; mở link khác không mặc định là mở push
SURVEY_OPENED Client sau khi trang hiển thị participationId, sessionId, eventId
SURVEY_STARTED Backend Lần bắt đầu làm bài hợp lệ, một lần mỗi participation
QUESTION_VIEWED Client Câu thực sự hiển thị; questionId, viewId, sessionId
ANSWER_SAVED Backend Đáp án nháp được chấp nhận; participationId và answerRevision
BRANCH_SELECTED Backend Quy tắc/đường chuyển được chấp nhận theo answerRevision
ELIGIBILITY_CHECKED Backend Mỗi lần kiểm tra đã hoàn tất; checkId, mã kết quả, nguồn kiểm tra
REWARD_HELD / REWARD_HOLD_EXPIRED Backend Chuyển trạng thái giữ suất, rewardId và revision
SURVEY_SUBMITTED Backend Hoàn thành thành công, một lần mỗi participation
REWARD_STATUS_CHANGED Backend Trạng thái thưởng mới nhất, rewardId và revision
FEE_STATUS_CHANGED Pipeline phí Chứng từ phí, command/batch và revision

BRANCH_SELECTED gắn với chuyển tiếp thực tế được server chấp nhận, không phát cho mọi cạnh khi tính lại toàn bộ đồ thị. Có thể đóng gói với ANSWER_SAVED để giảm số message nhưng vẫn có cùng ý nghĩa thống kê. Không phát sự kiện cho mỗi ký tự nhập.

8.3. Cấu trúc sự kiện và nhận diện người tham gia

Sự kiện có schemaVersion, eventId, eventName, producer, spCode, appId, environment, campaignId, runId, surveyId, surveyVersionId, participationId, occurredAt và receivedAt. Khi phù hợp có sessionId, sequenceNo, questionId, optionIds, ruleId, rewardId, deliveryId, entityRevision và reasonCode. Có businessTime do backend xác nhận và thời gian client riêng; không ghi đè bằng đồng hồ client.

customerIdAtEvent lấy từ liên kết người được mời/phiên đã xác thực tại thời điểm phát; không lấy chủ hiện tại của thiết bị để gán ngược lịch sử. Link bị chuyển tiếp chỉ xác định được lượt mời, chưa xác định người thật; lưu identityConfidence=INVITATION_LINK hoặc AUTHENTICATED. Ở trường hợp truy cập bằng link, customerId chỉ là khách được mời, chưa chứng minh chính khách đó đã thao tác.

Số nhận thưởng không trở thành danh tính hành vi của customer. Không đưa số đầy đủ, token, credentials, toàn bộ text trả lời hoặc đáp án nhạy cảm vào sự kiện hành vi chung. Báo cáo câu trả lời dùng nguồn dữ liệu riêng có phân quyền. Chỉ đồng bộ đáp án thành thuộc tính customer nếu có cấu hình mapping được duyệt, đúng kiểu/nguồn và có quyền; mặc định không ghi đè profile.

Khóa chống trùng sự kiện: (spCode, producer, eventId). Backend tạo eventId từ thực thể/chuyển trạng thái đã ghi; client tạo eventId một lần rồi tái sử dụng khi gửi lại. sequenceNo chỉ dùng sắp thứ tự trong session, không là khóa toàn cục. Consumer xử lý event đến trễ và ngược thứ tự bằng entityRevision, không cộng lại các trạng thái cũ.

8.4. Luồng giao diện và các trường hợp mất dữ liệu

  1. Sau khi trang/câu thực sự hiển thị, tạo sự kiện và xếp vào buffer có giới hạn.
  2. Gửi theo batch nhỏ khi chuyển câu, sau khoảng chờ hoặc khi app về nền. Việc flush lúc đóng trang chỉ là cố gắng, không bảo đảm thành công.
  3. Retry có giới hạn cùng eventId. Không giữ vô hạn sự kiện trên thiết bị. Theo dõi số bị bỏ do buffer đầy hoặc quá hạn, không chặn nộp bài vì gửi sự kiện giao diện bị lỗi.
  4. API kiểm quyền truy cập, schema, thời gian, kích thước, tốc độ; chuẩn hóa metadata server trước publish Kafka. Nếu một lô có phần lỗi, response phải chỉ rõ sự kiện nào đã nhận và sự kiện nào cần gửi lại.
  5. Đáp án dùng API riêng có revision. Lưu đáp án thành công và sự kiện ANSWER_SAVED được ghi nguyên tử bằng outbox; không phụ thuộc việc client gửi hành vi thành công.
Trường hợp Xử lý
Client gửi lại cùng event Một event hợp lệ trong thống kê
Đóng trang chưa flush Có thể thiếu view; bài/đơn thưởng đã chốt không mất
Event client nói hoàn thành Chỉ là hành vi không được chấp nhận làm SURVEY_SUBMITTED
Đồng hồ client sai Giữ clientTime riêng, dùng receivedAt/businessTime cho cửa sổ nghiệp vụ
Event mới tới trước cũ Bảng báo cáo giữ trạng thái có revision mới nhất; sự kiện gốc vẫn dùng để tra cứu
Bài nộp thành công nhưng báo cáo chậm Trang kết quả đọc Mongo; báo cáo hiển thị dataAsOf và ingest lag
Không xác định được người thật Vẫn thống kê lượt tham gia, không khẳng định là khách đã xác thực
Kafka/ClickHouse lỗi Outbox và consumer phục hồi; không mất quyền thưởng hoặc đổi kết quả bài

8.5. Các màn hình báo cáo

Màn hình Bộ lọc và nội dung
Tổng quan campaign khảo sát SP, campaign, run, app, phiên bản, khoảng thời gian; các bước từ mời đến thưởng
Kết quả câu hỏi Phiên bản, questionId, nhánh; số người được hỏi, trả lời, bỏ qua và phân bố đáp án
Luồng rẽ nhánh Số lượt đi từng nhánh theo đường cuối và số lần chuyển nhánh trong quá trình làm
Lượt tham gia Danh sách theo cursor, trạng thái, thời gian, đường đi, kết quả; chi tiết đáp án theo quyền
Phần thưởng và phí Pool, giữ suất, chờ chi, thành công, thất bại, chưa rõ; phí riêng và mã tra cứu
Vận hành Thời gian chờ, thời gian gọi hệ thống thưởng, mã lỗi, kết quả kiểm điều kiện và độ trễ báo cáo

Bộ lọc thời gian phải chỉ rõ đang xem nhóm được mời trong kỳ hay hoạt động phát sinh trong kỳ. Mặc định tổng quan chọn nhóm theo invitationCreatedAt; kết quả hoàn thành/thưởng của nhóm được cập nhật đến dataAsOf. Không lấy số hoàn thành hôm nay chia số được mời hôm nay nếu là hai nhóm khác nhau.

8.6. Định nghĩa số liệu và công thức

Số liệu Định nghĩa
Invited Số participation được tạo trong nhóm được chọn, không phải số thiết bị
Reachable Số participation có ít nhất một thiết bị đủ điều kiện tại lúc lập kế hoạch gửi
Push accepted Số delivery được Firebase chấp nhận; báo riêng số participation có ít nhất một accepted
Opened Số participation có SURVEY_OPENED; không đồng nghĩa bấm push
Started Số participation đã bắt đầu hợp lệ theo backend
Completed Số participation chốt COMPLETED từ backend
Completion rate Completed / Invited; nếu mẫu số 0 trả chưa xác định
Completion among started Completed / Started; tên riêng, không dùng thay completion rate
Elapsed completion time completedAt - startedAt; gồm thời gian rời trang, báo p50/p95
Active answering time Thời gian câu hiển thị khi trang đang active, có giới hạn mỗi khoảng; chỉ số ước tính từ client
Inactive STARTED chưa hoàn thành và quá inactivityThreshold đã cấu hình tại thời điểm báo cáo
Expired incomplete Chưa hoàn thành khi hết hạn nhận bài; chỉ số cuối, khác inactive có thể quay lại

Thời điểm bắt đầu là thao tác Bắt đầu được server chấp nhận: sau giữ suất với khảo sát có thưởng, trước câu đầu với khảo sát không thưởng. Người quay lại vẫn một Started. Inactive có thể trở lại và không cộng như một kết quả cuối độc lập với Completed. Khoảng thống kê ngày theo timezone thống nhất của ứng dụng, lưu mốc thời gian tuyệt đối ở nguồn.

Chỉ tính delivered khi có xác nhận từ nguồn hỗ trợ; nếu chưa có thì không hiển thị số suy đoán. Không cộng số delivery thành số người. Không dùng tỷ lệ sampling trace để tính thống kê khảo sát.

8.7. Thống kê câu hỏi và rẽ nhánh

Có hai cách xem riêng:

Với mỗi câu:

Loại Thống kê
Single-select Số chọn mỗi option / AnsweredCompleted; tổng tỷ lệ gần 100% sau làm tròn
Multi-select Số bài chọn option / AnsweredCompleted; tổng tỷ lệ có thể vượt 100%; một bài tính tối đa một lần cho mỗi option
Rating Phân bố mỗi điểm, số câu trả lời và trung bình; tách thang 1–5/1–10, không mặc định là NPS
Text Số trả lời/bỏ qua, danh sách nội dung có quyền truy cập; không index toàn text để tạo dashboard
Khác Số chọn option Khác; xem otherText ở danh sách riêng, không biến mỗi chuỗi thành một nhãn thống kê

Với rẽ nhánh, lưu số bài hoàn thành qua mỗi cạnh/ruleId và tỷ lệ trên số bài hoàn thành đi qua câu nguồn. Nếu hỏi về hành vi đổi nhánh, báo số lần BRANCH_SELECTED riêng; một bài có thể đổi nhiều lần nhưng chỉ một đường cuối. Hai nhánh gặp lại một câu không làm nhân đôi số bài ở câu đó.

Ví dụ Q1 có 100 bài hoàn thành: 30 chọn 1–3 đi Q2a, 70 chọn 4–5 đi Q2b. Mẫu số tỷ lệ đáp án Q2a là tối đa 30 bài được hỏi, không phải 100. Người đổi từ Q2a sang Q2b trước nộp chỉ tính Q2b trong báo cáo cuối.

8.8. Thưởng, phí và đối chiếu

8.9. Bảng báo cáo và cách chống đếm trùng

MongoDB giữ bài trả lời và trạng thái thưởng gốc. ClickHouse giữ dữ liệu phục vụ thống kê. Không tạo một bộ bảng riêng cho mỗi SP hoặc campaign.

Bảng đề xuất trong ClickHouse Nội dung Khóa để nhận diện
survey_event Sự kiện mở, xem câu, lưu đáp án, chuyển nhánh và xử lý nghiệp vụ spCode + producer + eventId
survey_participation_state Trạng thái mới nhất của mỗi lượt tham gia spCode + participationId, chọn revision mới nhất
survey_answer_final Đáp án cuối của bài nộp hợp lệ spCode + participationId + questionId + optionId hoặc loại giá trị
survey_reward_state Trạng thái mới nhất của mỗi quyền thưởng spCode + rewardId, chọn revision mới nhất
survey_report_aggregate Số liệu tổng hợp để hiển thị báo cáo nhanh SP/campaign/run/version/ngày và câu hỏi/đáp án nếu có

Luồng tạo báo cáo:

  1. Nhận sự kiện qua Kafka, kiểm mã sự kiện và phiên bản cập nhật để xử lý bản trùng hoặc đến trễ.
  2. Cập nhật dữ liệu của từng lượt tham gia và quyền thưởng theo trạng thái mới nhất. Sự kiện cũ không được làm lùi trạng thái.
  3. Với bài đã nộp, tạo dữ liệu đáp án từ kết quả server chấp nhận. Multi-select có nhiều dòng lựa chọn nhưng vẫn chỉ một người trả lời.
  4. Đánh dấu các nhóm số liệu bị thay đổi, ví dụ một campaign/run/ngày, rồi tính lại nhóm đó. Không quét mọi campaign khi nhận một sự kiện.
  5. Lưu kết quả tính thành một phiên bản tổng hợp mới, có mã phiên bản (generation) và thời điểm dữ liệu (dataAsOf). Chỉ cho báo cáo đọc khi phiên bản đã tính đủ và kiểm tra xong.
  6. Khi thay thế kết quả cũ, báo cáo chỉ chọn phiên bản đang được công bố. Không cộng cả bản cũ và bản mới.

Nội dung trả lời bằng văn bản vẫn lấy MongoDB làm nguồn chính. Nếu cần phân tích/xuất lượng text lớn thì thiết kế thêm bảng chi tiết có quyền và thời gian lưu riêng. Không đưa toàn bộ câu trả lời văn bản vào luồng hành vi chung.

Lưu ý khi triển khai ClickHouse:

Màn hình tổng quan đọc bảng tổng hợp; màn hình chi tiết đọc trạng thái hoặc dữ liệu gốc theo quyền. Xuất dữ liệu lớn chạy nền, giữ cố định bộ lọc, phiên bản khảo sát và mốc dữ liệu. Báo cáo trả thời điểm cập nhật và độ trễ; các quyết định cấp thưởng hoặc thu phí vẫn đọc MongoDB.

8.10. Khả năng mở rộng và các trường hợp thống kê

Tình huống Kết quả yêu cầu
Push nhiều thiết bị, mở nhiều lần Một invited/started/completed; delivery/view riêng
Multi-select có ba lựa chọn Một answered; ba lựa chọn, không ba người
Người đổi nhánh trước nộp Cuối chỉ có đáp án finalPath; lịch sử quá trình vẫn phân biệt
Bỏ câu tùy chọn Ghi skipped; không tính không áp dụng như bỏ câu
Nhánh kết thúc sớm Bài hợp lệ nếu đủ câu trên đường, không yêu cầu mọi câu của survey
Sự kiện gốc bị lặp trước khi ClickHouse gộp dữ liệu Dashboard và số liệu tài chính không tăng
Event cũ đến sau trạng thái mới Không hạ state từ COMPLETED về STARTED hoặc SUCCEEDED về PROCESSING
Hoàn thành hôm sau ngày gửi Báo cáo nhóm mời cập nhật đúng; báo cáo hoạt động ghi đúng ngày hoàn thành
Provider xác nhận sau UNKNOWN Một lần chuyển đúng bộ đếm; báo cáo bắt kịp theo revision
Đổi nhãn/ý nghĩa câu hỏi ở version khác Tách version; chỉ gộp bằng mapping được quản trị xác nhận

9. Các phần cần triển khai và thứ tự kết nối

Công việc được tách theo chức năng. Mỗi phần có dữ liệu, API, thay đổi database, ví dụ và tiêu chí kiểm tra riêng để triển khai song song sau khi thống nhất mục 10.

9.1. Quản lý bộ câu hỏi và cấu hình campaign

9.2. App, lời mời và trải nghiệm trả lời

9.3. Điều kiện và thực hiện thưởng

9.4. Hành vi, báo cáo và vận hành

Mốc Điều kiện hoàn thành
Hợp đồng chung Chốt cấu trúc dữ liệu, trạng thái, phiên bản quy tắc, cách ghi nguyên tử và phần phụ trách file chung
Phát triển độc lập Dùng version, app/provider và sự kiện mẫu có cùng hợp đồng; không cần chờ hệ thống thật
Ghép câu hỏi Tạo/preview/phát hành và làm bài đủ các nhánh, không thưởng
Ghép gửi và thưởng App đăng ký, push, giữ suất, nộp, worker và kết quả
Ghép thống kê Hành vi/finalPath/đáp án/thưởng/phí đúng khi sự kiện gửi lại hoặc đến trễ
Nghiệm thu Chạy toàn bộ mục 11, xác nhận provider trước bật thật

Mỗi nhóm sở hữu migration của mình; thống nhất số trước khi tạo và không sửa migration đã chạy. Thay đổi model/snapshot dùng chung đi qua một đầu mối của thành phần, tránh nhiều nhóm sửa cùng hàm lớn. Không cần thêm microservice chỉ để chia việc.

10. Đầu vào và đầu ra giữa các phần

Bảng dưới thống nhất dữ liệu trao đổi giữa các phần. Tên thao tác là tên đề xuất, không bắt buộc dùng nguyên tên cho API hoặc class.

Hợp đồng Đầu vào và kết quả Thành phần quản lý
ReadSurveyVersion versionId + SP → câu hỏi/quy tắc bất biến, schemaVersion/logicVersion Quản lý khảo sát
EvaluateSurveyPath version + đáp án + revision → đường hiện tại, rule, câu kế, lỗi và đáp án bị loại Bộ xử lý quy tắc dùng chung cho preview và làm bài
ReadSurveyCampaignSnapshot runId + SP → app, version, hạn, thưởng, policy Campaign
CheckAndHoldReward participationId + số + revision → rewardId, trạng thái, holdUntil, mã lý do Thưởng
FinalizeSubmission participationId + đáp án + revision → COMPLETED hoặc lỗi điều kiện API trả lời điều phối; dịch vụ thưởng tham gia transaction
ReadRewardStatus participationId đã xác thực → trạng thái và thông tin hiển thị Thưởng
SurveyRewardRequested eventId, SP, rewardId, participationId, campaign/run, policyVersion Outbox nghiệp vụ và dispatcher
IngestSurveyBehavior session/lượt tham gia đã xác thực + batch event → kết quả nhận từng phần Hành vi
SurveyFact / RewardFact eventId, revision, nguồn, trạng thái và thời gian Chủ dữ liệu phát; thống kê tạo dữ liệu báo cáo
ReadSurveyReport filter, chế độ nhóm/thời gian, cursor → số liệu, mẫu số, generation/dataAsOf Thống kê

FinalizeSubmission chạy trong cùng Mongo transaction: dịch vụ thưởng tham gia transaction hiện có, không tự commit riêng và không HTTP trong transaction. Outbox relay chỉ phát sau commit. Nếu Mongo triển khai không hỗ trợ transaction thì chưa bật luồng thưởng; không thay bằng các lệnh ghi rời.

Không gửi thông tin xác thực Firebase/VCGW vào Kafka. Yêu cầu thưởng tham chiếu rewardId; dispatcher đọc cấu hình đã chốt. Gói message có schemaVersion và ID ổn định. Mã lỗi tối thiểu: INVALID_PHONE, ALREADY_COMPLETED, REWARD_LIMIT_REACHED, NO_REWARD_CAPACITY, HOLD_EXPIRED, CHECK_UNAVAILABLE, SURVEY_CLOSED, INVALID_ANSWER, REVISION_CONFLICT. Status chờ phí khác lỗi provider.

11. Kiểm thử và nghiệm thu

Trường hợp Kết quả bắt buộc
Khảo sát không thưởng Gửi, mở, lưu nháp và hoàn thành không yêu cầu số
Sửa câu hỏi sau phát hành Lượt cũ vẫn đọc version đã ghim
SP/app khác Không đọc cấu hình, token hoặc dữ liệu của bên khác
Hai thiết bị cùng khách Một participation, nhiều delivery theo policy, một quyền thưởng
Hai người dùng một số Một lượt giữ/thưởng theo giới hạn; không vượt bằng request đồng thời
Hai request giữ suất cuối Chỉ một thắng, bộ đếm không âm hoặc vượt hạn mức
Hết giữ đồng thời với nộp Một chuyển trạng thái thắng; bài/entitlement/pool không lệch
Bấm nộp nhiều lần Một hoàn thành, một yêu cầu thưởng; trả kết quả cũ
Kafka hoặc result lặp Không tạo thêm command, thu phí hoặc tăng bộ đếm
Token đổi khi đang gửi Lỗi của token cũ không vô hiệu token mới
Logout hoặc đổi tài khoản Không gửi nội dung cá nhân dựa vào liên kết cũ
Provider từ chối rõ Ghi lý do; không báo thành công
Provider timeout hoặc process chết sau bắt đầu gọi UNKNOWN/đối soát, không tự gọi lại
Crash sau nộp trước phát Kafka Outbox phát bù cùng eventId
Thống kê nhận event lặp Số người/lượng thưởng/phí không bị nhân đôi
Campaign đóng/hết hạn Chặn bài mới đúng quy tắc, tiếp tục xử lý quyền thưởng đã chốt
Danh sách lớn Xử lý theo batch/cursor; kiểm pool/quota và dung lượng bằng số liệu đo
Bốn loại câu hỏi Đúng kiểu, bắt buộc, min/max, option và văn bản Khác
Rating 1–3/4–5 Đi đúng Q2a/Q2b hoặc END theo cấu hình
Multi-select khớp nhiều rule Priority xác định, preview và backend cùng kết quả
Sửa đáp án câu trước Đáp án nhánh cũ bị loại; required và finalPath tính lại
Câu không áp dụng Không bắt trả lời, không tính thiếu/bỏ qua trong báo cáo cuối
Cấu hình vòng lặp/đích thiếu Từ chối phát hành
Event hành vi giả hoàn thành Không cấp thưởng, không tăng Completed
Thiếu sự kiện giao diện Chỉ số ước tính có giới hạn; bài nộp và thưởng không mất
Multi-select và đổi nhánh Mẫu số/đáp án cuối đúng; không đếm nhiều người từ option hoặc view
Báo cáo trước khi ClickHouse gộp dữ liệu Event lặp không nhân số liệu, state lấy revision mới nhất
Sự kiện đến trễ và tính lại báo cáo Chỉ công bố bản tổng hợp đã tính đủ, không cộng cả bản cũ và bản mới

Kiểm thử dùng provider giả lập và tài khoản được phép; không cộng thưởng thật chỉ để kiểm chứng phần mềm. Bật môi trường thật sau khi xác nhận hợp đồng provider, kết quả đối soát và cấu hình phí.

12. Các điểm cần xác nhận trước khi triển khai thật

Có thể phát triển với các hợp đồng mẫu trong lúc xác nhận, nhưng không bật adapter kiểm tra hoặc cộng thưởng thật bằng giả định chưa được chứng minh.