Tất cả bài viết

Hướng dẫn xây dựng tính năng phân vùng giao hàng (Delivery Zones) tại Việt Nam bằng Map API

Hướng dẫn từng bước cách lập trình tính năng khoanh vùng giao hàng, tính phí ship và lọc địa chỉ theo quận huyện, phường xã tại Việt Nam.

Nếu bạn đang xây dựng một ứng dụng thương mại điện tử, app giao đồ ăn hoặc hệ thống logistics tại Việt Nam, bài toán đầu tiên bạn cần giải là: Làm sao để khoanh vùng phục vụ và tính phí giao hàng tự động?

Nhiều đội ngũ bắt đầu bằng việc tính phí theo bán kính (bán kính 2km, 5km...). Tuy nhiên, phương pháp này rất thiếu chính xác tại các đô thị lớn ở Việt Nam do sông ngòi, cầu cống, đường một chiều hoặc tình trạng kẹt xe.

Phương pháp tối ưu và thực tế nhất được các ông lớn như Grab, Shopee, hay Giaohangtietkiem áp dụng là phân vùng giao hàng theo địa giới hành chính (Quận/Huyện, Phường/Xã).

Bài viết này hướng dẫn chi tiết cách xây dựng tính năng phân vùng giao hàng bằng code thực tế, sử dụng dữ liệu ranh giới của GoGoDuk Map API và thư viện Turf.js.


Ý tưởng thiết kế hệ thống Phân vùng giao hàng

Hệ thống hoạt động qua 4 bước cơ bản:

  1. Định nghĩa vùng phục vụ: Bạn lấy đa giác ranh giới (boundary polygon) của các quận/huyện hoặc phường/xã muốn phục vụ.
  2. Chuẩn hóa địa chỉ khách hàng: Khi khách hàng nhập địa chỉ ở trang checkout, gợi ý địa chỉ tự động và phân giải nó thành một cặp tọa độ GPS (Vĩ độ - Kinh độ).
  3. Kiểm tra ranh giới (Point-in-Polygon): Chạy thuật toán kiểm tra xem điểm tọa độ của khách hàng nằm trong đa giác ranh giới nào.
  4. Tính phí: Áp dụng mức phí ship đã cấu hình cho phân vùng đó.
[Địa chỉ Khách nhập] ──> [Geocoding API] ──> [Tọa độ Lat/Lng]


[Áp phí Giao hàng] <── [Khớp phân vùng] <── [Kiểm tra Point-in-Polygon]

Bước 1: Lấy dữ liệu ranh giới hành chính Việt Nam

Thay vì phải tự mua bản quyền dữ liệu địa giới đắt đỏ, bạn có thể gọi endpoint /v1/admin-boundaries của GoGoDuk để lấy đa giác dạng GeoJSON của 63 tỉnh/thành phố và hơn 700 quận/huyện hoàn toàn miễn phí.

Ví dụ, để lấy ranh giới của Quận 1, Thành phố Hồ Chí Minh:

curl -H "X-API-Key: gdk_live_..." \
  "https://api.gogoduk.com/v1/admin-boundaries/resolve?district=Quan+1&province=Ho+Chi+Minh"

Response trả về sẽ chứa thông tin địa lý dạng GeoJSON:

{
  "name": "Quận 1",
  "province": "Thành phố Hồ Chí Minh",
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        [106.6974, 10.7852],
        [106.7041, 10.7811],
        ...
      ]
    ]
  }
}

Bước 2: Chuẩn hóa địa chỉ và lấy tọa độ GPS

Khi khách hàng gõ địa chỉ, chúng ta dùng endpoint gợi ý tự động (suggest) để giúp người dùng chọn địa chỉ chuẩn, tránh việc họ gõ sai chính tả hay gõ địa chỉ không tồn tại.

// Gọi khi người dùng đang gõ
async function getSuggestions(input) {
  const res = await fetch(`https://api.gogoduk.com/v1/suggest?input=${encodeURIComponent(input)}`, {
    headers: { "X-API-Key": "gdk_live_..." }
  });
  const data = await res.json();
  return data.predictions; // Trả về danh sách địa chỉ gợi ý chuẩn
}

Khi khách chọn một địa điểm trong danh sách (ví dụ: placeId = "gdk_place_12345"), ta gọi endpoint resolve để lấy tọa độ lat/lng chính xác:

async function resolveLocation(placeId) {
  const res = await fetch(`https://api.gogoduk.com/v1/resolve?id=${placeId}`, {
    headers: { "X-API-Key": "gdk_live_..." }
  });
  const data = await res.json();
  const { lat, lon } = data.result.location;
  return [lon, lat]; // Định dạng [lng, lat] dùng cho GeoJSON/Turf.js
}

