Bỏ qua nội dung

Phân bổ listings cho broker theo gói

Feature URL
Module
listing
Status
draft
Priority
P1
Platforms
fe · be
AC progress
0 / 10
Last reviewed
2026-05-29

Mục tiêu

Bài toán

Khi có tin BĐS mới, Backend (sau khi tin chuẩn + có trust_score) xây list_broker — danh sách broker đủ điều kiện nhận phân bổ tự động cho tin đó — rồi chọn tối đa 3 môi giới cùng gói, cùng phường để mở số sẵn. Môi giới khác nếu được phép xem marketplace thì thấy tin trong listings nhưng số ẩn.

Thuật toán phân bổ (theo từng tin)

Điều kiện chạy: listing ACTIVE, có phone_full, có listing.ward, trust_score đã tính xong, trust_score thuộc band phân bổ 50–79.

Bước 0 — list_broker (danh sách broker liên quan tin)

Backend tạo một danh sách list_broker cho từng listing. Đây là danh sách broker đủ điều kiện để xếp hạng và nhận slot mở số tự động.

Điều kiện vào list_brokerÝ nghĩa
Gói broker nằm trong đúng band trust_score của listingPremium 50–59, Pro 60–69, MAX 70–79
Broker cùng phường với listinglisting.ward khớp BrokerOperatingWard
Broker paid activeChỉ Premium / Pro / MAX còn hiệu lực
Broker chưa đạt daily_listing_distribution_limitCòn có thể nhận slot mở số trong ngày

Broker sai phường, sai band gói, gói Free, hoặc đã đạt daily_listing_distribution_limit thì không nằm trong list_broker của listing đó.

Kết quả sau khi rank list_brokerHiển thị
Top tối đa 3 broker theo fill_ratio thấp nhấtCó row listing_distributions, phone mở sẵn
Broker còn lại trong list_brokerListings có tin, phone ẩn

Bước 1 — Xác định đúng một gói (tier) theo trust_score

Một tin chỉ gán slot cho một tier: Premium hoặc Pro hoặc MAX (không trộn Pro + MAX trên cùng tin).

trust_score quyết định trực tiếp tin thuộc pool gói nào. Pool không chồng lấn; gói cao hơn không lấy tin thuộc pool thấp hơn.

trust_score của listingPool phân bổ tự động
50–59Premium
60–69Pro
70–79MAX
≥ 80Admin Archive; không phân bổ broker

Listing ngoài các band trên thì không tạo list_broker để phân bổ tự động. Nếu list_broker có 1–2 ứng viên thì gán 1–2; nếu không có ứng viên thì không gán slot tự động. Không thử tier khác để bù đủ 3 slot.

Bước 2 — Trong tier đã chọn: 3 slot, ưu tiên fill_ratio

Trong nhóm cùng tier + cùng phường vừa chọn:

  • Tính fill_ratio = distributed_today ÷ daily_listing_distribution_limit (AC-17).
  • Xếp hạng: fill_ratio thấp trước (broker cần được phân bổ hơn) → tie-break deterministic (AC-9).
  • Chọn tối đa 3 broker đầu → insert listing_distributionsphone_full mở sẵn.
Ứng viên trong tier đã chọnHành vi
≥ 3Gán đủ 3 broker cùng gói
1 hoặc 2Gán 1 hoặc 2 (không trộm tier khác để bù đủ 3)
0Không gán slot tự động

Bước 3 — Broker còn lại

NhómHiển thị
Cùng tier nhưng hạng 4+ trong tier đã chọnListings có tin — số ẩn
Gói khác với pool trust_score của tinKhông nhận slot tự động cho tin đó
Đã đạt daily_listing_distribution_limitListings có tin (nếu eligible xem) — số ẩn
FreeKhông slot auto; listings + unlock thủ công / V-credit

Diễn đạt: “3 slot mở số cùng gói” / “broker thứ 4 trở đi (trong cùng gói hoặc gói khác)”.

Ví dụ (tin A — Phường 12, trust_score = 55, 5 broker Premium cùng phường, chưa đạt limit):

BrokerGóifill_ratioKết quả
P1Premium0,10Slot 1 — số mở
P2Premium0,30Slot 2 — số mở
P3Premium0,50Slot 3 — số mở
P4Premium0,70Listings — số ẩn
P5Premium0,90Listings — số ẩn

