Phân bổ listings cho broker theo gói
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 listing | Premium 50–59, Pro 60–69, MAX 70–79 |
| Broker cùng phường với listing | listing.ward khớp BrokerOperatingWard |
| Broker paid active | Chỉ Premium / Pro / MAX còn hiệu lực |
Broker chưa đạt daily_listing_distribution_limit | Cò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_broker | Hiển thị |
|---|---|
Top tối đa 3 broker theo fill_ratio thấp nhất | Có row listing_distributions, phone mở sẵn |
Broker còn lại trong list_broker | Listings 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 listing | Pool phân bổ tự động |
|---|---|
50–59 | Premium |
60–69 | Pro |
70–79 | MAX |
≥ 80 | Admin 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_ratiothấ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_distributions→phone_fullmở sẵn.
| Ứng viên trong tier đã chọn | Hành vi |
|---|---|
| ≥ 3 | Gán đủ 3 broker cùng gói |
| 1 hoặc 2 | Gán 1 hoặc 2 (không trộm tier khác để bù đủ 3) |
| 0 | Không gán slot tự động |
Bước 3 — Broker còn lại
| Nhóm | Hiển thị |
|---|---|
| Cùng tier nhưng hạng 4+ trong tier đã chọn | Listings có tin — số ẩn |
Gói khác với pool trust_score của tin | Không nhận slot tự động cho tin đó |
Đã đạt daily_listing_distribution_limit | Listings có tin (nếu eligible xem) — số ẩn |
| Free | Khô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):
| Broker | Gói | fill_ratio | Kết quả |
|---|---|---|---|
| P1 | Premium | 0,10 | Slot 1 — số mở |
| P2 | Premium | 0,30 | Slot 2 — số mở |
| P3 | Premium | 0,50 | Slot 3 — số mở |
| P4 | Premium | 0,70 | Listings — số ẩn |
| P5 | Premium | 0,90 | Listings — 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 Pro vì
60–69; không xét MAX để nhận slot. - Trong 6 Pro cùng phường, chọn 3 Pro có
fill_ratiothấ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ện | Số đ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_ratio | Mở sẵn |
| Chỉ xem listings | Broker 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ói | daily_listing_distribution_limit (tin tự mở số / ngày) |
|---|---|
| Premium | 10 |
| Pro | 20 |
| MAX | 30 |
| Free | 0 (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_limit → khô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):
| Cap | Field / giá trị |
|---|---|
| 3 broker / tin | max_brokers_per_listing = 3 |
| Tin tự mở số / broker / ngày | daily_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ệu | Cô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 đầy | fill_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ần | fill_ratio | Thứ tự ưu tiên slot |
|---|---|---|---|---|
| A | 1 | 10 | 0,10 | #1 |
| B | 5 | 10 | 0,50 | #2 |
| C | 9 | 10 | 0,90 | #3 |
| D | 10 | 10 | 1,00 | Loạ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:
-
Đú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). -
Đú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_scorequyết định tin thuộc đúng một pool gói, không chồng lấn.
| Gói môi giới | Chỉ gán tin có trust_score |
|---|---|
| Premium | Từ 50 đến 59 |
| Pro | Từ 60 đến 69 |
| MAX | Từ 70 đến 79 |
| Gói | Hành vi |
|---|---|
| Free | Khô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ểm | Khô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 12 → khô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_limittin/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)— timezoneAsia/Ho_Chi_Minh. list_brokersau khi tin chuẩn + cótrust_score; chỉ gồm broker đúng band gói theo điểm (Premium50–59, Pro60–69, MAX70–79), cùng phường, paid active và chưa đạtdaily_listing_distribution_limit; rankfill_ratiotrong 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_scorechọn một tier không chồng lấn / tin; tối đa 3 broker tronglist_brokerđược mở số theofill_ratio.- Broker có row phân bổ →
phone_fullmở 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-createchư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_limitlần slot mở số / ngày; mỗi tin tối đa 1 slot / broker. - Broker trong
list_brokernhư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_fullvĩnh viễn, không bị reset. - Admin: listing TS
≥ 80route 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í
endPhân vai nghiệp vụ:
| Vai trò | Trách nhiệm |
|---|---|
| Backend | trust_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_distributions | Lưu mapping; enforce daily_listing_distribution_limit / broker / ngày |
| WebSocket | Push 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-credit | Check + 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_scorethuộc band50–79. Listingtrust_score ≥ 80route Admin Archive; listing ngoài band phân bổ không tạolist_broker. - AC-4d:
list_brokerchỉ gồm broker paid active, cùng phường, đúng band gói theotrust_score(Premium50–59, Pro60–69, MAX70–79) và chưa đạtdaily_listing_distribution_limit. - AC-9: Hệ thống chọn tối đa 3 broker trong
list_brokertheofill_ratiothấ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_distributionsvà thấyphone_fullmở 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_distributionsidempotent 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_DISTRIBUTEDtớ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ínhlist_brokercủ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_scorethuộc50–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.
- Xác định tier — dựa trực tiếp vào band
trust_score: Premium50–59, Pro60–69, MAX70–79; không thử tier khác để bù slot (AC-4d). - Tạo
list_broker— broker đúng gói theo band, cùng phường, paid active và chưa đạtdaily_listing_distribution_limit. fill_ratiotronglist_broker.- Rank + gán — tối đa 3 broker trong
list_broker,fill_ratiothấp trước. - Còn lại trong
list_broker— listings, phone ẨN nếu broker được phép xem marketplace. - 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_fulllifetime sau khi đổi gói.
Dữ liệu & Trạng thái
Entity nghiệp vụ:
list_broker: output sau chấmtrust_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ớilisting_distributions.
Trạng thái user-facing (per listing-broker):
| State | Ý nghĩa |
|---|---|
distributed_listing | 1 trong 3 slot cùng gói + cùng phường — có row → phone mở sẵn |
visible_phone_hidden | Khô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_mode | Gói Free; 3 unlock thủ công + V-credit |
admin_archived | Listing TS ≥ 80 |
Event WebSocket:
| Event | Khi nào phát | Channel |
|---|---|---|
LISTING_DISTRIBUTED | Insert row listing_distributions cho 1 broker | Channel broker đó |
LISTING_TRUST_SCORE_UPDATED | trust_score listing đổi | Public (theo listing-crawl-external-sources) |
Liên quan
- Phụ thuộc:
- listing-crawl-external-sources — nguồn listings +
trust_score; trigger luồng phân phối. - wallet-topup-create — nạp V-credit để dùng V-credit unlock.
- listing-crawl-external-sources — nguồn listings +
- Tham chiếu: auth-register-otp, wallet-topup-cancel, wallet-topup-sepay-qr-webhook, payment-order-detail-sync.
- Ảnh hưởng: Chưa có (sẽ kết nối feature dashboard broker, package purchase, admin archive UX,
user-listing-create,wallet-debitkhi được tạo).