Công nghệ - 22/04/2025 10:08:22
Xin chào các bạn, hôm nay, tôi muốn chia sẻ một case study thực tế về bài toán Trừ tiền trong gói trả trước, mà tôi nhận được từ bạn Trung, một lập trình viên trẻ đầy nhiệt huyết. Qua câu chuyện này, tôi không chỉ trình bày hướng giải bài toán mà còn nhấn mạnh tầm quan trọng của tư duy Test-Driven Development (TDD) và việc chuẩn bị Test trong việc đảm bảo chất lượng phần mềm.
Hãy cùng khám phá nhé!
Một buổi chiều, trong lúc rảnh lông đi làm ấm trà cho tỉnh táo, Trung tiện nhắn tin trong room với giọng đầy lo lắng: “Anh em ơi, em đang bí một bài toán trừ tiền gói trả trước cho hệ thống gửi tin, đặc biệt là khi chạy đa luồng và số dư ví không đủ. Em chưa biết cách xử lý sao cho chuẩn. Em đang dùng ngôn ngữ bla blô gì đó....”. ...
Gói trả trước thì khác gì mua thẻ trả trước điện thoại đâu nhỉ. Tuy nhiên, khi Trung mô tả kỹ hơn bài toán, hệ thống cần trừ tiền từ ví thanh toán của đại lý, xử lý các loại tin với giá khác nhau, và nếu số dư không đủ, thì không cho gửi tin nhắn nữa.
Nghe xong, tôi nhận ra đây là một bài toán điển hình nhưng đầy thử thách, đòi hỏi sự cân bằng giữa độ chính xác, hiệu suất, và khả năng debug. Tôi hiểu nếu Trung tự mày mò, cậu ấy có thể sẽ mất nhiều thời gian để tìm ra giải pháp tối ưu, đặc biệt là khi phải đảm bảo an toàn trong môi trường đa luồng. May mắn thay, tôi từng giải quyết một bài toán tương tự trong một dự án gửi tin nhắn quảng cáo, nên tôi gọi ngay cho Trung để thảo luận.
Sau khi trao đổi, chúng tôi chốt được một số vấn đề bài toán như sau:
Tôi nhận xét, bài toán này không chỉ kiểm tra kỹ năng lập trình mà còn đòi hỏi tư duy thiết kế hệ thống và khả năng dự đoán lỗi. Dự đoán lỗi + test là “tấm khiên” không thể thiếu để bảo vệ chất lượng đầu ra.
Đầu tiên gặp bài toán, tôi chưa quá quan tâm đến ngôn ngữ lập trình, hệ thống có những thành phần, services nào. Tôi suggest bạn Trung chú trọng vào 2 phần:
1. Phân chia các thành phần công việc theo đúng nghiệp vụ
2. Xác định bài toán bằng Test case trước khi bắt tay vào thực hiện
Dựa vào Test cases, tách các danh từ (object) và hành động (action) để xây dựng cụ thể từng đối tượng xây dựng.
2 hệ thống Ví và gửi tin phải được phân việc hoàn toàn độc lập - single responsibility (SOLID)
Vẫn chưa nói đến câu chuyện code như thế nào, dùng service gì, tôi đề xuất cho Trung tiện phương án giải quyết
Hệ thống nhận request từ người dùng qua API, mỗi request chứa thông tin về số lượng tin và loại tin (ví dụ: 120 tin A, 50 tin B). Để hỗ trợ xử lý bất đồng bộ và tránh blocking, request được đẩy vào một hàng đợi (message queue). Cách tiếp cận này giúp tách biệt việc nhận và xử lý, phù hợp với sản lượng lớn.
Hệ thống lấy request từ hàng đợi và kiểm tra tính hợp lệ:
- Số lượng tin phải lớn hơn 0.
- Loại tin phải thuộc danh sách hợp lệ (A, B, hoặc C). Nếu request không hợp lệ, hệ thống log lỗi (bao gồm request_id và lý do) và bỏ qua. Nếu hợp lệ, request được chuyển sang bước trừ tiền. Bước này tuy đơn giản nhưng cần test kỹ để đảm bảo dữ liệu đầu vào luôn sạch.
Đây là bước cốt lõi, nơi hệ thống xử lý logic trừ tiền và quyết định số tin có thể gửi. Để đảm bảo an toàn đa luồng, tôi đề xuất sử dụng lock để khóa ví của đại lý. Logic xử lý như sau:
1. Lấy số dư hiện tại của ví (current_balance).
2. Tính chi phí toàn bộ request: total_cost = num_messages * price, với price là giá của loại tin (100đ, 200đ, hoặc 300đ).
3. So sánh số dư với chi phí:
- Nếu số dư đủ (current_balance >= total_cost):
- Trừ tiền: current_balance -= total_cost.
- Đánh dấu toàn bộ số tin trong request là "ready to send".
- Nếu số dư không đủ (current_balance < total_cost):
- Tính số tin tối đa có thể gửi:max_messages = floor(current_balance / price).
- Tính chi phí cho max_messages: Magnum Opuspartial_cost = max_messages * price.
- Trừ tiền: current_balance -= partial_cost.
- Đánh dấu max_messages tin là "ready to send".
- Log phần còn lại(num_messages - max_messages) với thông tin: request_id, số lượng tin không gửi, loại tin, và lý do "Số dư không đủ".
4. Giải phóng lock sau khi xử lý.
5. Cập nhật số dư ví mới (vào database nếu cần - hoặc temp db).
Cơ chế cắt nhỏ request đảm bảo tận dụng tối đa số dư ví, và việc sử dụng lock giúp tránh race condition. Tuy nhiên, logic này phức tạp và dễ xảy ra lỗi nếu không có test đầy đủ.
Số tin được đánh dấu "ready to send" được chuyển đến hệ thống gửi tin (SMS gateway). Hệ thống log trạng thái gửi (thành công hoặc thất bại).
Hệ thống tổng hợp kết quả:
- Số tin đã gửi.
- Số tin không gửi được.
- Số tiền đã trừ.
- Số dư ví còn lại (thường là 0 nếu request bị cắt nhỏ).
Log chi tiết các tin không gửi được, bao gồm request_id, số lượng, loại tin, và lý do. Kết quả được trả về cho người dùng qua API hoặc giao diện.
Case study về bài toán Trừ tiền gói trả trước không chỉ là một bài tập kỹ thuật mà còn là cơ hội để tôi và Trung tiện cùng trao đổi lại giá trị của tư duy TDD - test first. Tiếp cận bài toán không bắt đầu từ code như thế nào, từ test, từ flow xử lý, từ nhận request, xác thực, trừ tiền với cắt nhỏ request, gửi tin, đến báo cáo, để giải quyết bài toán một cách hiệu quả và trọng vẹn hơn.
Điều quan trọng nhất là cách chúng ta sử dụng test để đảm bảo chất lượng và học hỏi từ kinh nghiệm của nhau để tránh sai lầm.
Nếu bạn đang phát triển một hệ thống phức tạp, hãy bắt đầu với tests và xây dựng một bộ test cases đầy đủ.
Và nếu bạn có câu chuyện nào, case study nào về Tests hãy chia sẻ trong phần bình luận nhé!
/Son Do - I share what works (and what didn’t). Let’s grow together.
#SoftwareDevelopment #TDD #ProblemSolving #TechBlog #DotNet #Mentorship #Testing #SystemDesign #TechCommunity #VietnamTech
#1percentbetter #wecommit100xshare