Ví dụ (tin B — Phường 12, trust_score = 65: 2 MAX + 6 Pro cùng phường):

  • Premium không trong list_broker (65 ≥ 60).
  • Tin thuộc pool Pro60–69; không xét MAX để nhận slot.
  • Trong 6 Pro cùng phường, chọn 3 Pro có fill_ratio thấp nhất → cùng gói Pro.
  • 3 Pro còn lại: thấy tin B, số ẩn.
  • 2 broker MAX không nhận slot cho tin B vì MAX chỉ nhận pool 70–79.

Ví dụ (10 tin mới trong ngày, broker Premium daily_listing_distribution_limit = 10):

  • Theo từng tin: mỗi tin trong 10 tin chỉ tạo tối đa 3 row listing_distributions (3 broker khác nhau được mở số cho tin đó).
  • Theo từng broker: một môi giới Premium tối đa 10 lần được chọn vào slot mở số trên 10 tin khác nhau trong ngày (mỗi lần tiêu thụ 1 trong limit 10).
  • Broker Premium là #4 trên tin X → tin X vẫn trong listings của họ, số ẩn; không tính vào 10 tin đã mở số trừ khi sau đó V-credit unlock.
Lớp hiển thịĐiều kiệnSố điện thoại
Có slot mở sốMột trong 3 broker cùng gói + cùng phường được chọn theo fill_ratioMở sẵn
Chỉ xem listingsBroker không được chọn slot nhưng vẫn được phép xem marketplaceẨn

Môi giới sai phường / sai pool → không thấy tin trong listings.

Ai được gán tin tự động?

Chỉ môi giới đang dùng gói trả phí Premium, Pro hoặc MAX (còn hiệu lực). Gói Free không tham gia luồng gán tự động.

daily_listing_distribution_limit — khác với cap 3 broker/tin

daily_listing_distribution_limit: số tin tối đa mỗi môi giới được hệ thống tự mở số (listing_distributions) trong một ngày (timezone Asia/Ho_Chi_Minh). Đếm theo số row (user_id, date) — mỗi tin được gán = tiêu thụ 1 trong giới hạn.

Góidaily_listing_distribution_limit (tin tự mở số / ngày)
Premium10
Pro20
MAX30
Free0 (không auto-phân bổ)

Chưa đạt limit = count row hôm nay < daily_listing_distribution_limit → vẫn có thể được chọn vào 1 trong 3 slot mở số của tin mới.

Đã đạt limit = số row hôm nay daily_listing_distribution_limitkhông thêm row phân bổ; tin eligible vẫn hiện trong listings (số ẩn), unlock bằng V-credit.

Ví dụ: Môi giới Pro (daily_listing_distribution_limit = 20). 9h đã có 20 row phân bổ → đã đạt limit. Tin mới 10h → không auto mở số; chỉ listings (số ẩn). Ngày mới reset → lại tối đa 20.

Hai cap độc lập (dễ nhầm):

CapField / giá trị
3 broker / tinmax_brokers_per_listing = 3
Tin tự mở số / broker / ngàydaily_listing_distribution_limit = 10 / 20 / 30 theo gói

Ưu tiên broker theo tỷ lệ đã phân bổ / tổng cần phân bổ

Áp dụng mỗi khi một tin mới sẵn sàng phân phối (sau chấm trust_score), trong bước chọn 3 slot của tin đó.

Ký hiệuCông thức (theo date hiện tại, timezone Asia/Ho_Chi_Minh)
Đã phân bổdistributed_today = COUNT(listing_distributions) WHERE user_id = broker
Tổng cần phân bổdaily_listing_distribution_limit của gói broker (10 / 20 / 30)
Tỷ lệ lấp đầyfill_ratio = distributed_today ÷ daily_listing_distribution_limit (0 → chưa nhận tin nào; 1 = đã đạt limit)

Quy tắc xếp hạng: chỉ áp dụng trong cùng tier đã chọn (cùng daily_listing_distribution_limit). fill_ratio thấp hơn → ưu tiên cao hơn. Không so fill_ratio giữa Pro và MAX để chọn slot trên cùng tin.

Ví dụ (cùng Premium, limit = 10, cùng phường + pool, tin mới vừa chấm xong TS):

BrokerĐã phân bổTổng cầnfill_ratioThứ tự ưu tiên slot
A1100,10#1
B5100,50#2
C9100,90#3
D10101,00Loại — đã đạt limit

→ Tin này gán slot cho A, B, C (đủ 3). D thấy tin trong listings, số ẩn.

Khi nhiều tin liên tiếp vào Data Marketplace trong ngày, mỗi tin chạy lại bước tính fill_ratio trên snapshot distributed_today tại thời điểm xử lý tin đó — broker đang thiếu quota dần được bù qua các tin sau.

