Managing Statutory Holidays in Enterprise Attendance with SpringBoot & Vue3

The article explains how to avoid common pitfalls in handling statutory holidays for enterprise attendance systems by building an independent work‑day calendar service, storing daily date types in a database, providing unified SpringBoot APIs for rule evaluation, and offering a Vue3 visual calendar for HR to maintain holidays, adjustments, and custom company holidays.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Managing Statutory Holidays in Enterprise Attendance with SpringBoot & Vue3

Core Pain Points and Design Idea

Legal holidays change every year, the government releases the schedule irregularly, and companies also have custom holidays and make‑up work days. Hard‑coding weekdays leads to incorrect attendance and payroll calculations. The solution is to create an independent Enterprise Work Calendar that stores each day’s type and lets all modules query this calendar.

1. Three Business Pain Points

Rules are not fixed : Holiday dates and make‑up work days vary annually.

Strong coupling with multiple modules : Check‑in validation, overtime calculation, payroll, and scheduling all need holiday information.

Many custom requirements : Companies add welfare holidays, anniversary days, and department‑specific rules.

2. Overall Solution – Central Calendar + Multi‑module Calls

All date attributes are consolidated into a single calendar table where each row represents a day and records its type, holiday name, whether check‑in is required, and overtime multiplier. Business logic calls the calendar service instead of embedding holiday rules.

3. Calendar Data Model (Three Layers)

Data Layer : attendance_calendar table, one record per day, fields include date_type, holiday_name, need_checkin, overtime_rate, etc.

Service Layer : Calendar maintenance service, holiday import service, and rule‑evaluation service exposing unified date‑judgment APIs.

Application Layer : Check‑in validation, scheduling, overtime application, and payroll all depend on the calendar service.

4. Date Type Enumeration

WORKDAY

– Normal work day, needs check‑in, overtime 1.5×. REST_DAY – Weekend, no check‑in, overtime 2× (if worked). LEGAL_HOLIDAY – Statutory holiday, no check‑in, overtime 3× (no make‑up). ADJUST_WORKDAY – Make‑up work day, needs check‑in, overtime 1×. COMPANY_HOLIDAY – Custom company holiday, no check‑in, paid leave.

Database Design

2.1 Attendance Calendar Table

CREATE TABLE attendance_calendar (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    calendar_date DATE NOT NULL COMMENT '日期',
    date_type TINYINT NOT NULL COMMENT '日期类型:1工作日 2普通休息日 3法定假日 4调休补班 5企业假',
    holiday_name VARCHAR(64) COMMENT '假期名称,如春节、国庆节',
    year INT NOT NULL COMMENT '年份,用于快速筛选',
    month INT NOT NULL COMMENT '月份',
    week_day TINYINT COMMENT '星期几 1-7',
    need_checkin TINYINT DEFAULT 1 COMMENT '是否需要打卡:0否 1是',
    overtime_rate DECIMAL(2,1) DEFAULT 1.0 COMMENT '加班薪资倍率',
    remark VARCHAR(255) COMMENT '备注',
    create_time DATETIME DEFAULT NOW(),
    update_time DATETIME DEFAULT NOW(),
    UNIQUE KEY uk_calendar_date (calendar_date),
    INDEX idx_year_month (year, month)
) COMMENT '企业工作日历表';

2.2 Optional Holiday Template Table (for multi‑department scenarios)

CREATE TABLE calendar_template (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    template_name VARCHAR(64) NOT NULL,
    dept_ids VARCHAR(255) COMMENT '适用部门ID,逗号分隔',
    status TINYINT DEFAULT 1,
    create_time DATETIME DEFAULT NOW()
) COMMENT '日历模板表';

SpringBoot Backend Core Implementation

3.1 Maven Dependencies (SpringBoot 3.x + MyBatis‑Plus)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

3.2 Core Calendar Service (Unified Judgment Entry)

@Service
public class AttendanceCalendarService {
    @Autowired
    private AttendanceCalendarMapper calendarMapper;

    /** Determine if a date is a work day (needs check‑in) */
    public boolean isWorkDay(LocalDate date) {
        AttendanceCalendar calendar = getCalendar(date);
        return calendar != null && calendar.getNeedCheckin() == 1;
    }

