Báo cáo công việc tuần

Chic Data — Tổng kết tuần

Giai đoạn: 11/08 – 18/08/2026 · Tổng hợp từ 70 commit của 2 repo, tài liệu sinh ra trong tuần, và số liệu vận hành truy vấn trực tiếp từ DB chicgo, chicstay, checkevent ngày 18/08
Người thực hiện: jc-hello
Ngày lập: 18/08/2026
Phạm vi: 2 dự án có commit + 2 dự án theo dõi vận hành

Tổng quan tuần

Tuầ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.

181
phòng nối nguồn tự động 6 hệ thống
60s
chu kỳ đồng bộ · 9 tài khoản
361
test tự động, xanh 18/08
70
commit · 2 repo
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.

1. Vận hành chicgo_data — không có tiến triển tồn 3 tuần

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ùng31/07 11:3031/07 11:30+7 ngày chết → 18 ngày
need_to_enrich37.03337.232+199
enriched23.89123.8910
Địa điểm mới cuối cùng09/08 22:2209/08 22:22+7 ngày dừng → 9 ngày
Hàng đợi từ khoá pending3203200
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.

2. ChicStay Room State — từ "hỏi hộ" thành "dịch vụ đồng bộ"

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.

Vì sao phải viết lại

Yêu cầu của OTABản Worker cũBản mới
Khách mở trang xem phòngMỗ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ậpKhông trả lời đượcVẫ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 PMSDưới 0,3 giây (1 phòng) · 0,42–0,54 giây (181 phòng)
Lịch sử thay đổi phòngKhô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.

2.1 Ba quyết định thiết kế xoay quanh việc chạy mỗi phút

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á.

3. Bảy lỗi "chạy trơn tru mà kết quả sai" phần đáng giá nhất

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.

3.1 Lấy thiếu dữ liệu bị hiểu thành "khách đã huỷ" nghiêm trọng nhất

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.

3.2 PMS trả về rỗng — dạng adapter không thể phát hiện

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.

3.3 Khách đặt lại đúng khung giờ cũ làm vỡ trọn một lượt đồng bộ

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 đó.

3.4 Bốn lỗi còn lại

LỗiTriệu chứngCá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.

Cách kiểm chứng: đột biến ngược. Mỗi lớp chặn thêm vào đều được kiểm bằng cách cố ý phá lại rồi xem test có đỏ không — 9 đột biến ở tầng đồng bộ (gỡ từng lớp chặn, bỏ neo đầu ngày, cho dry-run ghi thật, bỏ bộ lọc merchant…), 9 lần đỏ đúng test tương ứng. Test xanh mà không kiểm ngược thì không chứng minh được gì.

3.5 Một sự cố quy trình — không phải lỗi code bảo mật

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.

4. Nguồn thứ sáu — Google Sheet của chủ home

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.

Ba lỗi sửa trong lúc dựng — cả ba đều cho kết quả trông bình thường

Hai bẫy đã vấp, ghi lại để không vấp lại

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á.

5. Cảnh báo Lark — thiết kế để người trực còn đọc

Đồ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.

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ó.

6. API giao cho ChicStay — /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.

Quyết định quan trọng nhất: status có ba giá trị, không phải hai

Giá trịNghĩaĐược phép bán?
busyCó khách tại đầu cửa sổKhông
freeĐã nối nguồn, và nguồn nói không có aiCó
unknownPhòng chưa nối với nguồn nàoKhô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.

Hai lỗi bán trùng phòng tìm ra ở giai đoạn cuối

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.

6.1 Kiểm thử — chạy thật trên production, không mock

Phép đoKết quả
Quét từng phòng một, cả 423 phòng, 10 bất biến mỗi phòng0 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ất202 ms / 547 ms · tổng 90 giây
Phòng bị đánh dấu stale0/181
4 trang × 50 ghép lại = 1 lần lấy 181 phòngTrù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ủ homeKhớ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ả.

6.2 Phân bố 181 phòng đã nối nguồn

NhàPhòngNguồnLoại
Amoureux Homestay71pms:kiotvietPMS thật
Bum.Staycation41pms:gohostPMS thật
Fish Homestay20pms:gasApps Script tự dựng
AimeeHouse19pms:gsheetNgười điền tay
ABRI HOUSE8pms:dayladauPMS thật
SUN HOMESTAY7pms:gsheetNgười điền tay
Mer Homestay6pms:gsheetNgười điền tay
Homnaykhongngu5pms:dayladauPMS thật
Fish Homestay (VCC & KG)4pms:fish_pmsPMS 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.

7. Ghép phòng & tab theo dõi trên dashboard

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.

8. Tài liệu tích hợp OTA — ba phát hiện chặn việc bán cần quyết định

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.

Ba việc phải xử lý trước khi bán

Phát hiệnCon 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.

9. chicstay-data — ảnh phòng: chốt lại "đã cạn kiệt"

Ba lỗi chặn việc sync ảnh, tìm ra khi sync GBOX và ALÀ:

Chứng minh cạn kiệt — không dừng ở "công cụ không lấy thêm được"

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.

3.940
ảnh phòng trong kho +293
369
/423 phòng có ảnh 89% phòng có album
369
ảnh bìa · đúng 1 mỗi phòng
100
lựa chọn bìa của BD giữ nguyên

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ómSố phòngBD cần làm gì
Chưa share26Xin chủ home mở quyền — đã kiểm lại sau một ngày, vẫn khoá cả 26
Chỉ có video16Thiế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ào1Chủ 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.

10. checkevent_data — không có commit, vận hành ổn

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ướcHiện tại
Event1.9882.012
Đã đẩy thành công1.9561.980
Bị từ chối / đang chờ30 / 230 / 2
Tổ chức788796
Lượt crawl (7 ngày)192166

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.

Việc tiếp theo