Hai điều kiện để gán tin cho một môi giới

Cả hai phải đúng cùng lúc:

  1. Đúng phường — Phường/xã của tin (listing.ward) trùng với ít nhất một phường trong danh sách môi giới đã khai báo (BrokerOperatingWard). Mặc định: khớp đúng tên phường (phường lân cận có được tính hay không — chưa chốt).

  2. Đúng band điểm tin cậy theo gói — Mỗi tin có trust_score (0–100), do Backend chấm khi nhận tin (listing-crawl-external-sources). Gói không làm thay đổi điểm tin; trust_score quyết định tin thuộc đúng một pool gói, không chồng lấn.

Gói môi giớiChỉ gán tin có trust_score
PremiumTừ 50 đến 59
ProTừ 60 đến 69
MAXTừ 70 đến 79
GóiHành vi
FreeKhông gán tự động. Tự unlock 3 tin/ngày hoặc trả V-credit (tin dưới 80 điểm trong Data Marketplace).
Tin ≥ 80 điểmKhông gán cho môi giới nào — chuyển kho Admin; không unlock bằng V-credit.

Ví dụ: Tin ở Phường 12, trust_score = 65.

  • Môi giới Premium chỉ làm Phường 12không gán (65 không thuộc band Premium 50–59).
  • Môi giới Pro cùng phường → gán, số mở sẵn.
  • Môi giới MAX cùng phường → không gán tự động cho tin này (65 thuộc pool Pro, không thuộc pool MAX).
  • Môi giới Free → không gán tự động; vẫn có thể thấy tin trong danh sách chung (số ẩn) và tự unlock.

Khi đã đạt daily_listing_distribution_limit

Môi giới không nhận thêm tin tự mở số (không thêm row listing_distributions). Tin tiếp theo (đúng phường + pool) vẫn hiện trong danh sách, số ẩn — unlock bằng V-credit nếu đủ điều kiện.

Cam kết

  • Hệ thống đảm bảo tối đa daily_listing_distribution_limit tin/ngày tự mở số (10 / 20 / 30) — không hứa đủ limit nếu thiếu tin phù hợp.
  • Không nới phường, đổi band trust_score, hoặc thử tier khác để bù quota.
  • Đổi gói (nâng/hạ) → quy tắc mới chỉ áp tin mới sau khi gói đổi; tin đã xử lý trước đó không gán lại ngược (không retroactive).

Phạm vi

In scope:

  • Bảng listing_distributions (listing_id, user_id, date) — timezone Asia/Ho_Chi_Minh.
  • list_broker sau khi tin chuẩn + có trust_score; chỉ gồm broker đúng band gói theo điểm (Premium 50–59, Pro 60–69, MAX 70–79), cùng phường, paid active và chưa đạt daily_listing_distribution_limit; rank fill_ratio trong danh sách này (AC-4d, AC-9, AC-17).
  • Cap max_brokers_per_listing = 3 — số broker được mở số tự động cho một listing (config cố định).
  • Config daily_listing_distribution_limit: Premium 10, Pro 20, MAX 30 tin tự mở số / broker / ngày.
  • Ranking slot: fill_ratio = distributed_today / daily_listing_distribution_limit — ưu tiên broker thiếu quota (AC-17).
  • list_broker; trust_score chọn một tier không chồng lấn / tin; tối đa 3 broker trong list_broker được mở số theo fill_ratio.
  • Broker có row phân bổ → phone_full mở sẵn. Thấy tin nhưng không có row → phone ẨN + unlock (V-credit / Free).
  • Free tier: 3 unlock thủ công/ngày + V-credit; không vào listing_distributions.
  • V-credit unlock: strict pool band, idempotent, lifetime.
  • Reset quota theo ngày (Asia/Ho_Chi_Minh).

Out of scope:

  • Crawl, normalize, dedup, tính trust_score, route TS ≥ 80 (listing-crawl-external-sources).
  • Pipeline listing do user app tự đăng (feature user-listing-create chưa có).
  • Mua/gia hạn gói; nạp V-credit (wallet-topup-create và liên quan).
  • Giá V-credit / unlock, cap V-credit/broker/ngày, cơ chế debit ví — chưa chốt trong spec.
  • Schema DB, API contract, concurrency lock, công thức ranking — repo BE.
  • UI dashboard broker; UI admin archive — feature FE/admin riêng.

