const https = require('https'); /** * Fetch holiday data from timor.tech API for a specific year * @param {number} year * @returns {Promise} */ function fetchHolidaysFromNetwork(year) { return new Promise((resolve, reject) => { // Note: timor.tech API works best with trailing slash const url = `https://timor.tech/api/holiday/year/${year}/`; https.get(url, { headers: { 'User-Agent': 'WorkTime-Manager/1.0' } }, (res) => { let rawData = ''; res.on('data', (chunk) => { rawData += chunk; }); res.on('end', () => { try { const parsed = JSON.parse(rawData); if (parsed.code === 0 && parsed.holiday) { resolve(parsed.holiday); } else { reject(new Error(`API returned code ${parsed.code}`)); } } catch (e) { reject(e); } }); }).on('error', (err) => { reject(err); }); }); } /** * Get holiday data for a specific year, from cache or network * @param {object} db DB wrapper instance * @param {number} year * @returns {Promise} */ async function getHolidays(db, year) { try { const cached = db.get('SELECT holiday_data FROM holiday_cache WHERE year = ?', [year]); if (cached) { return JSON.parse(cached.holiday_data); } // Not cached, fetch from network console.log(`[Holiday] Fetching holiday data from network for year ${year}...`); const holidays = await fetchHolidaysFromNetwork(year); // Save to cache db.run( 'INSERT OR REPLACE INTO holiday_cache (year, holiday_data, updated_at) VALUES (?, ?, datetime(\'now\', \'localtime\'))', [year, JSON.stringify(holidays)] ); return holidays; } catch (e) { console.error(`[Holiday] Error getting holidays for ${year}:`, e.message); return null; } } /** * Helper to check if a date is weekend (Sat/Sun) in local time * @param {string} dateStr YYYY-MM-DD * @returns {boolean} */ function isWeekendDefault(dateStr) { const d = new Date(dateStr + 'T00:00:00'); const day = d.getDay(); return day === 0 || day === 6; } /** * Calculate the number of workdays for each month in a given year * @param {number} year * @param {object|null} holidayMap * @returns {Array<{month: number, days: number}>} */ function calculateMonthlyWorkdays(year, holidayMap) { const result = []; for (let m = 1; m <= 12; m++) { const daysInMonth = new Date(year, m, 0).getDate(); let workdays = 0; for (let day = 1; day <= daysInMonth; day++) { const dStr = `${year}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const mmDd = `${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`; let isWork = true; if (holidayMap && holidayMap[mmDd]) { // If date is in the map, holiday === true means rest day, holiday === false means work day (調休补班) isWork = !holidayMap[mmDd].holiday; } else { // Fall back to default weekends isWork = !isWeekendDefault(dStr); } if (isWork) { workdays++; } } result.push({ month: m, days: workdays }); } return result; } /** * Sync holiday data from network, calculate and update database workdays * @param {object} db * @param {number} year * @returns {Promise>} */ async function syncYearWorkdays(db, year) { // Clear cache first to force fresh fetch on manual sync request db.run('DELETE FROM holiday_cache WHERE year = ?', [year]); const holidayMap = await getHolidays(db, year); if (!holidayMap) { throw new Error('无法从网络同步节假日数据,请检查网络连接'); } const monthlyWorkdays = calculateMonthlyWorkdays(year, holidayMap); // Save to workdays table for (const item of monthlyWorkdays) { const existing = db.get('SELECT id FROM workdays WHERE year = ? AND month = ?', [year, item.month]); if (existing) { db.run('UPDATE workdays SET days = ? WHERE year = ? AND month = ?', [item.days, year, item.month]); } else { db.run('INSERT INTO workdays (year, month, days) VALUES (?, ?, ?)', [year, item.month, item.days]); } } return monthlyWorkdays; } module.exports = { getHolidays, syncYearWorkdays, calculateMonthlyWorkdays };