Trong quá trình phát triển phần mềm hàng ngày, việc xác định một ngày cụ thể là ngày làm việc, cuối tuần, ngày lễ quốc gia hay ngày làm bù thay thế ngày lễ là rất phổ biến. Ví dụ, một hệ thống có thể cần gửi thông báo đẩy đến người dùng chỉ trong giờ làm việc. Bài viết này sẽ hướng dẫn cách truy xuất dữ liệu lịch đầy đủ cho cả năm, bao gồm ngày làm việc, cuối tuần, ngày lễ và ngày làm bù, bằng cách sử dụng các API công cộng miễn phí.
Giới thiệu API
Chúng ta sẽ sử dụng kết hợp hai API để có được thông tin lịch toàn diện nhất:
API Lịch chung (bao gồm cuối tuần):
- URL:
https://api.apihubs.cn/holiday/get?size=500&year=2021 - API này cung cấp dữ liệu cho tất cả các ngày trong năm, phân biệt giữa ngày làm việc và cuối tuần.
- URL:
API Ngày lễ và Ngày bù lễ:
- URL:
https://timor.tech/api/holiday/year/2021 - API này chuyên cung cấp thông tin chi tiết về các ngày lễ quốc gia và các ngày làm bù được quy định.
- URL:
Triển khai mã nguồn
Để xử lý dữ liệu lịch, chúng ta cần một lớp để biểu diễn chi tiết từng ngày và một lớp tiện ích để thực hiện các cuộc gọi API và tổng hợp dữ liệu.
Lớp đối tượng NgayLeChiTiet
Lớp này đại diện cho thông tin chi tiết của một ngày, bao gồm ngày, loại ngày và mô tả. Chúng ta sẽ sử dụng Lombok để tự động tạo getter/setter và phương thức toString() thông qua chú thích @Data.
import lombok.Data;
/**
* Lớp đối tượng biểu diễn chi tiết một ngày trong lịch.
*/
@Data
public class NgayLeChiTiet {
private String ngay; // Định dạng ngày: YYYY-MM-DD
private int loaiNgay; // 0: Ngày làm việc, 1: Cuối tuần, 2: Ngày lễ công, 3: Ngày bù lễ
private String moTa; // Mô tả chi tiết về ngày
}
Lớp tiện ích KetNoiAPIUtils để gửi yêu cầu HTTP
Lớp này chịu trách nhiệm gửi các yêu cầu GET đến các API bên ngoài và trả về phản hồi JSON.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* Lớp tiện ích để gửi các yêu cầu HTTP GET.
*/
public class KetNoiAPIUtils {
private static final int THOI_GIAN_CHO_DOC_MS = 30 * 1000; // 30 giây
private static final int THOI_GIAN_CHO_KET_NOI_MS = 30 * 1000; // 30 giây
private static final String USER_AGENT_STRING = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
/**
* Gửi yêu cầu GET đến một URL và trả về phản hồi dưới dạng chuỗi.
*
* @param diaChiURL URL đích của yêu cầu GET.
* @return Phản hồi từ server dưới dạng chuỗi.
* @throws IOException nếu có lỗi trong quá trình kết nối hoặc đọc dữ liệu.
*/
public static String guiYeuCauGet(String diaChiURL) throws IOException {
StringBuilder phanHoi = new StringBuilder();
URL url = new URL(diaChiURL);
HttpURLConnection ketNoi = null;
try {
ketNoi = (HttpURLConnection) url.openConnection();
ketNoi.setRequestMethod("GET");
ketNoi.setReadTimeout(THOI_GIAN_CHO_DOC_MS);
ketNoi.setConnectTimeout(THOI_GIAN_CHO_KET_NOI_MS);
ketNoi.setRequestProperty("Accept-Charset", StandardCharsets.UTF_8.name());
ketNoi.setRequestProperty("User-Agent", USER_AGENT_STRING);
int maPhanHoi = ketNoi.getResponseCode();
if (maPhanHoi == HttpURLConnection.HTTP_OK) {
try (BufferedReader doc = new BufferedReader(new InputStreamReader(ketNoi.getInputStream(), StandardCharsets.UTF_8))) {
String dong;
while ((dong = doc.readLine()) != null) {
phanHoi.append(dong);
}
}
} else {
// Xử lý các phản hồi không thành công
throw new IOException("Yêu cầu API thất bại với mã trạng thái: " + maPhanHoi + " cho URL: " + diaChiURL);
}
} finally {
if (ketNoi != null) {
ketNoi.disconnect();
}
}
return phanHoi.toString();
}
}
Lớp chính XuLyLichNgayLe để tổng hợp dữ liệu
Lớp này chứa logic để gọi hai API, phân tích cú pháp dữ liệu JSON và kết hợp chúng để tạo ra danh sách lịch hoàn chỉnh cho một năm cụ thể.
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.text.ParseException; // Giữ lại cho tương thích với SimpleDateFormat nếu dùng
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Lớp tiện ích để xử lý và tổng hợp dữ liệu lịch ngày làm việc, cuối tuần và ngày lễ.
*/
public class XuLyLichNgayLe {
// Định nghĩa hằng số cho các loại ngày
public static final int NGAY_LAM_VIEC = 0;
public static final int CUOI_TUAN = 1;
public static final int NGAY_LE_CONG = 2;
public static final int NGAY_BU_LE = 3;
private static final String URL_API_LICH_TRON_NAM = "https://api.apihubs.cn/holiday/get?size=500&year=";
private static final String URL_API_NGAY_LE_BU_LE = "https://timor.tech/api/holiday/year/";
/**
* Lấy dữ liệu lịch đầy đủ cho một năm cụ thể, bao gồm ngày làm việc, cuối tuần, ngày lễ và ngày bù lễ.
*
* @param nam Năm cần truy xuất dữ liệu lịch (ví dụ: "2021").
* @return Danh sách các đối tượng NgayLeChiTiet đã được sắp xếp.
* @throws IOException nếu có lỗi trong quá trình giao tiếp API.
*/
public static List<NgayLeChiTiet> layLichNam(String nam) throws IOException {
// Sử dụng LinkedHashMap để duy trì thứ tự chèn, sau đó có thể sắp xếp lại.
Map<String, NgayLeChiTiet> lichTheoNgay = new LinkedHashMap<>();
ObjectMapper trinhChuyenDoiJson = new ObjectMapper();
DateTimeFormatter dinhDangNgay = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// BƯỚC 1: Lấy dữ liệu lịch toàn bộ ngày trong năm (bao gồm cuối tuần) từ API Hubs
String phanHoiLichChung = KetNoiAPIUtils.guiYeuCauGet(URL_API_LICH_TRON_NAM + nam);
Map<String, Object> duLieuLichChung = trinhChuyenDoiJson.readValue(phanHoiLichChung, Map.class);
Map<String, Object> phanDataLichChung = (Map<String, Object>) duLieuLichChung.get("data");
List<Map<String, Object>> danhSachNgayChung = (List<Map<String, Object>>) phanDataLichChung.get("list");
for (Map<String, Object> duLieuNgay : danhSachNgayChung) {
NgayLeChiTiet chiTietNgay = new NgayLeChiTiet();
// API Hubs trả về 'date' dưới dạng YYYYMMDD
String fullDateString = duLieuNgay.get("date").toString();
chiTietNgay.setNgay(LocalDate.parse(fullDateString, DateTimeFormatter.BASIC_ISO_DATE).format(dinhDangNgay));
// 'weekend' là "1" nếu là cuối tuần, "0" nếu là ngày làm việc
if ("1".equals(duLieuNgay.get("weekend").toString())) {
chiTietNgay.setLoaiNgay(CUOI_TUAN);
chiTietNgay.setMoTa("Cuối tuần");
} else {
chiTietNgay.setLoaiNgay(NGAY_LAM_VIEC);
chiTietNgay.setMoTa("Ngày làm việc");
}
lichTheoNgay.put(chiTietNgay.getNgay(), chiTietNgay);
}
// BƯỚC 2: Lấy dữ liệu ngày lễ và ngày bù lễ từ Timor.tech API
String phanHoiNgayLeBuLe = KetNoiAPIUtils.guiYeuCauGet(URL_API_NGAY_LE_BU_LE + nam);
Map<String, Object> duLieuNgayLeBuLe = trinhChuyenDoiJson.readValue(phanHoiNgayLeBuLe, Map.class);
Map<String, Map<String, Object>> danhSachChiTietLe = (Map<String, Map<String, Object>>) duLieuNgayLeBuLe.get("holiday");
if (danhSachChiTietLe != null) {
danhSachChiTietLe.forEach((ngayLeKey, chiTietLe) -> {
String ngayLeStr = chiTietLe.get("date").toString();
NgayLeChiTiet ngayHienTai = lichTheoNgay.getOrDefault(ngayLeStr, new NgayLeChiTiet());
ngayHienTai.setNgay(ngayLeStr);
// API Timor.tech dùng 'isHoliday': true cho ngày lễ, false cho ngày bù lễ
boolean laNgayLeThucSu = (boolean) chiTietLe.get("isHoliday");
String tenLe = chiTietLe.get("name").toString();
if (laNgayLeThucSu) {
ngayHienTai.setLoaiNgay(NGAY_LE_CONG);
ngayHienTai.setMoTa("Ngày lễ công (" + tenLe + ")");
} else { // Đây là một ngày làm bù
ngayHienTai.setLoaiNgay(NGAY_BU_LE);
ngayHienTai.setMoTa("Ngày bù lễ (" + tenLe + ")");
}
lichTheoNgay.put(ngayLeStr, ngayHienTai);
});
}
List<NgayLeChiTiet> ketQuaCuoiCung = new ArrayList<>(lichTheoNgay.values());
// Sắp xếp danh sách theo ngày tăng dần
ketQuaCuoiCung.sort(Comparator.comparing(ngay -> LocalDate.parse(ngay.getNgay(), dinhDangNgay)));
return ketQuaCuoiCung;
}
/**
* Phương thức main để kiểm tra chức năng.
*/
public static void main(String[] args) {
try {
// Lấy dữ liệu lịch cho năm 2021
List<NgayLeChiTiet> danhSachNgay = layLichNam("2021");
System.out.println("Dữ liệu lịch đầy đủ trong năm:");
for (NgayLeChiTiet ngay : danhSachNgay) {
System.out.println(ngay);
}
} catch (IOException e) {
e.printStackTrace();
System.err.println("Đã xảy ra lỗi khi lấy dữ liệu lịch: " + e.getMessage());
}
}
}
Kết quả mẫu
Khi chạy phương thức main, bạn sẽ nhận được đầu ra tương tự như sau (đối với năm 2021):
NgayLeChiTiet(ngay=2021-01-01, loaiNgay=2, moTa=Ngày lễ công (元旦))
NgayLeChiTiet(ngay=2021-01-02, loaiNgay=2, moTa=Ngày lễ công (元旦))
NgayLeChiTiet(ngay=2021-01-03, loaiNgay=2, moTa=Ngày lễ công (元旦))
NgayLeChiTiet(ngay=2021-01-04, loaiNgay=0, moTa=Ngày làm việc)
NgayLeChiTiet(ngay=2021-01-05, loaiNgay=0, moTa=Ngày làm việc)
NgayLeChiTiet(ngay=2021-01-06, loaiNgay=0, moTa=Ngày làm việc)
NgayLeChiTiet(ngay=2021-01-07, loaiNgay=0, moTa=Ngày làm việc)
NgayLeChiTiet(ngay=2021-01-08, loaiNgay=0, moTa=Ngày làm việc)
NgayLeChiTiet(ngay=2021-01-09, loaiNgay=1, moTa=Cuối tuần)
NgayLeChiTiet(ngay=2021-01-10, loaiNgay=1, moTa=Cuối tuần)
NgayLeChiTiet(ngay=2021-01-11, loaiNgay=0, moTa=Ngày làm việc)
...
NgayLeChiTiet(ngay=2021-02-07, loaiNgay=3, moTa=Ngày bù lễ (春节))
...
NgayLeChiTiet(ngay=2021-02-11, loaiNgay=2, moTa=Ngày lễ công (除夕))
...
NgayLeChiTiet(ngay=2021-02-20, loaiNgay=3, moTa=Ngày bù lễ (春节))
...
NgayLeChiTiet(ngay=2021-09-18, loaiNgay=3, moTa=Ngày bù lễ (中秋节))
...
NgayLeChiTiet(ngay=2021-10-09, loaiNgay=3, moTa=Ngày bù lễ (国庆节))
...
Dữ liệu đầy đủ cho cả năm có thể được lưu trữ vào cơ sở dữ liệu để sử dụng lâu dài, chỉ cần gọi API để đồng bộ hóa dữ liệu mỗi năm một lần.