    /** Determine if a date is a legal holiday */
    public boolean isLegalHoliday(LocalDate date) {
        AttendanceCalendar calendar = getCalendar(date);
        return calendar != null && calendar.getDateType() == 3;
    }

    public AttendanceCalendar getCalendar(LocalDate date) {
        return lambdaQuery()
                .eq(AttendanceCalendar::getCalendarDate, date)
                .one();
    }

    /** Count work days in a period */
    public long countWorkDays(LocalDate start, LocalDate end) {
        return lambdaQuery()
                .between(AttendanceCalendar::getCalendarDate, start, end)
                .eq(AttendanceCalendar::getNeedCheckin, 1)
                .count();
    }

    /** Generate default calendar for a whole year (Mon‑Fri work, weekend rest) */
    @Transactional
    public void generateYearCalendar(int year) {
        // Delete existing data for the year
        lambdaUpdate().eq(AttendanceCalendar::getYear, year).remove();
        LocalDate start = LocalDate.of(year, 1, 1);
        LocalDate end = LocalDate.of(year, 12, 31);
        List<AttendanceCalendar> list = new ArrayList<>();
        for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
            AttendanceCalendar cal = new AttendanceCalendar();
            cal.setCalendarDate(date);
            cal.setYear(year);
            cal.setMonth(date.getMonthValue());
            cal.setWeekDay(date.getDayOfWeek().getValue());
            int dayOfWeek = date.getDayOfWeek().getValue();
            if (dayOfWeek >= 1 && dayOfWeek <= 5) { // Mon‑Fri
                cal.setDateType(1);
                cal.setNeedCheckin(1);
                cal.setOvertimeRate(new BigDecimal("1.5"));
            } else { // Weekend
                cal.setDateType(2);
                cal.setNeedCheckin(0);
                cal.setOvertimeRate(new BigDecimal("2.0"));
            }
            list.add(cal);
        }
        saveBatch(list);
    }

    /** Batch update holidays after Excel import or API call */
    @Transactional
    public void batchUpdateHoliday(List<CalendarUpdateDTO> list) {
        for (CalendarUpdateDTO dto : list) {
            lambdaUpdate()
                .eq(AttendanceCalendar::getCalendarDate, dto.getDate())
                .set(AttendanceCalendar::getDateType, dto.getDateType())
                .set(AttendanceCalendar::getHolidayName, dto.getHolidayName())
                .set(AttendanceCalendar::getNeedCheckin, dto.getNeedCheckin())
                .set(AttendanceCalendar::getOvertimeRate, dto.getOvertimeRate())
                .update();
        }
    }
}

3.3 Management Backend APIs

@RestController
@RequestMapping("/attendance/calendar")
public class AttendanceCalendarController {
    @Autowired
    private AttendanceCalendarService calendarService;

    // Query calendar by month
    @GetMapping("/month")
    public Result<List<AttendanceCalendar>> getMonthCalendar(@RequestParam int year, @RequestParam int month) {
        return Result.success(calendarService.getMonthList(year, month));
    }

    // Generate default calendar for a year
    @PostMapping("/generate")
    public Result<Void> generateYear(@RequestParam int year) {
        calendarService.generateYearCalendar(year);
        return Result.success();
    }

    // Update a single day's attributes
    @PutMapping("/update")
    public Result<Void> updateOne(@RequestBody CalendarUpdateDTO dto) {
        calendarService.updateOne(dto);
        return Result.success();
    }

    // Import holidays from Excel
    @PostMapping("/import")
    public Result<Void> importExcel(MultipartFile file) {
        calendarService.importHolidayExcel(file);
        return Result.success();
    }
}

3.4 Holiday Data Sources (Three Options)

Excel batch import : HR prepares an Excel file after the government releases the schedule; the system imports it. Stable and controllable.

Third‑party holiday API : Automatic yearly sync, but depends on external service and may be blocked in intranet environments.

Built‑in lunar algorithm + manual adjustment : Generates base holidays (Spring Festival, Dragon Boat, Mid‑Autumn) and lets HR fine‑tune make‑up days.

Recommended approach: auto‑generate the default calendar, import legal holidays via Excel, and fine‑tune through the UI.

Vue3 Frontend Visual Implementation

The front end visualizes the calendar and provides interactive holiday management using Element Plus.