User Stories

  • Broker paid: tối đa daily_listing_distribution_limit lần slot mở số / ngày; mỗi tin tối đa 1 slot / broker.
  • Broker trong list_broker nhưng không có slot (hạng 4+ cùng gói): listings + phone ẨN + V-credit.
  • Broker Free: dùng 3 unlock thủ công/ngày + V-credit; không tham gia phân phối.
  • Broker vừa nâng cấp Free → paid: được phân phối từ listing mới sau khi gói active (không retroactive).
  • Broker đã V-credit unlock: giữ phone_full vĩnh viễn, không bị reset.
  • Admin: listing TS ≥ 80 route Admin Archive, không phân phối đại trà.

Luồng chức năng

sequenceDiagram
    participant Source as Nguồn (App / Crawl)
    participant BE as Backend
    participant DM as Data Marketplace<br/>(TS < 80)
    participant AA as Admin Archive<br/>(TS ≥ 80)
    participant LD as listing_distributions
    participant WS as WebSocket
    participant BPaid as Broker còn slot
    participant BFull as Broker đầy slot
    participant Wallet as Ví V-credit

    Source->>BE: Listing mới
    BE->>BE: Validate + tính trust_score

    alt TS ≥ 80
        BE->>AA: Lưu Admin Archive
    else TS 50–79
        BE->>DM: Lưu Data Marketplace
        BE->>BE: Xác định tier theo TS<br/>(50–59 Premium, 60–69 Pro, 70–79 MAX)
        BE->>BE: Tạo list_broker<br/>đúng gói + phường + còn limit
        BE->>BE: fill_ratio trong list_broker
        BE->>BE: Chọn tối đa 3 broker
        loop Mỗi broker trong 3 slot
            BE->>LD: Insert (listing_id, user_id, date)
            BE->>WS: LISTING_DISTRIBUTED
            WS->>BPaid: Push + phone_full mở sẵn
        end
        BFull->>BE: Xem danh sách listings
        BE-->>BFull: Tin hiện, phone ẨN<br/>(không có slot: hạng ≥4 hoặc đạt limit)
    end

    BFull->>BE: Bấm V-credit unlock
    BE->>Wallet: Check + trừ V-credit
    alt Đủ V-credit
        BE-->>BFull: Hiển thị phone_full (lifetime)
    else Thiếu
        BE-->>BFull: Prompt nạp ví
    end

Phân vai nghiệp vụ:

Vai tròTrách nhiệm
Backendtrust_score; list_broker đúng band gói + phường + còn limit; chọn tối đa 3 broker theo fill_ratio; insert + event
listing_distributionsLưu mapping; enforce daily_listing_distribution_limit / broker / ngày
WebSocketPush LISTING_DISTRIBUTED tới broker nhận phân phối
Frontend (paid)Hiển thị danh sách phân phối với phone mở sẵn; phần còn lại phone ẨN + nút V-credit unlock
Frontend (Free)Pool công khai TS < 80, phone ẨN, 3 unlock thủ công + V-credit
Ví V-creditCheck + trừ V-credit khi unlock; ghi nhận lịch sử

Acceptance Criteria

  • AC-1: Listing chỉ chạy auto-phân bổ khi ACTIVE, có phone_full, có listing.ward, và trust_score thuộc band 50–79. Listing trust_score ≥ 80 route Admin Archive; listing ngoài band phân bổ không tạo list_broker.
  • AC-4d: list_broker chỉ gồm broker paid active, cùng phường, đúng band gói theo trust_score (Premium 50–59, Pro 60–69, MAX 70–79) và chưa đạt daily_listing_distribution_limit.
  • AC-9: Hệ thống chọn tối đa 3 broker trong list_broker theo fill_ratio thấp trước. Không trộn tier, không thử tier khác, không bù bằng broker ngoài phường / ngoài band / đã đạt limit.
  • AC-4: Broker được chọn có row listing_distributions và thấy phone_full mở sẵn. Broker không có row nhưng được phép xem marketplace thì thấy phone ẩn và có thể unlock theo rule.
  • AC-5: listing_distributions idempotent theo (listing_id, user_id, date), tối đa 3 row cho một (listing_id, date), và mỗi broker không vượt limit ngày: Premium 10, Pro 20, MAX 30.
  • AC-6: Free không tham gia auto-phân bổ; dùng 3 unlock thủ công/ngày và V-credit nếu đủ điều kiện.
  • AC-7: V-credit unlock strict theo pool band, idempotent, lifetime, không tính vào daily_listing_distribution_limit; thiếu V-credit thì từ chối và prompt nạp ví.
  • AC-8: Nâng cấp Free → paid hoặc đổi paid tier chỉ áp dụng với listing mới sau thời điểm gói active; không retroactive.
  • AC-10: Backend phát LISTING_DISTRIBUTED tới broker mỗi khi tạo row phân bổ thành công.
  • AC-17: fill_ratio = distributed_today / daily_listing_distribution_limit, tính trong chính list_broker của listing đang xử lý.

