chicgo, chicstay, checkevent ngày 18/08Tuần này ChicStay đi từ một công cụ hỏi phòng trống thành một dịch vụ đồng bộ chạy nền 24/7 và một API để hệ thống ChicStay gọi sang bán hàng. Đó là hai loại phần mềm khác hẳn nhau: cái đầu sai thì người dùng bấm lại, cái sau sai thì bán trùng một phòng đã có khách.
/v1/rooms giao cho ChicStay, kèm tài liệu và kết quả kiểm thử thật. 181 phòng đã nối nguồn, quét thật từng phòng một cả 423 phòng: 0 phòng tự mâu thuẫn. 361 test tự động, chạy lại ngày 18/08 đều xanh.Tồn đọng sang tuần thứ ba — chicgo_data vẫn đứng. Enrich chết 18 ngày (lần chạy cuối 31/07 11:30), hàng chờ 37.232 địa điểm. Crawl dừng 9 ngày (địa điểm mới cuối cùng 09/08 22:22). Cả hai đã nêu ở hai báo cáo trước và chưa được xử lý. Chi tiết mục 1.
Không có commit chicgo_data trong tuần. Số liệu truy vấn ngày 18/08 xác nhận cả hai luồng vẫn nằm im đúng như tuần trước:
| Chỉ số | Tuần trước (11/08) | Hiện tại (18/08) | Thay đổi |
|---|---|---|---|
| Lượt enrich cuối cùng | 31/07 11:30 | 31/07 11:30 | +7 ngày chết → 18 ngày |
need_to_enrich | 37.033 | 37.232 | +199 |
enriched | 23.891 | 23.891 | 0 |
| Địa điểm mới cuối cùng | 09/08 22:22 | 09/08 22:22 | +7 ngày dừng → 9 ngày |
Hàng đợi từ khoá pending | 320 | 320 | 0 |
| BD confirm (tổng) | — | 19.929 | ~18 lượt cả tuần |
Đọc con số này thế nào. Hàng chờ enrich chỉ tăng 199 trong cả tuần, trong khi tuần trước tăng 4.866. Đó không phải tin tốt — nó là bằng chứng thứ hai cho việc crawl cũng đã dừng: không có địa điểm mới thì cũng không có gì mới để xếp vào hàng chờ. Hai luồng đứng cùng nhau nên một luồng đứng đang che số liệu của luồng kia.
BD confirm cả tuần chỉ 18 lượt (so với ~400/ngày hồi đầu tháng 8). Đội BD không có việc để làm vì phía trước không đẩy dữ liệu mới xuống. Đây là nút thắt lớn nhất đang mở của cả hệ thống chicdata, và tuần này không ai chạm vào nó vì toàn bộ thời gian đổ vào ChicStay.
Bản cũ (báo cáo tuần trước) là Cloudflare Worker không lưu gì: mỗi request đi thẳng sang PMS của chủ home rồi quay về. Đúng cho một công cụ tra cứu, nhưng sai hoàn toàn cho một OTA.
| Yêu cầu của OTA | Bản Worker cũ | Bản mới |
|---|---|---|
| Khách mở trang xem phòng | Mỗi lượt xem = 1 lần đăng nhập vào PMS chủ home | Đọc DB, không chạm PMS |
| PMS của chủ home đang sập | Không trả lời được | Vẫn trả lời, kèm freshness nói dữ liệu cũ bao nhiêu giây |
| Tốc độ | 0,25 – 5 giây, phụ thuộc PMS | Dưới 0,3 giây (1 phòng) · 0,42–0,54 giây (181 phòng) |
| Lịch sử thay đổi phòng | Không có | Ghi lại từng thay đổi, đối soát được với chủ home |
Chuyển cả codebase từ TypeScript/Worker sang Python/FastAPI trên Railway để chạy được tiến trình sống — cron của Railway bật container mới mỗi lượt, mà riêng phần đăng nhập lại vào từng PMS đã tốn hơn phần lấy dữ liệu. Chạy trong tiến trình sống thì token KiotViet (28 ngày) và phiên Gohost giữ nguyên giữa các lượt.
SELECT. Ghi đè toàn bộ mỗi lượt sẽ là ~400 nghìn UPDATE mỗi ngày cho dữ liệu y hệt nhau.Không đẻ bảng mới. Khảo sát DB trước khi viết: chicstay đã có sẵn source_mapping (15.042 dòng) và inventory_blocks — chỉ là chưa ai đổ dữ liệu vào. Việc ghép phòng PMS ↔ phòng ChicStay dùng lại đúng cơ chế đó, migration chỉ cộng thêm cột, không sửa không xoá.
Cả bảy lỗi dưới đây có chung một hình dạng: hệ thống không thấy dữ liệu, rồi kết luận là không có khách. Không lỗi nào tự bật lên — tất cả đều lộ ra khi chạy thật trên production và đi truy tận gốc.
Lượt đầu chạy production lộ ra ngay: Apps Script của Fish VCC timeout ở tháng 9, adapter chỉ trả dữ liệu tháng 8. Đồng bộ không thấy booking tháng 9 nên đóng sạch 20 khoảng bận tháng 9 — rồi mở lại đúng 20 khoảng đó ở lượt sau khi Apps Script trả lời được.
run#64 12:16 fish_vcc đóng=20 ← tháng 9 timeout run#71 12:17 fish_vcc thêm=20 ← tháng 9 trả lời được
Trong một phút đó dashboard báo 3 phòng đang kín thành phòng trống — đúng thứ nguy hiểm nhất hệ thống này có thể nói sai, vì sales sẽ nhận khách vào phòng có người.
Gốc rễ: suy từ "không thấy" ra "không có". Đóng một khoảng bận là một kết luận phủ định, mà kết luận phủ định chỉ có căn cứ khi đã nhìn được trọn khoảng thời gian chứa nó. Sửa: lượt nào lấy thiếu thì bỏ toàn bộ phần đóng dòng, giữ nguyên phần thêm/sửa — cái gì PMS có trả về là bằng chứng dương, luôn dùng được.
Cùng gốc bệnh nhưng vá ở 3.1 không cứu được. Chạy thật:
12:39 abri + homnaykhongngu FAILED Dayladau GET /v1/listings → HTTP 502 12:40 abri + homnaykhongngu OK thêm=0 đóng=17 và 27
Một phút sau lỗi 502, Dayladau trả lời lại được: HTTP 200, danh sách phòng đầy đủ (5/5 và 8/8 phòng ghép được), nhưng danh sách booking rỗng. Response hợp lệ về mọi mặt — không có gì để adapter báo lỗi. Và đồng bộ đóng sạch 44 khoảng bận của hai home.
Sửa: thêm lớp chặn cuối không phụ thuộc adapter, chỉ nhìn hình dạng của thay đổi — không thêm gì mà lại đóng quá nửa số khoảng đang có thì từ chối. Đánh đổi được cân nhắc có chủ ý: chặn nhầm thì mất một cơ hội bán; không chặn thì sales nhận khách vào phòng có người. Nghiêng về giữ khoảng bận.
Soak test 1.798 lượt trong 5 giờ lộ ra lỗi chưa test nào chạm tới: khách huỷ rồi đặt lại đúng khung giờ cũ, PMS cấp mã đơn mới → dòng mới trùng khoá tự nhiên với dòng cũ. Vì mọi thao tác của một lượt nằm trong một giao dịch, cả lượt mất trắng — kể cả những thay đổi chẳng liên quan gì tới phòng đó.
Phải sửa cả ba chỗ, thiếu chỗ nào cũng còn đường vỡ: (1) đổi thứ tự ghi — đóng trước, chèn sau; (2) index chỉ ràng buộc trên dòng đang mở (index cũ phủ cả dòng đã đóng, nên một lượt đặt từ tháng trước đã huỷ vẫn chiếm chỗ vĩnh viễn); (3) hai mã đơn trùng khít khung giờ trong cùng một phòng thì bỏ bớt một — về mặt vật lý không thể có hai khách cùng ở một phòng cùng khoảng đó.
| Lỗi | Triệu chứng | Cách sửa |
|---|---|---|
| Lượt chạy kẹt RUNNING | Deploy giết tiến trình giữa chừng, dòng nằm lại RUNNING vĩnh viễn. Trên dashboard trông y hệt "lượt đồng bộ đang treo" dù chuyện xảy ra nửa tiếng trước |
Dọn ở mỗi vòng, không chỉ lúc khởi động — hệ thống tự khỏi trong ~6 phút sau bất kỳ lần chết nào |
| Cảnh báo giả làm mất nghĩa cảnh báo thật | 3/6 tài khoản ra PARTIAL trong khi không có gì sai — KiotViet lượt nào cũng kèm range_chunked vì nó chỉ trả từng tuần một |
Chỉ 2 mã làm lượt thành PARTIAL. Ba tài khoản vĩnh viễn màu cam thì người trực học được đúng một điều: màu cam vô nghĩa |
| Ghi đè mỗi phút | Đo trên production: 285 khoảng bị ghi lại và 32 phòng báo "đổi" MỖI PHÚT cho dữ liệu không hề thay đổi — kéo theo một thẻ Lark mỗi phút cho mỗi nhà | Cắt khoảng bận theo đúng cửa sổ đồng bộ. Khoảng nằm trước cửa sổ không bao giờ khớp được nên bị coi là mới mãi mãi |
| Trang trạng thái: hỏng trông như rỗng | Trang hiện "Chưa có phòng nào khớp bộ lọc" — thực ra endpoint trả 400 vì bộ lọc trống gửi chuỗi rỗng. Kèm 3 lỗi nữa: server Railway chạy UTC nên thanh thời gian lệch 17 tiếng so với 00:00 giờ VN | Phân biệt rõ "đang tải" / "gọi hỏng" / "thật sự rỗng". Test nay chạy dưới 4 múi giờ — code cũ xanh ở VN và đỏ 7/14 ở UTC |
Vì sao test cũ không thấy: endpoint được test bằng input tự nghĩ ra (sạch), còn trang được test với endpoint bị stub thay thế. Không test nào đi qua ranh giới giữa hai bên — mà lỗi nằm đúng ở đó. Nay hàm dựng query nằm ở shared/ và test gọi đúng hàm trang dùng, không chép lại.
Ba dòng "mở tạm" thêm vào để chạy kiểm thử headless đã bị hai commit tự động gom theo và đẩy lên origin/stagging. Dòng thứ ba là dòng nguy hiểm — nó nằm ở server middleware, tức lớp chặn thật chứ không phải lớp UX. Với ba dòng đó, trang /chicstay/room-state và mọi endpoint /api/chicstay/room-state* mở cho bất kỳ ai không cần đăng nhập — phơi ra lịch phòng, tên cơ sở, tên brand và mã đơn của merchant.
Đã gỡ ngay khi phát hiện. Bài học về quy trình: sửa file bảo mật để chạy kiểm thử thì không được để nó nằm trong thư mục làm việc lâu hơn một lệnh — lần sau dùng biến môi trường chỉ đọc lúc chạy test, để không có cửa nào cho một lần commit tự động vô tình cuốn theo.
Nhóm nhà không có PMS trước đây bị xếp "ngoài phạm vi". Tuần này khảo sát lại: 10 file đọc được thật, 6 bố cục khác nhau. Dựng adapter đọc qua Sheets API.
Phát hiện buộc phải đổi thiết kế: SUN ghi lịch bận bằng màu nền ô — 2.860 trong 2.943 ô bận là ô rỗng chữ. Đọc bằng CSV sẽ ra "cả tháng trống trơn": chạy trơn tru và sai 97%. Phải đọc qua includeGridData để lấy được màu nền.
24- bị hiểu là bận trọn ngày hôm sau, trong khi nó nghĩa là khách ở qua nửa đêm và hôm sau -12h mới là giờ trả. Đọc theo cặp mở/đóng thay vì từng ô.8/2026 vs 82026) — gọi sai thì gviz lặng lẽ trả tab đầu tiên chứ không báo lỗi.PBCt2 (4859), tháng 9 cùng phòng thành PBCt2 (3658). Ghép theo cả nhãn thì cứ sang tháng mới là đứt sạch — nay ghép theo phần tên ổn định, kèm cơ sở vì Trích Sài và Phạm Hồng Thái đều có phòng "401".Kết quả: 5/7 sheet đọc được, thêm 32 phòng vào hệ thống (Mer 6, SUN 7, AimeeHouse 19). LẶNG và TL để layout_unsupported thay vì đoán chữ viết tay — đoán sai một cột là bán nhầm phòng. Ba file vẫn tắc: MƠ và Cheese's trả 403, CHILL đã bị xoá.
Đồng bộ chạy mỗi phút nên phần thông báo toàn bộ là bài toán chống nhiễu. Nguyên tắc: một cảnh báo mà người trực học được cách bỏ qua thì tệ hơn không có cảnh báo.
MODIFIED liệt kê đúng trường đã đổi kèm giá trị cũ → mới (giờ nhận, giờ trả, tổng thời lượng ± bao nhiêu giờ). "Đổi giờ" trần trụi buộc người trực mở PMS lên so tay mới biết.#gid, không chỉ đúng file — sheet của AimeeHouse có 102 tab tên gần giống nhau, mở nhầm tab thì thấy một bảng vẫn trông bình thường.Chi tiết dễ bỏ sót: tin nhắn gửi sau khi ghi DB xong. Đặt trước thì một group chat chậm sẽ giữ chân cả lượt đồng bộ. Lỗi gửi tin bị nuốt có chủ ý — dữ liệu phòng quan trọng hơn cái tin báo về nó.
/v1/roomsĐây là sản phẩm bàn giao của tuần: một API để backend ChicStay gọi sang khi bán phòng, kèm Swagger có nút Authorize và tài liệu docs/api-chicstay.md tự chứa — dán lệnh curl trong đó là chạy được ngay, không phải mở thêm chỗ nào khác.
status có ba giá trị, không phải hai| Giá trị | Nghĩa | Được phép bán? |
|---|---|---|
busy | Có khách tại đầu cửa sổ | Không |
free | Đã nối nguồn, và nguồn nói không có ai | Có |
unknown | Phòng chưa nối với nguồn nào | Không — ta không biết gì về phòng này |
242/423 phòng đang là unknown. Coi unknown là trống là cách chắc chắn nhất để bán trùng một phòng đã có khách — nên nó được tách thành giá trị riêng ngay ở tầng API chứ không để bên gọi tự suy.
busy_until đọc từ mảng thô thay vì mảng đã gộp. Phòng có hai đơn chồng nhau kết thúc khác giờ trả mốc hết bận sớm hơn thực tế 2 giờ — bên gọi mở bán từ đúng lúc đó trong khi phòng còn khách.state=busy/free neo theo now() thay vì đầu cửa sổ. "Cho tôi phòng trống ngày 15/09" đang trả về 5 phòng đã có khách ngày 15/09.Vì sao bộ quét không bắt được: nó chỉ kiểm busy_until có tồn tại chứ không kiểm giá trị, và không bao giờ chạy với min_hours > 0. Nay kiểm cả hai. Đây là lý do bộ quét mới đi từng phòng một chứ không lấy mẫu.
| Phép đo | Kết quả |
|---|---|
| Quét từng phòng một, cả 423 phòng, 10 bất biến mỗi phòng | 0 phòng tự mâu thuẫn |
| Thời gian trả lời (181 phòng, 5 lần liên tiếp) | 0,42 – 0,54 giây · 128 KB |
| Quét từng phòng: trung vị / chậm nhất | 202 ms / 547 ms · tổng 90 giây |
Phòng bị đánh dấu stale | 0/181 |
| 4 trang × 50 ghép lại = 1 lần lấy 181 phòng | Trùng khít, không trùng lặp, không sót |
busy + free = đúng độ dài cửa sổ | Đúng trên cả 423 phòng |
| Đối chiếu ngược với bảng gốc của chủ home | Khớp (soi tay SUN HOMESTAY) |
| Bộ test tự động (chạy lại 18/08) | 361 test xanh, 7,5 giây |
Một phát hiện từ chính bộ quét: 5 phòng của Amoureux mang hai đơn KiotViet trùng khít cùng quãng — một ghi overnight, một ghi hourly. Không gộp thì tổng giờ bận dài hơn cả cửa sổ (đo thật: 264 giờ trong cửa sổ 168 giờ), và bên gọi cộng giờ bận để tính công suất phòng sẽ ra số vô lý. Nay API gộp trước khi trả.
| Nhà | Phòng | Nguồn | Loại |
|---|---|---|---|
| Amoureux Homestay | 71 | pms:kiotviet | PMS thật |
| Bum.Staycation | 41 | pms:gohost | PMS thật |
| Fish Homestay | 20 | pms:gas | Apps Script tự dựng |
| AimeeHouse | 19 | pms:gsheet | Người điền tay |
| ABRI HOUSE | 8 | pms:dayladau | PMS thật |
| SUN HOMESTAY | 7 | pms:gsheet | Người điền tay |
| Mer Homestay | 6 | pms:gsheet | Người điền tay |
| Homnaykhongngu | 5 | pms:dayladau | PMS thật |
| Fish Homestay (VCC & KG) | 4 | pms:fish_pms | PMS thật |
Cột "loại" là thông tin ChicStay cần biết. Nhà dùng PMS thật thì khoảng bận sinh ra cùng lúc với đơn đặt. Nhà dùng Google Sheet thì "trống" có thể chỉ nghĩa là chưa ai kịp ghi vào đó. API trả source_ref để bên ChicStay đặt mức tin cậy khác nhau cho hai nhóm này.
Ghép ID phòng bên PMS với room_id của ChicStay là chỗ sai thì bán nhầm phòng của nhà khác, nên được đối chiếu 147 cặp bằng hai cách độc lập: đọc tay toàn bộ, và kiểm bằng máy trên ba điều kiện. Hai cách cho kết quả khớp nhau — 142 cặp ghép theo số phòng, 5 theo tên riêng (Homnaykhongngu đặt tên phòng không đánh số: Dark/Mario/Sarang/Meem/Space), 0 cặp đáng ngờ.
Một ca thật đã xảy ra: Amoureux đổi gian hàng KiotViet giữa tuần, toàn bộ ID cũ chết. Phải ghép lại theo (cơ sở + số phòng) thay vì theo ID — và giữ bản sao mapping cũ trước khi ghi đè.
Trên dashboard thêm tab thứ tư "Nguồn & ghép ID" vào chính trang trạng thái phòng, không dựng trang mới. Tab này đọc hoàn toàn từ DB, không gọi sang PMS: mở một trang dashboard không được tạo tải lên hệ thống của chủ home, và trang vẫn phải xem được lúc PMS của họ đang sập. Thêm link mở thẳng lịch bên PMS từ dòng phòng — nguồn lạ trả null thay vì đoán, vì link mở ra lịch nhà khác còn tệ hơn không có link.
Soạn phương án tích hợp lịch trống cho backend ChicStay, dựng trên schema thật. Điều quan trọng nhất tài liệu nêu ra không phải API mà là một mắt xích chưa tồn tại:
ChicStay bán một phòng thì PMS của chủ home không hề biết — nên chủ home sẽ bán lại chính phòng đó. Chiều ghi ngược quyết định ChicStay được phép bán kiểu gì, nên phải chốt trước khi viết dòng code bán hàng nào.
| Phát hiện | Con số | Hệ quả nếu bỏ qua |
|---|---|---|
| Cột khai tay và độ phủ thật đã tách rời hoàn toàn | 54 phòng khai INSTANT mà không có lịch tự động nào phía sau; 134 phòng có lịch mà chưa khai |
Khách đặt tức thì vào phòng không ai biết còn trống hay không |
| Không phòng nào khai đệm dọn phòng | 0/507 dòng availability_config |
Với 402 phòng bán theo giờ, bán 12:00–14:00 ngay sau khi khách trả phòng lúc 12:00 sẽ là chuyện hằng ngày |
| Chiều ghi ngược gần như không có | Chỉ Apps Script (19 phòng) chắc chắn ghi được. Gohost có API đối tác nhưng quyền tài liệu hoá chỉ có đọc. Dayladau không có đường ghi nào | Bán xong không báo được cho chủ home → hai bên cùng bán một phòng |
Khuyến nghị: ra mắt bằng mô hình phân bổ — chủ home dành sẵn một phần phòng cho ChicStay — thay vì chờ dựng được chiều ghi ngược. Đây là cách các OTA dùng khi chưa nối được hai chiều. Tài liệu kèm hợp đồng INPUT/OUTPUT, 6 quyết định có khuyến nghị sẵn, lộ trình 4 giai đoạn và bảng rủi ro xếp theo mức thiệt hại.
Ba lỗi chặn việc sync ảnh, tìm ra khi sync GBOX và ALÀ:
?id= trỏ tới một ảnh bị bỏ trắng. Dạng open?id=<id> dùng chung cho cả folder lẫn file, Drive không nói ra ở link. Tool luôn đoán folder, nhận 404 rồi bỏ qua — toàn bộ 39 album của GBOX UPROAD dùng dạng này, 29 phòng trắng ảnh trong khi từng link đều là một JPEG tải được bình thường.renumber() xoá ảnh bìa do BD tự chọn. Hàm ép is_primary = (thứ tự == 1), nhưng dashboard cho BD chọn ảnh bìa riêng. 100/303 phòng đang chọn tấm khác tấm đầu — chạy sync một lần là mất sạch 100 lựa chọn đó. Tình cờ, một lỗi trùng khoá khác đã che chở 100 lựa chọn này suốt thời gian qua.is_primary=false: một nơi quyết định ảnh bìa, không phải hai.Lần quét trước dừng ở "quét 81 cơ sở, 0 ảnh mới" rồi kết luận. Chưa đủ: đó mới chứng minh công cụ không lấy thêm được, chưa chứng minh không còn ảnh. Soi tiếp bằng Drive API qua ba đường mà công cụ không đi — mọi album của từng phòng (mỗi phòng có cả RAW lẫn CURATED), thư mục cha của link file lẻ, và đích của link phòng khi là lối tắt. Kết quả: 0 ảnh.
Đồng thời truy được một nghi vấn: Drive API báo "thiếu 172 ảnh" ở 51 phòng — truy tiếp thì đó là ảnh trùng nội dung, mỗi phòng có hai thư mục RAW và CURATED chứa cùng bộ ảnh. Toàn hệ thống 5.057 file Drive → 4.698 ảnh thật, dư 359 file trùng. Tool khử trùng theo vân tay nội dung nên lưu đúng, không phải sót.
43 phòng còn trắng ảnh, và chúng không cùng một lý do — gộp chung thì BD không biết phải làm gì:
| Nhóm | Số phòng | BD cần làm gì |
|---|---|---|
| Chưa share | 26 | Xin chủ home mở quyền — đã kiểm lại sau một ngày, vẫn khoá cả 26 |
| Chỉ có video | 16 | Thiếu ảnh ở nguồn, không phải lỗi kỹ thuật. Cấp quyền Drive cũng không cứu được nhóm này (12 phòng NOBLE APARTMENT) |
| Không có file ảnh nào | 1 | Chủ home chưa upload |
Kèm reports/nhan-chu-home-anh-phong.md — gom theo chủ home, mỗi nhóm kèm câu hướng dẫn cụ thể để BD copy gửi thẳng được.
Không có thay đổi code. Số liệu DB ngày 18/08 cho thấy hệ thống vẫn chạy đều:
| Chỉ số | Tuần trước | Hiện tại |
|---|---|---|
| Event | 1.988 | 2.012 |
| Đã đẩy thành công | 1.956 | 1.980 |
| Bị từ chối / đang chờ | 30 / 2 | 30 / 2 |
| Tổ chức | 788 | 796 |
| Lượt crawl (7 ngày) | 192 | 166 |
vẫn bị chặn Luồng học bổng đứng im sang tuần thứ ba. 375 bản ghi vẫn ở needs_review, mốc kiểm cuối cùng là 16/06. Nguyên nhân không đổi: App key production thiếu quyền scholarships:read/write/publish, cần admin CheckEvent cấp. Đây là việc chờ người khác, không tự gỡ được.
INSTANT mà không có lịch tự động phía sau — phải hoặc nối nguồn, hoặc hạ mức bán xuống, trước khi mở bán tức thì.unknown. Ba nhóm lớn nhất chờ tài khoản: Rosy Hotel 93 phòng (go2joy), Alà Homestay 90 (ezcloud), Sazi Home 26 (sazi). Đây là việc xin tài khoản, không phải việc code.nhan-chu-home-anh-phong.md đã soạn sẵn câu nhắn, BD gửi được ngay.scholarships:read/write/publish.