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 += `| ${escapeHtml(dateStr)} | ${escapeHtml(r.plan_name || '')} | | | |
`;
} else {
for (const it of list) {
html += `| ${escapeHtml(dateStr)} | ${escapeHtml(r.plan_name || '')} | ${escapeHtml(it.name || '')} | ${Number(it.quantity || 0)} | ${escapeHtml(it.unit || '')} |
`;
}
}
totalCalories += Number(r.calories || 0);
totalDuration += Number(r.exercise_duration || 0);
totalStand += Number(r.stand_count || 0);
}
html += '
';
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
};