Quy tắc nghiệp vụ

Distribution Fill Rule

  • Trigger ngay khi listing đủ điều kiện: ACTIVE, có phone_full, có phường, trust_score thuộc 50–79.
  • Mỗi listing tạo tối đa 3 row listing_distributions; broker có row được mở số sẵn.
  • Mỗi broker bị giới hạn số listing tự mở số theo ngày: Premium 10, Pro 20, MAX 30.
  • Cung thiếu thì phân bổ ít hơn 3; không nới phường, không đổi band điểm, không dùng broker đã đạt limit để bù.
  • (listing_id, user_id, date) không được insert trùng.

Broker Selection Rule (tóm tắt thuật toán)

Đầu vào: listing validate + trust_score thuộc band 50–79 + phường + phone_full.

  1. Xác định tier — dựa trực tiếp vào band trust_score: Premium 50–59, Pro 60–69, MAX 70–79; không thử tier khác để bù slot (AC-4d).
  2. Tạo list_broker — broker đúng gói theo band, cùng phường, paid active và chưa đạt daily_listing_distribution_limit.
  3. fill_ratio trong list_broker.
  4. Rank + gán — tối đa 3 broker trong list_broker, fill_ratio thấp trước.
  5. Còn lại trong list_broker — listings, phone ẨN nếu broker được phép xem marketplace.
  6. Push LISTING_DISTRIBUTED.

Tie-break phải deterministic và không bias user_id thấp (chống starvation broker mới đăng ký).

Requirement chỉ định nghĩa eligibility + thứ tự ưu tiên. Công thức ranking cụ thể, concurrency lock khi insert — thuộc repo BE.

V-credit Unlock Rule

  • Áp dụng cho broker không có row phân bổ nhưng được phép xem listing trong marketplace.
  • Unlock strict theo pool band; Free dùng pool TS < 80.
  • Unlock bằng V-credit là kênh độc lập: không tính vào daily_listing_distribution_limit, idempotent, và giữ phone lifetime cho broker đó.
  • Thiếu V-credit thì từ chối unlock và prompt nạp ví.

Listing Eligibility

Listing được phân phối khi đủ dữ liệu bắt buộc và trust_score thuộc band 50–79. Listing TS ≥ 80, duplicate mạnh, expired, inactive hoặc thiếu phone_full / phường thì không vào auto-phân bổ.

Upgrade Rule

  • Gói mới chỉ áp dụng cho listing mới sau thời điểm gói active; không phân phối lại listing đã xử lý.
  • V-credit unlock cũ vẫn giữ phone_full lifetime sau khi đổi gói.

Dữ liệu & Trạng thái

Entity nghiệp vụ:

  • list_broker: output sau chấm trust_score; gồm broker đúng band gói, cùng phường, paid active, còn limit ngày.
  • listing_distributions: mapping (listing_id, user_id, date); unique, tối đa 3 broker / listing / ngày, và không vượt limit broker / ngày.
  • BrokerPackage: Free / Premium / Pro / MAX; quy định band điểm và limit ngày: 0 / 10 / 20 / 30.
  • BrokerOperatingWard: phường hoạt động của broker.
  • BrokerVcreditUnlock: ghi nhận unlock lifetime, độc lập với listing_distributions.

Trạng thái user-facing (per listing-broker):

StateÝ nghĩa
distributed_listing1 trong 3 slot cùng gói + cùng phường — có row → phone mở sẵn
visible_phone_hiddenKhông có row nhưng vẫn được phép xem marketplace → phone ẨN
vcredit_unlockedĐã V-credit unlock → phone vĩnh viễn
free_unlock_modeGói Free; 3 unlock thủ công + V-credit
admin_archivedListing TS ≥ 80

Event WebSocket:

EventKhi nào phátChannel
LISTING_DISTRIBUTEDInsert row listing_distributions cho 1 brokerChannel broker đó
LISTING_TRUST_SCORE_UPDATEDtrust_score listing đổiPublic (theo listing-crawl-external-sources)

Liên quan