cursor done.

This commit is contained in:
2025-11-11 14:36:09 +08:00
parent 7a5fb889c5
commit 9b1eb6cafd
27 changed files with 4748 additions and 552 deletions

11
server/config.js Normal file
View File

@@ -0,0 +1,11 @@
const path = require('path');
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const DB_PATH = path.join(__dirname, '..', 'data.db');
module.exports = {
PORT,
JWT_SECRET,
DB_PATH
};

54
server/database.js Normal file
View File

@@ -0,0 +1,54 @@
const sqlite3 = require('sqlite3').verbose();
const { DB_PATH } = require('./config');
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
console.error('数据库连接失败:', err.message);
process.exit(1);
}
console.log('已连接到 SQLite 数据库');
});
function initializeDatabase() {
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS calendar_marks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date TEXT NOT NULL,
status TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, date),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_user_date ON calendar_marks(user_id, date)`);
});
}
initializeDatabase();
function closeDatabase() {
return new Promise((resolve) => {
db.close((err) => {
if (err) {
console.error('关闭数据库失败:', err.message);
} else {
console.log('数据库连接已关闭');
}
resolve();
});
});
}
module.exports = {
db,
closeDatabase
};

23
server/middleware/auth.js Normal file
View File

@@ -0,0 +1,23 @@
const jwt = require('jsonwebtoken');
const { JWT_SECRET } = require('../config');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ success: false, error: '未提供认证令牌' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ success: false, error: '无效的认证令牌' });
}
req.user = user;
next();
});
}
module.exports = {
authenticateToken
};

86
server/routes/auth.js Normal file
View File

@@ -0,0 +1,86 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { db } = require('../database');
const { JWT_SECRET } = require('../config');
const { authenticateToken } = require('../middleware/auth');
const router = express.Router();
router.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, error: '用户名和密码不能为空' });
}
if (username.length < 3) {
return res.status(400).json({ success: false, error: '用户名至少需要3个字符' });
}
if (password.length < 6) {
return res.status(400).json({ success: false, error: '密码至少需要6个字符' });
}
const passwordHash = await bcrypt.hash(password, 10);
db.run(
'INSERT INTO users (username, password_hash) VALUES (?, ?)',
[username, passwordHash],
function (err) {
if (err) {
if (err.message.includes('UNIQUE constraint failed')) {
return res.status(400).json({ success: false, error: '用户名已存在' });
}
return res.status(500).json({ success: false, error: '注册失败' });
}
const token = jwt.sign({ userId: this.lastID, username }, JWT_SECRET, { expiresIn: '7d' });
res.json({ success: true, token, userId: this.lastID, username });
}
);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
router.post('/login', (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, error: '用户名和密码不能为空' });
}
db.get(
'SELECT * FROM users WHERE username = ?',
[username],
async (err, user) => {
if (err) {
return res.status(500).json({ success: false, error: '登录失败' });
}
if (!user) {
return res.status(401).json({ success: false, error: '用户名或密码错误' });
}
const validPassword = await bcrypt.compare(password, user.password_hash);
if (!validPassword) {
return res.status(401).json({ success: false, error: '用户名或密码错误' });
}
const token = jwt.sign({ userId: user.id, username: user.username }, JWT_SECRET, { expiresIn: '7d' });
res.json({ success: true, token, userId: user.id, username: user.username });
}
);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
router.get('/me', authenticateToken, (req, res) => {
res.json({ success: true, userId: req.user.userId, username: req.user.username });
});
module.exports = router;

92
server/routes/calendar.js Normal file
View File

@@ -0,0 +1,92 @@
const express = require('express');
const { db } = require('../database');
const router = express.Router();
router.get('/', (req, res) => {
const userId = req.user.userId;
db.all(
'SELECT date, status FROM calendar_marks WHERE user_id = ?',
[userId],
(err, rows) => {
if (err) {
return res.status(500).json({ success: false, error: '获取数据失败' });
}
const markedDates = {};
rows.forEach((row) => {
markedDates[row.date] = row.status;
});
res.json({ success: true, markedDates });
}
);
});
router.post('/', (req, res) => {
const userId = req.user.userId;
const { markedDates } = req.body || {};
if (!markedDates || typeof markedDates !== 'object') {
return res.status(400).json({ success: false, error: '无效的数据格式' });
}
const dbOperations = Object.entries(markedDates).map(([date, status]) => {
return new Promise((resolve, reject) => {
db.run(
`INSERT INTO calendar_marks (user_id, date, status, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(user_id, date) DO UPDATE SET
status = excluded.status,
updated_at = CURRENT_TIMESTAMP`,
[userId, date, status],
(err) => {
if (err) reject(err);
else resolve();
}
);
});
});
const dates = Object.keys(markedDates);
if (dates.length > 0) {
const placeholders = dates.map(() => '?').join(',');
dbOperations.push(
new Promise((resolve, reject) => {
db.run(
`DELETE FROM calendar_marks WHERE user_id = ? AND date NOT IN (${placeholders})`,
[userId, ...dates],
(err) => {
if (err) reject(err);
else resolve();
}
);
})
);
} else {
dbOperations.push(
new Promise((resolve, reject) => {
db.run(
'DELETE FROM calendar_marks WHERE user_id = ?',
[userId],
(err) => {
if (err) reject(err);
else resolve();
}
);
})
);
}
Promise.all(dbOperations)
.then(() => {
res.json({ success: true, message: '数据保存成功' });
})
.catch((err) => {
console.error('保存数据失败:', err);
res.status(500).json({ success: false, error: '保存数据失败' });
});
});
module.exports = router;

41
server/utils/shutdown.js Normal file
View File

@@ -0,0 +1,41 @@
const { closeDatabase } = require('../database');
function registerShutdown(server) {
let isShuttingDown = false;
async function shutdown(reason) {
if (isShuttingDown) return;
isShuttingDown = true;
if (reason) {
console.log(`\n收到 ${reason},正在关闭服务器...`);
}
await new Promise((resolve) => {
server.close(() => {
console.log('HTTP 服务器已关闭');
resolve();
});
});
await closeDatabase();
process.exit(reason === 'uncaughtException' || reason === 'unhandledRejection' ? 1 : 0);
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', err);
shutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('未处理的 Promise 拒绝:', reason);
shutdown('unhandledRejection');
});
}
module.exports = {
registerShutdown
};