Mở khóa số điện thoại trên listing
Feature URL
Module
masking_unlocks
Status
shipped
Priority
P0
Platforms
fe · be
AC progress
12 / 18
Last reviewed
2026-05-30
Mục tiêu
Số điện thoại của chủ tin đăng bị che mặc định với người xem khác để bảo vệ thông tin liên hệ và tạo điểm kiếm tiền cho nền tảng. Cơ chế mở khóa phân theo loại gói của người xem:
- Gói Freemium: mỗi ngày được hạn mức mở miễn phí (
daily_unlock_limit) theo gói đăng ký. Khi hết hạn mức, người xem trả V-Credit để mở thêm. - Gói trả phí: hệ thống tự động phân phối phone_full cho một số lượng listing mỗi ngày (
daily_listing_distribution_limit). Các listing vượt giới hạn phân phối hiển thị phone_masked; người xem phải dùng V-Credit để mở.
Chủ tin luôn xem được số của chính mình mà không tốn quota hay credit.
Phạm vi
Trong phạm vi (In scope):
- Che số điện thoại của chủ tin với người xem không phải chủ và chưa mở khóa.
- Phân loại người xem theo gói đăng ký (Freemium / Paid) để áp dụng cơ chế quota đúng.
- Freemium: mở khóa bằng
daily_unlock_limithằng ngày; khi hết quota thì yêu cầu V-Credit. - Gói trả phí: phân phối phone_full tự động trong
daily_listing_distribution_limitmỗi ngày; listing vượt giới hạn cần V-Credit để mở. - Chủ tin mở khóa số của chính mình miễn phí, không tốn quota và credit.
- Mở khóa bằng V-Credit khi hết quota (Freemium) hoặc vượt phân phối (Paid).
- Tra cứu trạng thái mở khóa: loại gói, quota còn lại theo loại gói, số dư ví, bảng giá mở.
- Tính idempotent: đã mở hoặc đã được phân phối rồi thì lần sau không tốn thêm.
- Lịch sử mở khóa khi dùng V-Credit.
- Thông báo cho chủ tin khi người khác mở số của họ.
- Reset quota theo ngày.
Ngoài phạm vi (Out of scope):
- Nạp V-Credit vào ví, xem feature
wallet-topup-create. - Quản lý gói đăng ký và cấu hình
daily_unlock_limit/daily_listing_distribution_limit(thuộc subscription/packages). - Che/mở địa chỉ (legacy, không còn dùng).
- Tạo/sửa listing và hiển thị danh sách listing.
- UI chi tiết màn ví.
User Stories
- Là người xem Freemium còn hạn mức, tôi muốn dùng
daily_unlock_limitđể xem số liên hệ mà không tốn tiền. - Là người xem Freemium đã hết hạn mức, tôi muốn trả V-Credit để mở thêm số liên hệ.
- Là người xem gói trả phí trong giới hạn phân phối ngày, tôi muốn tự động thấy phone_full mà không cần thao tác mở khóa.
- Là người xem gói trả phí đã vượt phân phối ngày, tôi muốn trả V-Credit để xem số điện thoại của listing chưa được phân phối.
- Là chủ tin đăng, tôi muốn luôn xem được số của chính mình mà không tốn quota hay credit.
- Là người xem, tôi muốn biết loại gói, quota còn lại và số dư ví trước khi quyết định mở.
- Là người xem đã mở một listing, tôi muốn xem lại số đó mà không bị tính phí lần nữa.
- Là chủ tin, tôi muốn được thông báo khi có người mở số của tôi để biết mức độ quan tâm.
Luồng chức năng
Luồng Freemium:
sequenceDiagram
actor Viewer as Người xem Freemium
participant App as FE/Mobile
participant BE
actor Owner as Chủ tin
Viewer->>App: Mở listing
App->>BE: GET unlock-status
BE-->>App: Gói Freemium, daily_unlock_limit còn lại, số dư ví
alt Là chủ tin
BE-->>App: phone_full miễn phí, chủ sở hữu
App-->>Viewer: Hiện phone_full
else Còn daily_unlock_limit
App-->>Viewer: Hiện phone_masked
Viewer->>App: Bấm mở khóa
App->>BE: POST unlock
BE->>BE: Trừ một lượt daily_unlock_limit
BE-->>App: Mở thành công, nguồn DAILY_FREE
App-->>Viewer: Hiện phone_full
BE-->>Owner: Thông báo mở số
else Hết daily_unlock_limit
App-->>Viewer: Hết quota, cần V-Credit
Viewer->>App: Xác nhận trả V-Credit
App->>BE: POST unlock-credits
alt Đủ số dư V-Credit
BE->>BE: Trừ V-Credit và ghi giao dịch ví
BE-->>App: Mở thành công, nguồn CREDIT
App-->>Viewer: Hiện phone_full
BE-->>Owner: Thông báo mở số
else Không đủ số dư
BE-->>App: 409 thiếu V-Credit
App-->>Viewer: Báo thiếu V-Credit
end
endLuồng gói trả phí:
sequenceDiagram
actor Viewer as Người xem Paid
participant App as FE/Mobile
participant BE
actor Owner as Chủ tin
Viewer->>App: Mở listing
App->>BE: GET unlock-status
BE-->>App: Gói Paid, daily_listing_distribution_limit còn lại
alt Là chủ tin
BE-->>App: phone_full miễn phí, chủ sở hữu
App-->>Viewer: Hiện phone_full
else Trong daily_listing_distribution_limit
BE-->>App: phone_full tự động (trong phân phối ngày)
App-->>Viewer: Hiện phone_full (phân phối sẵn)
else Vượt daily_listing_distribution_limit
App-->>Viewer: Vượt phân phối, cần V-Credit
Viewer->>App: Xác nhận trả V-Credit
App->>BE: POST unlock-credits
alt Đủ số dư V-Credit
BE->>BE: Trừ V-Credit và ghi giao dịch ví
BE-->>App: Mở thành công, nguồn CREDIT
App-->>Viewer: Hiện phone_full
BE-->>Owner: Thông báo mở số
else Không đủ số dư
BE-->>App: 409 thiếu V-Credit
App-->>Viewer: Báo thiếu V-Credit
end
endAcceptance Criteria
- AC-1: Số điện thoại của chủ tin bị che mặc định với người xem không phải chủ và chưa mở khóa.
- AC-2: Chủ tin xem được số đầy đủ trên listing của chính mình, không tốn quota hay credit.
- AC-3: (Freemium) Người xem có thể mở khóa bằng
daily_unlock_limitnếu còn lượt; mỗi lần mở trừ đúng một lượt. - AC-4: (Freemium) Khi hết
daily_unlock_limit, yêu cầu mở miễn phí trả 409 (hết hạn mức) và không tự động trừ credit. - AC-5: Người xem có thể mở khóa bằng V-Credit; hệ thống trừ credit theo giá cấu hình và ghi một giao dịch ví với loại tham chiếu
MASKING_UNLOCK. - AC-6: Khi số dư V-Credit không đủ, yêu cầu mở bằng credit trả 409 và không thay đổi số dư ví.
- AC-7: Mở khóa có tính idempotent: nếu user đã mở một listing, các lần gọi sau trả lại kết quả cũ và không trừ thêm quota hay credit.
- AC-8: API tra trạng thái trả về loại gói, trạng thái mở khóa, quota còn lại theo loại gói, số dư ví và bảng giá mở; không phát sinh phí.
- AC-9: Khi người xem không phải chủ mở số thành công, hệ thống gửi thông báo cho chủ tin.
- AC-10: (Freemium)
daily_unlock_limitđược reset theo ngày dựa trên hạn mức gói đăng ký đang áp dụng. - AC-11: Một user chỉ có một bản ghi mở khóa cho mỗi cặp listing và loại mở; không tạo bản ghi trùng.
- AC-12: Mọi thao tác mở khóa yêu cầu user đã đăng nhập và có quyền tương ứng (
masking_unlock:useđể mở,masking_unlock:read:ownđể xem trạng thái). - AC-13: (Paid) User gói trả phí xem listing trong
daily_listing_distribution_limit→ hệ thống trả phone_full tự động, không cần thao tác mở khóa. - AC-14: (Paid) Khi user gói trả phí vượt
daily_listing_distribution_limittrong ngày, listing tiếp theo hiển thị phone_masked và cần V-Credit để mở. - AC-15:
daily_unlock_limitkhông áp dụng cho user gói trả phí;POST /listings/{id}/unlocktrả 403 nếu user đang có gói Paid. - AC-16:
daily_listing_distribution_limitkhông áp dụng cho user Freemium; user Freemium không nhận phone_full tự động theo cơ chế phân phối Paid. - AC-17: Không trừ V-Credit nếu listing đã được phân phối sẵn (Paid, trong quota ngày) hoặc đã mở khóa trước đó.
- AC-18:
GET /listings/{id}/unlock-statustrả đủ thông tin để FE phân biệt: đã hiển thị phone_full (phân phối / đã mở / chủ tin), cần V-Credit để mở, hoặc hết quota trong ngày.
Quy tắc nghiệp vụ
Phân loại gói:
- Hệ thống xác định loại gói của người xem:
FREEMIUMhoặcPAID. Tài khoản chưa có gói đăng ký được coi làFREEMIUM. - Mỗi loại gói áp dụng cơ chế quota riêng; không pha trộn quota giữa hai loại.
Cơ chế Freemium:
- Quota miễn phí hằng ngày dùng
daily_unlock_limitcủa gói đăng ký đang áp dụng (mặc định 0 khi chưa có gói). - Nếu còn
daily_unlock_limit: mở quaPOST /listings/{id}/unlock, trừ một lượt, nguồnDAILY_FREE. - Nếu hết
daily_unlock_limit:POST /listings/{id}/unlocktrả 409; user phải chủ động gọiPOST /listings/{id}/unlock-credits.
Cơ chế Paid:
- Hệ thống tự động phân phối phone_full cho mỗi listing user mở trong giới hạn
daily_listing_distribution_limitcủa gói. - Listing được phân phối phone_full tự động; không yêu cầu thao tác mở khóa từ phía user.
- Khi đã hết
daily_listing_distribution_limittrong ngày, listing tiếp theo hiển thị phone_masked; user phải dùng V-Credit để mở. POST /listings/{id}/unlocktrả 403 với user gói Paid (endpoint này chỉ dành cho Freemium).
Quy tắc chung:
- Chủ tin mở số trên listing của mình luôn miễn phí, không tốn quota và không tốn credit.
- Mở miễn phí và mở bằng credit là hai thao tác tách biệt; hệ thống không tự động chuyển sang trừ credit khi hết quota — client phải chủ động gọi endpoint riêng.
- Mở khóa có tính idempotent: listing đã được phân phối hoặc đã mở khóa sẽ trả kết quả cũ, không tốn thêm quota hay credit.
- Không trừ V-Credit nếu listing đã được phân phối sẵn (Paid, trong quota ngày) hoặc đã có bản ghi mở khóa trước đó.
- Mở khóa không có thời hạn hết hiệu lực: đã mở là user đó xem được mãi trên listing đó.
- Giá mở số bằng credit lấy từ cấu hình runtime
app.unlock.price.phone-credits, mặc định 5 credit. - Mỗi lần trừ credit phải tạo giao dịch ví ghi số dư trước và sau, số credit trừ, loại tham chiếu
MASKING_UNLOCKvà id bản ghi mở khóa, phục vụ đối soát. - Mỗi bản ghi mở khóa gắn đúng một user, một listing và một loại mở; bộ ba này là duy nhất.
- Hiện tại chỉ hỗ trợ loại mở
PHONE; loạiADDRESSvàFULLlà legacy, không còn dùng. - Số điện thoại khi bị che hiển thị dạng ẩn các chữ số cuối, ví dụ
+841234****.
Dữ liệu & Trạng thái
Entity nghiệp vụ:
MaskingUnlock: bản ghi mỗi lần mở khóa — ai mở, listing nào, loại mở, nguồn mở (DAILY_FREE,CREDIT), số credit đã tiêu, số điện thoại được lộ, thời điểm mở.UnlockRequest: yêu cầu mở khóa từ client, gồm loại mở (hiện chỉPHONE).MaskingUnlockResponse: kết quả mở khóa — id bản ghi, user, listing, loại mở, nguồn, số credit đã tiêu, số điện thoại lộ, thời điểm.UnlockStatusResponse: trạng thái mở khóa của user trên listing — loại gói (FREEMIUM/PAID), thông tin quota theo loại gói, bảng giá mở, số dư ví, phone (masked hoặc full).DailyUnlockQuota(Freemium): hạn mức miễn phí theo ngày —daily_unlock_limit,used,remaining, ngày hiệu lực.DailyDistributionQuota(Paid): hạn mức phân phối theo ngày —daily_listing_distribution_limit,distributed,remaining, ngày hiệu lực.WalletTransaction: giao dịch trừ credit khi mở bằng V-Credit, với loại tham chiếuMASKING_UNLOCK.
Nguồn mở khóa (enum UnlockSource):
DAILY_FREE— mở bằngdaily_unlock_limithằng ngày (Freemium); hoặc chủ tin mở miễn phí.CREDIT— mở bằng cách trừ V-Credit (áp dụng cho mọi user khi hết quota tương ứng).
Endpoint BE:
GET /listings/{id}/unlock-status— xem trạng thái mở khóa: loại gói, trạng thái phone (masked/full), quota còn lại theo loại gói, số dư ví, bảng giá mở; không phát sinh phí.POST /listings/{id}/unlock— (Freemium only) mở khóa bằngdaily_unlock_limit; trả 403 nếu user đang có gói Paid, trả 409 nếu hết hạn mức.POST /listings/{id}/unlock-credits— mở khóa bằng V-Credit; áp dụng cho Freemium (hết quota) và Paid (vượt phân phối); trả 409 khi không đủ số dư.
Liên quan
- Phụ thuộc: auth-login-phone, wallet-topup-create
- Ảnh hưởng: Chưa có
- Ghi chú nghiệp vụ: phụ thuộc vào Listings (đối tượng che/mở số) và Subscription Packages (cấp
daily_unlock_limitcho Freemium vàdaily_listing_distribution_limitcho Paid); thông báo dùng sự kiệnUNLOCK_PHONE. Các phần này hiện chưa tách thành feature doc riêng.