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.
| 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.
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 |
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 |
| 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ụ:
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ị:
Luồng trả lời:
| 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 |
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 |
Đổ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 |
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 |
campaign.cost.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 |
| 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 |
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.
| 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.
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.
survey_participationMộ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.
reward_entitlementBả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.
campaign_reward_poolMộ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.
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.
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ủ.
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.
| 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.
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ũ.
| 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 |
| 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.
| 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.
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.
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:
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.
| 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 |
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.
| 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.
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.
| 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í.
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.