Bước 3: Thuật toán kiểm tra điểm nằm trong vùng giao hàng (Point-in-Polygon)

Bây giờ bạn đã có tọa độ của khách hàng là một điểm Point = [lng, lat], và bạn có dữ liệu ranh giới của Quận 1 là một Polygon. Làm thế nào để kiểm tra điểm đó có nằm trong quận hay không?

Chúng ta có thể xử lý việc này ở tầng Backend bằng database (ví dụ PostGIS sử dụng hàm ST_Contains) hoặc xử lý trực tiếp trên ứng dụng Node.js/Browser bằng thư viện mã nguồn mở Turf.js.

Cài đặt Turf.js helper:

npm install @turf/boolean-point-in-polygon @turf/helpers

Đoạn code JS kiểm tra điểm nằm trong đa giác:

import pointInPolygon from "@turf/boolean-point-in-polygon";
import { point, polygon } from "@turf/helpers";
 
// Tọa độ khách hàng nhận từ GoGoDuk Resolve API
const customerCoordinate = [106.6994, 10.7769]; // Chợ Bến Thành
const customerPoint = point(customerCoordinate);
 
// Ranh giới Quận 1 lấy từ GoGoDuk Admin Boundaries API
const districtBoundaryGeometry = {
  "type": "Polygon",
  "coordinates": [
    [
      [106.6925, 10.7850],
      [106.7080, 10.7800],
      [106.7020, 10.7680],
      [106.6880, 10.7730],
      [106.6925, 10.7850] // Khép kín polygon
    ]
  ]
};
const districtPolygon = polygon(districtBoundaryGeometry.coordinates);
 
// Chạy thuật toán kiểm tra
const isInside = pointInPolygon(customerPoint, districtPolygon);
 
if (isInside) {
  console.log("Khách hàng nằm trong vùng giao hàng của Quận 1!");
  // Áp dụng biểu phí giao hàng của Quận 1 (ví dụ: 15,000 VND)
} else {
  console.log("Địa chỉ nằm ngoài phạm vi phục vụ.");
}

Bước 4: Tự động hóa tính phí vận chuyển theo Quận/Huyện

Để tối ưu hóa luồng thanh toán, bạn nên định nghĩa một danh sách biểu phí trong database của mình:

Vùng phục vụ (ID Quận)Phí giao hàngThời gian dự kiến
Quận 115.000 VND15-30 phút
Quận Bình Thạnh20.000 VND30-45 phút
Quận 730.000 VND45-60 phút

Quy trình checkout tự động hoàn chỉnh trên server:

app.post("/api/checkout/shipping-fee", async (req, res) => {
  try {
    const { placeId } = req.body;
    
    // 1. Dịch Place ID thành tọa độ GPS
    const customerLatLng = await resolveLocation(placeId);
    
    // 2. Chạy hàm reverse geocode hoặc ST_Contains trong DB để xem điểm đó
    // thuộc ranh giới của Quận nào. Ở đây chúng ta gọi database để kiểm tra.
    const activeDistricts = await db.query(
      "SELECT id, name, shipping_fee FROM delivery_zones WHERE ST_Contains(geom, ST_SetSRID(ST_Point($1, $2), 4326))",
      [customerLatLng.lng, customerLatLng.lat]
    );
 
    if (activeDistricts.rows.length > 0) {
      const zone = activeDistricts.rows[0];
      return res.json({
        success: true,
        district: zone.name,
        fee: zone.shipping_fee,
      });
    } else {
      return res.json({
        success: false,
        message: "Rất tiếc, địa chỉ của bạn nằm ngoài vùng phục vụ của chúng tôi."
      });
    }
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Kết luận

Xây dựng tính năng phân vùng giao hàng theo ranh giới hành chính thay vì bán kính hình tròn giúp doanh nghiệp của bạn loại bỏ các tranh chấp về phí giao hàng với shipper, hạn chế hủy đơn và dự báo chính xác thời gian vận chuyển tại các khu vực đặc thù ở Việt Nam.

Với dữ liệu địa giới được cập nhật liên tục và các API gợi ý địa chỉ tối ưu tiếng Việt của GoGoDuk, bạn có thể tự tin thiết kế hệ thống giao vận chuyên nghiệp chỉ trong vài giờ.

Bắt đầu tích hợp miễn phí ngay bằng cách truy cập tài liệu hướng dẫn tại GoGoDuk Docs.

Muốn dùng GoGoDuk?

Miễn phí trọn đời — 100 request/ngày mỗi tài khoản, không cần thẻ tín dụng. Giới hạn cao hơn theo yêu cầu.

Đăng ký →