first
This commit is contained in:
+223
@@ -0,0 +1,223 @@
|
||||
const initSqlJs = require('sql.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const DB_PATH = path.join(__dirname, 'data', 'worktime.db');
|
||||
const dataDir = path.join(__dirname, 'data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
let db = null;
|
||||
|
||||
// Save database to disk
|
||||
function saveDB() {
|
||||
if (db) {
|
||||
const data = db.export();
|
||||
const buffer = Buffer.from(data);
|
||||
fs.writeFileSync(DB_PATH, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-save every 30 seconds
|
||||
setInterval(saveDB, 30000);
|
||||
|
||||
// Wrapper to provide a simpler API
|
||||
class DBWrapper {
|
||||
constructor(sqlDb) {
|
||||
this.db = sqlDb;
|
||||
}
|
||||
|
||||
run(sql, params = []) {
|
||||
this.db.run(sql, params);
|
||||
saveDB();
|
||||
const result = this.db.exec("SELECT last_insert_rowid() as id");
|
||||
return { lastInsertRowid: result.length > 0 ? result[0].values[0][0] : 0 };
|
||||
}
|
||||
|
||||
get(sql, params = []) {
|
||||
const stmt = this.db.prepare(sql);
|
||||
if (params.length > 0) stmt.bind(params);
|
||||
if (stmt.step()) {
|
||||
const cols = stmt.getColumnNames();
|
||||
const vals = stmt.get();
|
||||
stmt.free();
|
||||
const row = {};
|
||||
cols.forEach((c, i) => { row[c] = vals[i]; });
|
||||
return row;
|
||||
}
|
||||
stmt.free();
|
||||
return null;
|
||||
}
|
||||
|
||||
all(sql, params = []) {
|
||||
const stmt = this.db.prepare(sql);
|
||||
if (params.length > 0) stmt.bind(params);
|
||||
const rows = [];
|
||||
while (stmt.step()) {
|
||||
const cols = stmt.getColumnNames();
|
||||
const vals = stmt.get();
|
||||
const row = {};
|
||||
cols.forEach((c, i) => { row[c] = vals[i]; });
|
||||
rows.push(row);
|
||||
}
|
||||
stmt.free();
|
||||
return rows;
|
||||
}
|
||||
|
||||
exec(sql) {
|
||||
this.db.run(sql);
|
||||
saveDB();
|
||||
}
|
||||
}
|
||||
|
||||
async function initDB() {
|
||||
const SQL = await initSqlJs();
|
||||
|
||||
if (fs.existsSync(DB_PATH)) {
|
||||
const fileBuffer = fs.readFileSync(DB_PATH);
|
||||
db = new SQL.Database(fileBuffer);
|
||||
} else {
|
||||
db = new SQL.Database();
|
||||
}
|
||||
|
||||
db.run("PRAGMA foreign_keys = ON");
|
||||
|
||||
// Create tables
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
realname TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK(role IN ('admin', 'sales', 'rd')),
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
sales_id INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY (sales_id) REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS timesheets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
client_id INTEGER NOT NULL,
|
||||
work_date TEXT NOT NULL,
|
||||
hours REAL NOT NULL CHECK(hours > 0 AND hours <= 8),
|
||||
remark TEXT DEFAULT '',
|
||||
service_type TEXT DEFAULT '售前',
|
||||
project_type TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (client_id) REFERENCES clients(id),
|
||||
UNIQUE(user_id, client_id, work_date, project_type)
|
||||
)
|
||||
`);
|
||||
|
||||
// Add columns to existing timesheets table if they don't exist
|
||||
try { db.run("ALTER TABLE timesheets ADD COLUMN service_type TEXT DEFAULT '售前'"); } catch (e) { }
|
||||
try { db.run("ALTER TABLE timesheets ADD COLUMN project_type TEXT DEFAULT ''"); } catch (e) { }
|
||||
// Add service_types to clients if not exist
|
||||
try { db.run("ALTER TABLE clients ADD COLUMN service_types TEXT DEFAULT '[]'"); } catch (e) { }
|
||||
|
||||
// Migration: update UNIQUE constraint from (user_id, client_id, work_date) to include project_type
|
||||
try {
|
||||
const res = db.exec("SELECT sql FROM sqlite_master WHERE type='table' AND name='timesheets'");
|
||||
if (res.length > 0) {
|
||||
const sqlString = res[0].values[0][0];
|
||||
if (sqlString.includes('UNIQUE(user_id, client_id, work_date)') && !sqlString.includes('UNIQUE(user_id, client_id, work_date, project_type)')) {
|
||||
db.run("PRAGMA foreign_keys=off");
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS timesheets_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
client_id INTEGER NOT NULL,
|
||||
work_date TEXT NOT NULL,
|
||||
hours REAL NOT NULL CHECK(hours > 0 AND hours <= 8),
|
||||
remark TEXT DEFAULT '',
|
||||
service_type TEXT DEFAULT '售前',
|
||||
project_type TEXT DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (client_id) REFERENCES clients(id),
|
||||
UNIQUE(user_id, client_id, work_date, project_type)
|
||||
)
|
||||
`);
|
||||
db.run("INSERT INTO timesheets_new SELECT id, user_id, client_id, work_date, hours, remark, service_type, project_type, created_at FROM timesheets");
|
||||
db.run("DROP TABLE timesheets");
|
||||
db.run("ALTER TABLE timesheets_new RENAME TO timesheets");
|
||||
db.run("PRAGMA foreign_keys=on");
|
||||
console.log("✅ Migrated timesheets table to updated UNIQUE constraint");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Migration error:", e.message);
|
||||
}
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS workdays (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
year INTEGER NOT NULL,
|
||||
month INTEGER NOT NULL CHECK(month >= 1 AND month <= 12),
|
||||
days INTEGER NOT NULL CHECK(days >= 0 AND days <= 31),
|
||||
UNIQUE(year, month)
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS project_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indexes (ignore if exists)
|
||||
try { db.run("CREATE INDEX idx_timesheets_user ON timesheets(user_id)"); } catch (e) { }
|
||||
try { db.run("CREATE INDEX idx_timesheets_client ON timesheets(client_id)"); } catch (e) { }
|
||||
try { db.run("CREATE INDEX idx_timesheets_date ON timesheets(work_date)"); } catch (e) { }
|
||||
try { db.run("CREATE INDEX idx_clients_sales ON clients(sales_id)"); } catch (e) { }
|
||||
|
||||
const wrapper = new DBWrapper(db);
|
||||
|
||||
// Seed admin
|
||||
const adminExists = wrapper.get('SELECT id FROM users WHERE username = ?', ['admin']);
|
||||
if (!adminExists) {
|
||||
const hash = bcrypt.hashSync('admin123', 10);
|
||||
wrapper.run(
|
||||
'INSERT INTO users (username, password, realname, role, status) VALUES (?, ?, ?, ?, ?)',
|
||||
['admin', hash, '系统管理员', 'admin', 1]
|
||||
);
|
||||
console.log('✅ Default admin account created (admin / admin123)');
|
||||
}
|
||||
|
||||
// Seed workdays for current year
|
||||
const currentYear = new Date().getFullYear();
|
||||
const existingWorkdays = wrapper.get('SELECT id FROM workdays WHERE year = ?', [currentYear]);
|
||||
if (!existingWorkdays) {
|
||||
const defaultDays = [22, 20, 23, 22, 21, 21, 23, 22, 22, 22, 21, 23];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
try {
|
||||
wrapper.run('INSERT INTO workdays (year, month, days) VALUES (?, ?, ?)', [currentYear, i + 1, defaultDays[i]]);
|
||||
} catch (e) { }
|
||||
}
|
||||
console.log(`✅ Default workdays for ${currentYear} initialized`);
|
||||
}
|
||||
|
||||
saveDB();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
module.exports = { initDB };
|
||||
Reference in New Issue
Block a user