Bới móc bài toán Trừ tiền trong Gói trả trước – tầm quan trọng của "tư duy" TDD

Công nghệ - 22/04/2025 10:08:22

Bạn từng nghĩ trừ tiền trong gói trả trước là chuyện đơn giản? Bài viết này hé lộ một case study đầy bất ngờ, nơi mọi thứ bắt đầu không từ code, mà từ test – và tư duy hệ thống mới là chìa khóa giải quyết vấn đề!

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é!


Bài toán từ Trung tiện

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.


Những vấn đề trong bài toán Trừ tiền

Sau khi trao đổi, chúng tôi chốt được một số vấn đề bài toán như sau:

Thông tin cơ bản:

  • Đại lý có ví thanh toán, ví dụ: 10,000đ.
  • Có 3 loại tin với giá cố định: Tin A (100đ), Tin B (200đ), Tin C (300đ).
  • Mỗi request gửi tin yêu cầu gửi một số lượng tin cụ thể (ví dụ: 120 tin A, 50 tin B).
  • Sản lượng lớn, khoảng 10,000 tin/lệnh, yêu cầu xử lý đa luồng.

Yêu cầu nghiệp vụ:

  • Trừ tiền từ ví trước khi gửi tin.
  • Nếu số dư ví đủ, gửi toàn bộ số tin trong request.
  • Nếu số dư không đủ, cắt nhỏ request để gửi tối đa số tin có thể, trừ tiền đến khi ví cạn (số dư = 0).
  • Log chi tiết các tin không gửi được (phần còn lại của request) thay vì đánh dấu toàn bộ request là thất bại.
  • Đảm bảo xử lý chính xác trong môi trường đa luồng, tránh race condition.

Thách thức:

  • Độ chính xác: Trừ tiền phải atomic, không để số dư âm hoặc bị trùng lặp.
  • Hiệu suất: Xử lý sản lượng lớn với đa luồng mà không gây tắc nghẽn.
  • Tính linh hoạt: Cắt nhỏ request để tối ưu hóa sử dụng số dư ví, đồng thời cung cấp log rõ ràng.
  • Khả năng debug: Log phải đủ chi tiết để tra cứu khi có lỗi.

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.


Tiếp cận bài toán

Đầ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

Test cases - điều đầu tiên phải tư duy đến khi nhận bất kỳ bài toán nào

  • Test case 1: Số dư đủ cho toàn bộ request (ví dụ: ví 10,000đ, request 70 tin A). Kiểm tra: gửi 70 tin, trừ 7,000đ, số dư còn 3,000đ.
  • Test case 2: Số dư không đủ, cắt nhỏ request (ví dụ: ví 10,000đ, request 120 tin A). Kiểm tra: gửi 100 tin, trừ 10,000đ, log 20 tin không gửi, số dư = 0.
  • Test case 3: Nhiều request đồng thời (ví dụ: ví 10,000đ, request 1: 120 tin B, request 2: 50 tin A). Kiểm tra: gửi 50 tin B, log 70 tin B và 50 tin A không gửi, số dư = 0.
  • Test case 4: Request không hợp lệ (ví dụ: số lượng tin = 0). Kiểm tra: log lỗi, không trừ tiền, không gửi tin.
  • Test case 5: Kiểm tra race condition (gửi 100 request đồng thời). Kiểm tra: số dư không âm, tổng số tiền trừ khớp với số tin gửi.

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.

Đối tượng trong Bài toán (draft)

  • : làm business, kiểm tra tiền, trừ tiền, trừ tiền temp - chủ đề trừ tiền temp này rất hay nha - trả phí 1 bữa sáng để tối ưu đoạn này nhé Trung tiện :D
  • Hệ thống gửi tin: chỉ tập trung làm nhiệm vụ gửi tin.
  • Logging

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)


Phương án giải quyết - hầu như ở tầng Ứng dụng

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

Bước 1: Nhận request gửi tin

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.

Bước 2: Xác thực request

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.

Bước 3: Trừ tiền ví với cơ chế cắt nhỏ request

Đâ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 đủ.

Bước 4: Gửi tin

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

Bước 5: Báo cáo và log

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.


Kết luậ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

Công nghệ

Xem tất cả