Bỏ qua nội dung

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_limit hằ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_limit mỗ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

  • 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.
  • người xem Freemium đã hết hạn mức, tôi muốn trả V-Credit để mở thêm số liên hệ.
  • 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.
  • 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.
  • 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.
  • 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ở.
  • 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.
  • 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
    end

Luồ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
    end

Acceptance 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_limit nế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_limit trong ngày, listing tiếp theo hiển thị phone_masked và cần V-Credit để mở.
  • AC-15: daily_unlock_limit không áp dụng cho user gói trả phí; POST /listings/{id}/unlock trả 403 nếu user đang có gói Paid.
  • AC-16: daily_listing_distribution_limit khô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-status trả đủ 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: FREEMIUM hoặc PAID. 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_limit củ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ở qua POST /listings/{id}/unlock, trừ một lượt, nguồn DAILY_FREE.
  • Nếu hết daily_unlock_limit: POST /listings/{id}/unlock trả 409; user phải chủ động gọi POST /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_limit củ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_limit trong ngày, listing tiếp theo hiển thị phone_masked; user phải dùng V-Credit để mở.
  • POST /listings/{id}/unlock trả 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_UNLOCK và 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ại ADDRESSFULL là 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ếu MASKING_UNLOCK.

Nguồn mở khóa (enum UnlockSource):

  • DAILY_FREE — mở bằng daily_unlock_limit hằ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ằng daily_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_limit cho Freemium và daily_listing_distribution_limit cho Paid); thông báo dùng sự kiện UNLOCK_PHONE. Các phần này hiện chưa tách thành feature doc riêng.