refactor: 系统优化 - 错误处理、输入验证、无限滚动
主要改进: - 新增配置管理 (server/config.js) - 新增日志工具 (server/utils/logger.js) - 添加全局错误处理中间件,统一错误响应格式 - 添加输入验证和 AppError 错误类 - 重构 db.js 迁移逻辑,代码更清晰 - 前端列表改为无限滚动加载,每页 20 条 - 封装 AppState 状态管理模块 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
19e5a48f07
commit
d551841035
+104
-74
@@ -1,95 +1,125 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
const db = new Database(path.join(__dirname, '..', 'data.db'));
|
||||
|
||||
// 启用 WAL 模式和外键约束
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
// 创建表
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
/**
|
||||
* 创建表结构
|
||||
*/
|
||||
function createTables() {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('income', 'expense')),
|
||||
category TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
quantity INTEGER,
|
||||
unit_price REAL,
|
||||
boss TEXT,
|
||||
partner TEXT,
|
||||
source TEXT,
|
||||
destination TEXT,
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('income', 'expense')),
|
||||
category TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
quantity INTEGER,
|
||||
unit_price REAL,
|
||||
boss TEXT,
|
||||
partner TEXT,
|
||||
source TEXT,
|
||||
destination TEXT,
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_records_user_id ON records(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_category ON records(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_created_at ON records(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_type ON records(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_boss ON records(boss);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_partner ON records(partner);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_source ON records(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_user_date ON records(user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_user_category ON records(user_id, category, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_destination ON records(destination);
|
||||
`);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_user_id ON records(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_category ON records(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_created_at ON records(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_type ON records(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_boss ON records(boss);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_partner ON records(partner);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_source ON records(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_user_date ON records(user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_user_category ON records(user_id, category, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_destination ON records(destination);
|
||||
`);
|
||||
logger.debug('表结构创建完成');
|
||||
}
|
||||
|
||||
// 迁移:去掉 category 的 CHECK 约束
|
||||
try {
|
||||
const tableInfo = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='records'").get();
|
||||
if (tableInfo && tableInfo.sql.includes("CHECK(category IN")) {
|
||||
db.exec(`
|
||||
CREATE TABLE records_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('income', 'expense')),
|
||||
category TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
quantity INTEGER,
|
||||
unit_price REAL,
|
||||
boss TEXT,
|
||||
partner TEXT,
|
||||
source TEXT,
|
||||
destination TEXT,
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
INSERT INTO records_new SELECT * FROM records;
|
||||
DROP TABLE records;
|
||||
ALTER TABLE records_new RENAME TO records;
|
||||
`);
|
||||
/**
|
||||
* 迁移:去掉 category 的 CHECK 约束
|
||||
*/
|
||||
function migrateRemoveCategoryCheck() {
|
||||
try {
|
||||
const tableInfo = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='records'").get();
|
||||
if (tableInfo && tableInfo.sql.includes("CHECK(category IN")) {
|
||||
logger.info('执行迁移:移除 category CHECK 约束');
|
||||
db.exec(`
|
||||
CREATE TABLE records_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('income', 'expense')),
|
||||
category TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
quantity INTEGER,
|
||||
unit_price REAL,
|
||||
boss TEXT,
|
||||
partner TEXT,
|
||||
source TEXT,
|
||||
destination TEXT,
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
INSERT INTO records_new SELECT * FROM records;
|
||||
DROP TABLE records;
|
||||
ALTER TABLE records_new RENAME TO records;
|
||||
`);
|
||||
logger.info('迁移完成:category CHECK 约束已移除');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('迁移跳过 (category):', e.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Migration skipped:', e.message);
|
||||
}
|
||||
|
||||
// 迁移:添加 rebate 列(派单返点)
|
||||
try {
|
||||
const cols = db.prepare("PRAGMA table_info(records)").all();
|
||||
if (!cols.find(c => c.name === 'rebate')) {
|
||||
db.exec("ALTER TABLE records ADD COLUMN rebate REAL");
|
||||
/**
|
||||
* 迁移:添加 rebate 列
|
||||
*/
|
||||
function migrateAddRebateColumn() {
|
||||
try {
|
||||
const cols = db.prepare("PRAGMA table_info(records)").all();
|
||||
if (!cols.find(c => c.name === 'rebate')) {
|
||||
logger.info('执行迁移:添加 rebate 列');
|
||||
db.exec("ALTER TABLE records ADD COLUMN rebate REAL");
|
||||
logger.info('迁移完成:rebate 列已添加');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('迁移跳过 (rebate):', e.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Rebate migration skipped:', e.message);
|
||||
}
|
||||
|
||||
// 初始化默认用户 lizhilun
|
||||
const existingUser = db.prepare('SELECT id FROM users WHERE username = ?').get('lizhilun');
|
||||
if (!existingUser) {
|
||||
const hash = bcrypt.hashSync('lizhilun', 10);
|
||||
db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run('lizhilun', hash);
|
||||
/**
|
||||
* 初始化默认用户
|
||||
*/
|
||||
function initDefaultUser() {
|
||||
const existingUser = db.prepare('SELECT id FROM users WHERE username = ?').get('lizhilun');
|
||||
if (!existingUser) {
|
||||
const hash = bcrypt.hashSync('lizhilun', 10);
|
||||
db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run('lizhilun', hash);
|
||||
logger.info('默认用户 lizhilun 已创建');
|
||||
}
|
||||
}
|
||||
|
||||
// 执行初始化
|
||||
createTables();
|
||||
migrateRemoveCategoryCheck();
|
||||
migrateAddRebateColumn();
|
||||
initDefaultUser();
|
||||
|
||||
module.exports = db;
|
||||
|
||||
Reference in New Issue
Block a user