4.1 Monthly Calendar Component

<template>
  <div class="calendar-container">
    <div class="calendar-header">
      <el-date-picker v-model="currentMonth" type="month" placeholder="Select month" @change="loadCalendarData" />
      <el-button type="primary" @click="showImport = true">Import Holidays</el-button>
    </div>
    <el-calendar v-model="currentMonth">
      <template #date-cell="{ data }">
        <div class="calendar-cell" :class="getDateClass(data.day)">
          <span class="date-num">{{ data.day.split('-').slice(2).join('') }}</span>
          <span class="date-label" v-if="getHolidayName(data.day)">{{ getHolidayName(data.day) }}</span>
        </div>
      </template>
    </el-calendar>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { getMonthCalendar } from '@/api/attendance/calendar';

const currentMonth = ref(new Date());
const calendarMap = ref({});

const loadCalendarData = async () => {
  const year = currentMonth.value.getFullYear();
  const month = currentMonth.value.getMonth() + 1;
  const res = await getMonthCalendar(year, month);
  calendarMap.value = {};
  res.data.forEach(item => {
    calendarMap.value[item.calendarDate] = item;
  });
};

const getDateClass = (dateStr) => {
  const info = calendarMap.value[dateStr];
  if (!info) return '';
  const typeMap = { 1: 'workday', 2: 'rest-day', 3: 'legal-holiday', 4: 'adjust-workday', 5: 'company-holiday' };
  return typeMap[info.dateType] || '';
};

const getHolidayName = (dateStr) => calendarMap.value[dateStr]?.holidayName || '';

onMounted(() => loadCalendarData());
</script>

<style scoped>
.calendar-cell { height: 60px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.legal-holiday { background: #fef0f0; color: #f56c6c; }
.adjust-workday { background: #ecf5ff; color: #409eff; }
.rest-day { color: #909399; }
.date-label { font-size: 12px; }
</style>

4.2 Click‑to‑Edit Date Attributes

<el-dialog v-model="editVisible" title="Modify Date" width="400px">
  <el-form :model="editForm" label-width="80px">
    <el-form-item label="Date">
      <span>{{ editForm.calendarDate }}</span>
    </el-form-item>
    <el-form-item label="Type">
      <el-select v-model="editForm.dateType">
        <el-option label="Workday" :value="1" />
        <el-option label="Rest Day" :value="2" />
        <el-option label="Legal Holiday" :value="3" />
        <el-option label="Adjust Workday" :value="4" />
        <el-option label="Company Holiday" :value="5" />
      </el-select>
    </el-form-item>
    <el-form-item label="Holiday Name">
      <el-input v-model="editForm.holidayName" placeholder="e.g., Spring Festival" />
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="editVisible = false">Cancel</el-button>
    <el-button type="primary" @click="saveDate">Save</el-button>
  </template>
</el-dialog>

4.3 Employee‑Side Calendar Display

Employees can reuse the same calendar component, with additional badges for check‑in status, leave, and overtime, so they instantly see the month’s holiday arrangement and their attendance record.

Advanced Scenarios & Production Tips

Multiple departments with different rules : Use calendar templates linked to departments; each department queries its own template.

Partial‑day public holidays (e.g., Women’s Day) : Keep the day as a normal work day in the calendar and apply half‑day logic in the attendance rule layer.

Night‑shift cross‑day handling : Base the judgment on the shift’s start date or split the shift across two calendar days.

Year‑ahead data generation : Generate the next year’s default calendar in December; after the government releases the schedule, import updates. Provide a fallback rule that treats missing dates as Mon‑Fri work days.

Modification audit trail : Record who changed a date, when, and the before/after values for payroll auditability.

Full Summary

Enterprise attendance systems should not hard‑code holiday dates. Instead, build a maintainable work‑day calendar infrastructure: a SpringBoot backend that stores daily attributes, offers unified APIs for check‑in, scheduling, overtime, and payroll, and a Vue3 front‑end that lets HR visually manage holidays, make‑up days, and custom company holidays. This decouples holiday rules from business logic, ensures consistency across modules, and greatly improves long‑term maintainability.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontend developmentbackend developmentSpringBootMyBatis-PlusVue3attendanceHoliday Calendar
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.