const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); const axios = require('axios'); const XLSX = require('xlsx'); let db = null; function initDb(dbPath) { const dir = path.dirname(dbPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } db = new Database(dbPath); db.exec(` CREATE TABLE IF NOT EXISTS exercise_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, unit TEXT NOT NULL, weekly_goal INTEGER DEFAULT 0 ) `); try { const cols = db.prepare("PRAGMA table_info(exercise_types)").all(); if (!cols.some(c => c.name === 'sort_order')) { db.exec("ALTER TABLE exercise_types ADD COLUMN sort_order INTEGER DEFAULT 0"); } } catch (e) {} db.exec(` CREATE TABLE IF NOT EXISTS daily_plans ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, is_active BOOLEAN DEFAULT 1 ) `); try { const cols = db.prepare("PRAGMA table_info(daily_plans)").all(); if (!cols.some(c => c.name === 'is_active')) { db.exec("ALTER TABLE daily_plans ADD COLUMN is_active BOOLEAN DEFAULT 1"); } // Normalize NULL values to active db.exec("UPDATE daily_plans SET is_active = 1 WHERE is_active IS NULL"); } catch (e) {} db.exec(` CREATE TABLE IF NOT EXISTS exercise_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, record_date TEXT NOT NULL, calories INTEGER NOT NULL DEFAULT 0, exercise_duration INTEGER NOT NULL DEFAULT 0, stand_count INTEGER NOT NULL DEFAULT 0, daily_plan_id INTEGER, diastolic_bp INTEGER, systolic_bp INTEGER, heart_rate INTEGER, waist_circ INTEGER, weight_kg REAL, created_time TEXT NOT NULL, FOREIGN KEY (daily_plan_id) REFERENCES daily_plans(id) ) `); try { const cols = db.prepare("PRAGMA table_info(exercise_records)").all(); const colNames = new Set(cols.map(c => c.name)); if (!colNames.has('diastolic_bp')) { db.exec("ALTER TABLE exercise_records ADD COLUMN diastolic_bp INTEGER"); } if (!colNames.has('systolic_bp')) { db.exec("ALTER TABLE exercise_records ADD COLUMN systolic_bp INTEGER"); } if (!colNames.has('waist_circ')) { db.exec("ALTER TABLE exercise_records ADD COLUMN waist_circ INTEGER"); } if (!colNames.has('heart_rate')) { db.exec("ALTER TABLE exercise_records ADD COLUMN heart_rate INTEGER"); } if (!colNames.has('weight_kg')) { db.exec("ALTER TABLE exercise_records ADD COLUMN weight_kg REAL"); } } catch (e) { console.error('[FitnessDiary] Migration error:', e.message); } db.exec(` CREATE TABLE IF NOT EXISTS exercise_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, record_id INTEGER NOT NULL, exercise_type_id INTEGER NOT NULL, quantity INTEGER NOT NULL DEFAULT 0, unit TEXT NOT NULL, FOREIGN KEY (record_id) REFERENCES exercise_records(id) ON DELETE CASCADE, FOREIGN KEY (exercise_type_id) REFERENCES exercise_types(id) ) `); // Performance indexes try { db.exec(`CREATE INDEX IF NOT EXISTS idx_records_date ON exercise_records(record_date)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_items_record ON exercise_items(record_id)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_items_type ON exercise_items(exercise_type_id)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_records_plan ON exercise_records(daily_plan_id)`); } catch (e) {} // Seed or import data for daily_plans/exercise_types when empty try { const typeCount = db.prepare('SELECT COUNT(1) AS c FROM exercise_types').get().c || 0; const planCount = db.prepare('SELECT COUNT(1) AS c FROM daily_plans').get().c || 0; if (planCount === 0) { // Try import from old DB first const oldPath = path.join('d:\\', 'Trae_Files', 'FitnessDiary_1', 'exercise_log.db'); let imported = false; try { if (fs.existsSync(oldPath)) { const OldDB = require('better-sqlite3'); const old = new OldDB(oldPath); const oldPlans = old.prepare('SELECT id, name FROM daily_plans').all(); if (Array.isArray(oldPlans) && oldPlans.length) { const stmt = db.prepare('INSERT OR IGNORE INTO daily_plans (name, description, is_active) VALUES (?, ?, 1)'); for (const p of oldPlans) { stmt.run(String(p.name || '').trim(), '', 1); } imported = true; } } } catch {} if (!imported) { // Fallback seed defaults const defaults = ['正常训练', '轻断食', '跑步日', '力量日', '恢复日', '高强运动日']; const stmt = db.prepare('INSERT OR IGNORE INTO daily_plans (name, description, is_active) VALUES (?, ?, 1)'); defaults.forEach(n => stmt.run(n, '', 1)); } } if (typeCount === 0) { // Try import exercise_types from old DB try { const oldPath = path.join('d:\\', 'Trae_Files', 'FitnessDiary_1', 'exercise_log.db'); if (fs.existsSync(oldPath)) { const OldDB = require('better-sqlite3'); const old = new OldDB(oldPath); const oldTypes = old.prepare('SELECT id, name, unit, weekly_goal FROM exercise_types').all(); if (Array.isArray(oldTypes) && oldTypes.length) { const stmt = db.prepare('INSERT OR IGNORE INTO exercise_types (name, unit, weekly_goal, sort_order) VALUES (?, ?, ?, ?)'); let sort = 0; for (const t of oldTypes) { stmt.run(String(t.name || '').trim(), String(t.unit || '').trim(), parseInt(t.weekly_goal || 0), sort++); } } } } catch {} } } catch {} } function getDb() { return db; } // Helper: Push to Weekly async function pushToWeekly(recordId) { try { const record = db.prepare(` SELECT r.*, p.name as plan_name FROM exercise_records r LEFT JOIN daily_plans p ON r.daily_plan_id = p.id WHERE r.id = ? `).get(recordId); if (!record || record.calories <= 100) { if (record && record.calories <= 100) console.log(`[FitnessDiary] Skipping push to weekly: calories ${record.calories} <= 100`); return; } const dateStr = String(record.record_date || '').slice(0, 10); if (!dateStr) return; const records = db.prepare(` SELECT r.*, p.name as plan_name FROM exercise_records r LEFT JOIN daily_plans p ON r.daily_plan_id = p.id WHERE r.record_date = ? ORDER BY r.id `).all(dateStr); if (!Array.isArray(records) || !records.length) return; const ids = records.map(r => r.id); let itemsByRecord = {}; if (ids.length) { const placeholders = ids.map(() => '?').join(','); const rows = db.prepare(` SELECT i.record_id, i.quantity, i.unit, t.name FROM exercise_items i LEFT JOIN exercise_types t ON i.exercise_type_id = t.id WHERE i.record_id IN (${placeholders}) `).all(...ids); for (const it of rows) { const rid = it.record_id; if (!itemsByRecord[rid]) itemsByRecord[rid] = []; itemsByRecord[rid].push(it); } } const escapeHtml = s => String(s || '') .replace(/&/g, '&') .replace(//g, '>'); let totalCalories = 0; let totalDuration = 0; let totalStand = 0; let html = ''; for (const r of records) { const list = itemsByRecord[r.id] || []; if (!list.length) { html += ``; } else { for (const it of list) { html += ``; } } totalCalories += Number(r.calories || 0); totalDuration += Number(r.exercise_duration || 0); totalStand += Number(r.stand_count || 0); } html += '
日期计划项目数量单位
${escapeHtml(dateStr)}${escapeHtml(r.plan_name || '')}
${escapeHtml(dateStr)}${escapeHtml(r.plan_name || '')}${escapeHtml(it.name || '')}${Number(it.quantity || 0)}${escapeHtml(it.unit || '')}
'; const payload = { key: process.env.FITNESS_DIARY_API_KEY || process.env.SYSTEM_AUTH_PASS_93220 || '', external_id: String(recordId), date: dateStr, items: html, calories: totalCalories, duration: totalDuration }; // Assuming local server is running on process.env.PORT or default 8976 const port = process.env.PORT || 8976; const url = `http://127.0.0.1:${port}/api/weekly/ext/fitness`; console.log(`[FitnessDiary] Pushing to weekly: ${url}`, JSON.stringify(payload)); await axios.post(url, payload); console.log('[FitnessDiary] Push successful'); } catch (err) { console.error('[FitnessDiary] Failed to push to weekly:', err.message); } } async function deleteFromWeekly(recordId) { try { const port = process.env.PORT || 8976; const url = `http://127.0.0.1:${port}/api/weekly/ext/fitness`; const payload = { key: process.env.FITNESS_DIARY_API_KEY || process.env.SYSTEM_AUTH_PASS_93220 || '', external_id: String(recordId), action: 'delete' }; console.log(`[FitnessDiary] Deleting from weekly: ${url}`, JSON.stringify(payload)); await axios.post(url, payload); console.log('[FitnessDiary] Delete push successful'); } catch (err) { console.error('[FitnessDiary] Failed to delete from weekly:', err.message); } } // Auth Helper - DEPRECATED: Auth handled by main server middleware // const NAV_AUTH_COOKIE = 'nav_gate'; // const parseCookie = ... // const checkAuth = ... function bindRoutes(app) { const API = '/api/fitness_diary'; // Disable caching for all fitness_diary API responses app.use(API, (req, res, next) => { try { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.set('Pragma', 'no-cache'); res.set('Expires', '0'); res.set('Surrogate-Control', 'no-store'); } catch {} next(); }); // Get all exercise types app.get(`${API}/types`, (req, res) => { try { const rows = db.prepare('SELECT * FROM exercise_types ORDER BY sort_order ASC, name ASC').all(); res.json({ ok: true, data: rows }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Get all daily plans app.get(`${API}/daily_plans`, (req, res) => { try { let hasActive = false; try { const cols = db.prepare('PRAGMA table_info(daily_plans)').all(); hasActive = cols.some(c => c.name === 'is_active'); } catch {} const sql = hasActive ? 'SELECT * FROM daily_plans WHERE is_active = 1 ORDER BY id' : 'SELECT * FROM daily_plans ORDER BY id'; const rows = db.prepare(sql).all(); res.json({ ok: true, data: rows }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Get records (with optional date filter) app.get(`${API}/records`, (req, res) => { try { const { start, end } = req.query; // console.log(`[FitnessDiary] Get records: start=${start}, end=${end}`); let sql = ` SELECT r.*, p.name as plan_name FROM exercise_records r LEFT JOIN daily_plans p ON r.daily_plan_id = p.id `; const params = []; if (start && end) { sql += ` WHERE r.record_date >= ? AND r.record_date <= ?`; params.push(start, end); } sql += ` ORDER BY r.record_date DESC`; const records = db.prepare(sql).all(...params); // console.log(`[FitnessDiary] Found ${records.length} records`); const ids = records.map(r => r.id); let itemsByRecord = {}; if (ids.length) { const placeholders = ids.map(()=>'?').join(','); const items = db.prepare(` SELECT i.*, t.name as type_name FROM exercise_items i LEFT JOIN exercise_types t ON i.exercise_type_id = t.id WHERE i.record_id IN (${placeholders}) `).all(...ids); for (const it of items) { const rid = it.record_id; if (!itemsByRecord[rid]) itemsByRecord[rid] = []; itemsByRecord[rid].push(it); } } for (const rec of records) { rec.items = itemsByRecord[rec.id] || []; } res.json({ ok: true, data: records }); } catch (e) { console.error('[FitnessDiary] Get records error:', e); res.status(500).json({ ok: false, error: e.message }); } }); // Add new record app.post(`${API}/records`, async (req, res) => { try { const { record_date, calories, exercise_duration, stand_count, daily_plan_id, diastolic_bp, systolic_bp, heart_rate, waist_circ, weight_kg, items } = req.body; // 1. Check duplicate for same day const existing = db.prepare('SELECT id FROM exercise_records WHERE record_date = ?').get(record_date); if (existing) { return res.json({ ok: false, error: '该日期已存在记录,请勿重复添加' }); } // 2. Insert Record const insertRecord = db.prepare(` INSERT INTO exercise_records (record_date, calories, exercise_duration, stand_count, daily_plan_id, diastolic_bp, systolic_bp, heart_rate, waist_circ, weight_kg, created_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime')) `); let planId = null; if (daily_plan_id !== undefined && daily_plan_id !== null && daily_plan_id !== '') { const n = parseInt(daily_plan_id, 10); if (!Number.isNaN(n) && n > 0) planId = n; } const getIntOrNull = (val) => { if (val === undefined || val === null || val === '') return null; const n = parseInt(val, 10); return (!Number.isNaN(n) && n > 0) ? n : null; }; const getRealOrNull = (val) => { if (val === undefined || val === null || val === '') return null; const n = parseFloat(val); return (!Number.isNaN(n)) ? n : null; }; const info = insertRecord.run( record_date, calories, exercise_duration, stand_count, planId, getIntOrNull(diastolic_bp), getIntOrNull(systolic_bp), getIntOrNull(heart_rate), getIntOrNull(waist_circ), getRealOrNull(weight_kg) ); const recordId = info.lastInsertRowid; // 3. Insert Items const insertItem = db.prepare(` INSERT INTO exercise_items (record_id, exercise_type_id, quantity, unit) VALUES (?, ?, ?, ?) `); if (Array.isArray(items)) { for (const it of items) { insertItem.run(recordId, it.exercise_type_id, it.quantity, it.unit); } } // 4. Async Push to Weekly const pushResult = await pushToWeekly(recordId); res.json({ ok: true, id: recordId, push: pushResult }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Delete record app.delete(`${API}/records/:id`, async (req, res) => { try { const { id } = req.params; db.prepare('DELETE FROM exercise_records WHERE id = ?').run(id); // Items cascade delete automatically try { await deleteFromWeekly(id); } catch {} res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Record detail app.get(`${API}/record/:id`, (req, res) => { try { const { id } = req.params; const r = db.prepare(` SELECT r.*, p.name as plan_name FROM exercise_records r LEFT JOIN daily_plans p ON r.daily_plan_id = p.id WHERE r.id = ? `).get(id); if (!r) return res.status(404).json({ ok: false, error: 'not_found' }); const items = db.prepare(` SELECT i.id, i.exercise_type_id, i.quantity, i.unit, t.name FROM exercise_items i LEFT JOIN exercise_types t ON i.exercise_type_id = t.id WHERE i.record_id = ? `).all(id); res.json({ id: r.id, record_date: r.record_date, calories: r.calories, exercise_duration: r.exercise_duration, stand_count: r.stand_count, diastolic_bp: r.diastolic_bp, systolic_bp: r.systolic_bp, heart_rate: r.heart_rate, waist_circ: r.waist_circ, weight_kg: r.weight_kg, daily_plan: r.plan_name || '', daily_plan_id: r.daily_plan_id, items }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Update record app.put(`${API}/records/:id`, async (req, res) => { try { const { id } = req.params; const body = req.body || {}; const r = db.prepare(`SELECT * FROM exercise_records WHERE id = ?`).get(id); if (!r) return res.status(404).json({ ok: false, error: 'not_found' }); const recordDate = new Date(String(r.record_date).slice(0,10)+'T00:00:00'); const diffDays = Math.floor((Date.now() - recordDate.getTime())/86400000); if (diffDays > 31) return res.status(400).json({ ok: false, error: '超过一个月的历史日期不允许修改' }); const getRealOrNull = (val) => { if (val === undefined || val === null || val === '') return null; const n = parseFloat(val); return (!Number.isNaN(n)) ? n : null; }; db.prepare(` UPDATE exercise_records SET daily_plan_id = ?, calories = ?, exercise_duration = ?, stand_count = ?, diastolic_bp = ?, systolic_bp = ?, heart_rate = ?, waist_circ = ?, weight_kg = ? WHERE id = ? `).run((() => { const v = body.daily_plan_id; if (v === undefined || v === null || v === '') return null; const n = parseInt(v, 10); return (!Number.isNaN(n) && n > 0) ? n : null; })(), parseInt(body.calories||0), parseInt(body.exercise_duration||0), parseInt(body.stand_count||0), body.diastolic_bp ? parseInt(body.diastolic_bp, 10) : null, body.systolic_bp ? parseInt(body.systolic_bp, 10) : null, body.heart_rate ? parseInt(body.heart_rate, 10) : null, body.waist_circ ? parseInt(body.waist_circ, 10) : null, getRealOrNull(body.weight_kg), id); db.prepare(`DELETE FROM exercise_items WHERE record_id = ?`).run(id); if (Array.isArray(body.items)) { const stmt = db.prepare(`INSERT INTO exercise_items (record_id, exercise_type_id, quantity, unit) VALUES (?, ?, ?, ?)`); for (const it of body.items) { stmt.run(id, it.exercise_type_id, parseInt(it.quantity||0), String(it.unit||'')); } } const pushResult = await pushToWeekly(id); res.json({ ok: true, push: pushResult }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Stats (Sum for range) app.get(`${API}/stats`, (req, res) => { try { const { start, end } = req.query; let sql = ` SELECT SUM(calories) as sum_calories, SUM(exercise_duration) as sum_duration, SUM(stand_count) as sum_stand FROM exercise_records `; const params = []; if (start && end) { sql += ` WHERE record_date >= ? AND record_date <= ?`; params.push(start, end); } const stats = db.prepare(sql).get(...params); res.json({ ok: true, stats: { sum_calories: stats.sum_calories || 0, sum_duration: stats.sum_duration || 0, sum_stand: stats.sum_stand || 0 } }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Weekly Progress (for progress bars) app.get(`${API}/weekly_progress`, (req, res) => { try { const fmt = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; const baseDateStr = (req.query.date ? String(req.query.date).slice(0, 10) : fmt(new Date())); const d = new Date(baseDateStr + 'T00:00:00'); const day = d.getDay() || 7; const start = new Date(d); start.setDate(d.getDate() - day + 1); const end = new Date(start); end.setDate(start.getDate() + 6); const sStr = fmt(start); const eStr = fmt(end); // Get all types and goals const types = db.prepare('SELECT * FROM exercise_types ORDER BY sort_order ASC, id ASC').all(); const progress = {}; types.forEach(t => { progress[t.id] = { ...t, done: 0 }; }); // Sum up records in range const records = db.prepare(` SELECT i.exercise_type_id, i.quantity FROM exercise_items i JOIN exercise_records r ON i.record_id = r.id WHERE r.record_date >= ? AND r.record_date <= ? `).all(sStr, eStr); records.forEach(r => { if (progress[r.exercise_type_id]) { progress[r.exercise_type_id].done += r.quantity; } }); const result = Object.values(progress).map(p => ({ type_id: p.id, name: p.name, unit: p.unit, weekly_goal: p.weekly_goal, done: p.done, remaining: Math.max(0, p.weekly_goal - p.done) })); res.json({ ok: true, week_start: sStr, week_end: eStr, progress: result }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Update types app.post(`${API}/types/update`, (req, res) => { try { const body = req.body || {}; const arr = Array.isArray(body.types) ? body.types : []; const names = arr.map(x => String(x.name||'').trim()).filter(Boolean); const dup = names.filter((n, i) => names.indexOf(n) !== i); if (dup.length) return res.status(400).json({ ok: false, error: `项目名称重复:${Array.from(new Set(dup)).join('、')}` }); const update = db.prepare(`UPDATE exercise_types SET name = ?, unit = ?, weekly_goal = ?, sort_order = ? WHERE id = ?`); const checkUnique = db.prepare(`SELECT id FROM exercise_types WHERE name = ? AND id != ?`); for (const t of arr) { const name = String(t.name||'').trim(); const unit = String(t.unit||'').trim(); const goal = parseInt(t.weekly_goal||0); const sort = parseInt(t.sort_order||0); const id = parseInt(t.id||0); if (!id || !name || !unit) continue; const exists = checkUnique.get(name, id); if (exists) return res.status(400).json({ ok: false, error: '保存失败:项目名称已存在(数据库唯一约束)。请检查并修改后再保存。' }); update.run(name, unit, goal, sort, id); } res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Add type app.post(`${API}/types/add`, (req, res) => { try { const { name, unit, weekly_goal } = req.body || {}; const n = String(name||'').trim(); const u = String(unit||'').trim(); const g = parseInt(weekly_goal||0); if (!n || !u) return res.status(400).json({ ok: false, error: '请输入项目名称和单位' }); const exists = db.prepare(`SELECT id FROM exercise_types WHERE name = ?`).get(n); if (exists) return res.status(400).json({ ok: false, error: '该项目已存在' }); db.prepare(`INSERT INTO exercise_types (name, unit, weekly_goal, sort_order) VALUES (?, ?, ?, ?)`).run(n, u, g, 0); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Delete type app.delete(`${API}/types/:id`, (req, res) => { try { const id = parseInt(req.params.id || '0', 10); if (!id) return res.status(400).json({ ok: false, error: '参数错误' }); const used = db.prepare(`SELECT COUNT(1) AS c FROM exercise_items WHERE exercise_type_id = ?`).get(id); if (Number(used.c || 0) > 0) { return res.status(400).json({ ok: false, error: '该项目已在记录中使用,无法删除' }); } db.prepare(`DELETE FROM exercise_types WHERE id = ?`).run(id); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Update plans app.post(`${API}/plans/update`, (req, res) => { try { const body = req.body || {}; const arr = Array.isArray(body.plans) ? body.plans : []; const names = arr.map(x => String(x.name||'').trim()).filter(Boolean); const dup = names.filter((n, i) => names.indexOf(n) !== i); if (dup.length) return res.status(400).json({ ok: false, error: `计划名称重复:${Array.from(new Set(dup)).join('、')}` }); const update = db.prepare(`UPDATE daily_plans SET name = ? WHERE id = ?`); const checkUnique = db.prepare(`SELECT id FROM daily_plans WHERE name = ? AND id != ?`); for (const p of arr) { const name = String(p.name||'').trim(); const id = parseInt(p.id||0); if (!id || !name) continue; const exists = checkUnique.get(name, id); if (exists) return res.status(400).json({ ok: false, error: '保存失败:计划名称已存在。' }); update.run(name, id); } res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Add plan app.post(`${API}/plans/add`, (req, res) => { try { const { name } = req.body || {}; const n = String(name||'').trim(); if (!n) return res.status(400).json({ ok: false, error: '请输入计划名称' }); const exists = db.prepare(`SELECT id FROM daily_plans WHERE name = ?`).get(n); if (exists) return res.status(400).json({ ok: false, error: '该计划已存在' }); db.prepare(`INSERT INTO daily_plans (name) VALUES (?)`).run(n); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Delete plan (soft delete) app.delete(`${API}/plans/:id`, (req, res) => { try { const id = parseInt(req.params.id || '0', 10); if (!id) return res.status(400).json({ ok: false, error: '参数错误' }); db.prepare(`UPDATE daily_plans SET is_active = 0 WHERE id = ?`).run(id); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Clear data app.post(`${API}/records/clear`, (req, res) => { try { db.prepare(`DELETE FROM exercise_items`).run(); db.prepare(`DELETE FROM exercise_records`).run(); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // Auto-fill missing records for previous days app.post(`${API}/records/auto-fill-missing`, (req, res) => { try { const today = new Date(); const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`; // Get all records before today, ordered by date descending const allRecords = db.prepare(` SELECT id, record_date, calories, exercise_duration, stand_count FROM exercise_records WHERE record_date < ? ORDER BY record_date DESC `).all(todayStr); if (!allRecords || allRecords.length === 0) { return res.json({ ok: true, filled: 0, message: '无历史空缺记录' }); } // Group records by date (in case of multiple records per day) const recordsByDate = {}; allRecords.forEach(r => { const dateStr = String(r.record_date).slice(0, 10); if (!recordsByDate[dateStr]) { recordsByDate[dateStr] = []; } recordsByDate[dateStr].push(r); }); const filledDates = []; const updateStmt = db.prepare(` UPDATE exercise_records SET calories = ?, exercise_duration = ?, stand_count = ? WHERE id = ? `); // Check each date before today Object.keys(recordsByDate).sort().forEach(dateStr => { const records = recordsByDate[dateStr]; // Check if any record on this date has missing fields records.forEach(record => { const needsFill = (record.calories === 0 || record.calories === null) || (record.exercise_duration === 0 || record.exercise_duration === null) || (record.stand_count === 0 || record.stand_count === null); if (needsFill) { const newCalories = (record.calories === 0 || record.calories === null) ? 330 : record.calories; const newDuration = (record.exercise_duration === 0 || record.exercise_duration === null) ? 45 : record.exercise_duration; const newStandCount = (record.stand_count === 0 || record.stand_count === null) ? 12 : record.stand_count; updateStmt.run(newCalories, newDuration, newStandCount, record.id); if (!filledDates.includes(dateStr)) { filledDates.push(dateStr); } } }); }); const filledCount = filledDates.length; const message = filledCount > 0 ? `已自动补填 ${filledCount} 天空缺记录:${filledDates.join('、')}` : '无空缺记录需要补填'; console.log(`[FitnessDiary] Auto-fill missing: ${message}`); res.json({ ok: true, filled: filledCount, dates: filledDates, message }); } catch (e) { console.error('[FitnessDiary] Auto-fill error:', e); res.status(500).json({ ok: false, error: e.message }); } }); // Export Excel app.get(`${API}/export`, (req, res) => { try { const { start, end } = req.query; let sql = `SELECT r.*, p.name as plan_name FROM exercise_records r LEFT JOIN daily_plans p ON r.daily_plan_id = p.id`; const params = []; if (start && end) { sql += ` WHERE r.record_date >= ? AND r.record_date <= ?`; params.push(start, end); } sql += ` ORDER BY r.record_date ASC`; const records = db.prepare(sql).all(...params); const items = []; for (const r of records) { const its = db.prepare(` SELECT i.*, t.name as type_name FROM exercise_items i LEFT JOIN exercise_types t ON i.exercise_type_id = t.id WHERE i.record_id = ? `).all(r.id); its.forEach(it => items.push({ id: it.id, record_id: r.id, date: r.record_date, exercise_type: it.type_name || '', quantity: it.quantity, unit: it.unit })); } const ws1 = XLSX.utils.json_to_sheet(records.map(r => ({ id: r.id, record_date: r.record_date, daily_plan: r.plan_name || '', calories: r.calories, exercise_duration: r.exercise_duration, stand_count: r.stand_count, created_time: r.created_time }))); const ws2 = XLSX.utils.json_to_sheet(items); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws1, 'Records'); XLSX.utils.book_append_sheet(wb, ws2, 'Items'); const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); const startName = (start || '').replace(/-/g, '') || 'start'; const endName = (end || '').replace(/-/g, '') || 'end'; res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename="export_${startName}_${endName}.xlsx"`); res.send(buf); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); } module.exports = { initDb, getDb, bindRoutes };