const path = require('path'); const express = require('express'); const cors = require('cors'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const sqlite3 = require('sqlite3').verbose(); const PORT = process.env.PORT || 3000; const JWT_SECRET = process.env.JWT_SECRET || 'CHANGE_ME_TO_A_RANDOM_SECRET'; const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data.sqlite'); const app = express(); app.use(cors()); app.use(express.json()); // ===== DB ===== const db = new sqlite3.Database(DB_PATH); function run(sql, params = []) { return new Promise((resolve, reject) => { db.run(sql, params, function (err) { if (err) return reject(err); resolve({ lastID: this.lastID, changes: this.changes }); }); }); } function get(sql, params = []) { return new Promise((resolve, reject) => { db.get(sql, params, (err, row) => { if (err) return reject(err); resolve(row); }); }); } function all(sql, params = []) { return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) return reject(err); resolve(rows); }); }); } async function initDb() { await run('PRAGMA foreign_keys = ON;'); await run(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, name TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin','leader','staff')), phone TEXT, email TEXT, status TEXT NOT NULL DEFAULT 'active', creator_id INTEGER, created_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY(creator_id) REFERENCES users(id) ); `); await run(` CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, staff_id INTEGER NOT NULL, regDate TEXT, -- YYYY-MM-DD customerName TEXT NOT NULL, phone TEXT, expireDate TEXT, -- YYYY-MM-DD amount REAL NOT NULL DEFAULT 0, serviceType TEXT, remark TEXT, lastRenewAt TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY(staff_id) REFERENCES users(id) ); `); const admin = await get('SELECT * FROM users WHERE username = ?', ['admin']); if (!admin) { const hash = await bcrypt.hash('admin123456', 10); await run( 'INSERT INTO users(username, name, password_hash, role, status) VALUES(?,?,?,?,?)', ['admin', '老板', hash, 'admin', 'active'] ); console.log('已创建默认管理员账号:xs / xs123456...'); } const staff = await get('SELECT * FROM users WHERE username = ?', ['staff1']); if (!staff) { const hash = await bcrypt.hash('staff123456', 10); await run( 'INSERT INTO users(username, name, password_hash, role, status) VALUES(?,?,?,?,?)', ['staff1', '员工1', hash, 'staff', 'active'] ); console.log('已创建默认员工账号:staff1 / staff123456'); } // 创建默认组长账号 const leader = await get('SELECT * FROM users WHERE username = ?', ['leader1']); if (!leader) { const hash = await bcrypt.hash('leader123456', 10); await run( 'INSERT INTO users(username, name, password_hash, role, status) VALUES(?,?,?,?,?)', ['leader1', '组长1', hash, 'leader', 'active'] ); console.log('已创建默认组长账号:leader1 / leader123456'); } } function asyncHandler(fn) { return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); } // ===== Auth middleware ===== function authRequired(req, res, next) { const auth = req.headers.authorization || ''; const token = auth.startsWith('Bearer ') ? auth.slice(7) : ''; if (!token) return res.status(401).json({ message: '未登录' }); try { const payload = jwt.verify(token, JWT_SECRET); req.user = payload; // {id, username, role, name} next(); } catch (e) { return res.status(401).json({ message: '登录已失效,请重新登录' }); } } function roleRequired(roles) { return (req, res, next) => { if (!req.user) return res.status(401).json({ message: '未登录' }); if (!roles.includes(req.user.role)) return res.status(403).json({ message: '无权限' }); next(); }; } // ===== date helpers ===== function parseDateValue(value) { if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return d; } function isExpired(expireDate) { const d = parseDateValue(expireDate); if (!d) return false; return d.getTime() < Date.now(); } function isExpiringSoon(expireDate, days = 7) { const d = parseDateValue(expireDate); if (!d) return false; const diff = d.getTime() - Date.now(); return diff >= 0 && diff <= days * 24 * 60 * 60 * 1000; } // ===== Common: build client status (for response) ===== function getClientStatus(expireDate, days = 7) { if (isExpired(expireDate)) return 'expired'; if (isExpiringSoon(expireDate, days)) return 'expiring'; return 'active'; } // ===== API: Auth ===== app.post('/api/auth/login', asyncHandler(async (req, res) => { const { username, password } = req.body || {}; if (!username || !password) return res.status(400).json({ message: '缺少账号或密码' }); const user = await get('SELECT * FROM users WHERE username = ?', [username]); if (!user) return res.status(401).json({ message: '账号或密码错误' }); if (user.status === 'inactive') return res.status(403).json({ message: '该账号已被禁用,请联系管理员' }); const ok = await bcrypt.compare(password, user.password_hash); if (!ok) return res.status(401).json({ message: '账号或密码错误' }); const token = jwt.sign( { id: user.id, username: user.username, role: user.role, name: user.name }, JWT_SECRET, { expiresIn: '7d' } ); res.json({ token, user: { id: user.id, username: user.username, name: user.name, role: user.role } }); })); app.get('/api/auth/me', authRequired, asyncHandler(async (req, res) => { const u = await get( 'SELECT id, username, name, role, phone, email, status FROM users WHERE id=?', [req.user.id] ); res.json({ user: u }); })); // ====================================================================== // ✅ 新增:Dashboard Summary(总览卡片 + 员工统计) // - admin:返回全局 stats + staffStats 列表 // - staff:返回自己的 stats(不返回全部员工) // ====================================================================== app.get('/api/dashboard/summary', authRequired, asyncHandler(async (req, res) => { const days = Number(req.query.days || 7); // 取所有客户(admin) or 取自己的客户(staff) let rows; if (req.user.role === 'admin') { rows = await all(`SELECT staff_id, expireDate, amount FROM clients`); } else { rows = await all(`SELECT staff_id, expireDate, amount FROM clients WHERE staff_id = ?`, [req.user.id]); } let totalAmount = 0; let totalClients = rows.length; let expiringCount = 0; let expiredCount = 0; for (const r of rows) { totalAmount += Number(r.amount || 0); if (isExpired(r.expireDate)) expiredCount += 1; else if (isExpiringSoon(r.expireDate, days)) expiringCount += 1; } const stats = { totalAmount, totalClients, expiringCount, expiredCount }; if (req.user.role !== 'admin') { return res.json({ stats }); } // admin:补员工统计 const staffRows = await all(` SELECT u.id, u.username, u.name, u.phone, u.email, u.status, u.created_at, u.creator_id, c.name as creator_name, c.username as creator_username FROM users u LEFT JOIN users c ON u.creator_id = c.id WHERE u.role='staff' ORDER BY u.id ASC `); const map = new Map(); staffRows.forEach(s => { map.set(s.id, { id: s.id, username: s.username, name: s.name, phone: s.phone, email: s.email, status: s.status, created_at: s.created_at, creator_id: s.creator_id, creator_name: s.creator_name || s.creator_username || 'Admin', clients: 0, total: 0, expired: 0, expiring: 0 }); }); rows.forEach(c => { const t = map.get(c.staff_id); if (!t) return; t.clients += 1; t.total += Number(c.amount || 0); if (isExpired(c.expireDate)) t.expired += 1; else if (isExpiringSoon(c.expireDate, days)) t.expiring += 1; }); res.json({ stats, staffStats: Array.from(map.values()) }); })); // ====================================================================== // ✅ 新增:Staff 通用接口(更贴近前端) // - GET /api/staff admin:员工列表(含聚合) leader:员工列表(不含聚合) staff:返回自己 // - POST /api/staff admin/leader:新增员工 // - PUT /api/staff/:id admin/leader:编辑员工 // - DELETE /api/staff/:id admin/leader:禁用员工 // ====================================================================== app.get('/api/staff', authRequired, asyncHandler(async (req, res) => { // staff 只能看自己 if (req.user.role === 'staff') { const me = await get(` SELECT id, username, name, phone, email, status, created_at FROM users WHERE id=? AND status != 'inactive' `, [req.user.id]); if (!me) { return res.json({ staff: [] }); } return res.json({ staff: [me] }); } // admin 和 leader 都可以查看员工列表 // admin 可以看到所有 staff 和 leader,leader 只能看自己创建的 staff const includeInactive = req.query.includeInactive === 'true'; const statusFilter = includeInactive ? '' : "AND u.status != 'inactive'"; let staffRows; if (req.user.role === 'admin') { staffRows = await all(` SELECT u.id, u.username, u.name, u.phone, u.email, u.status, u.role, u.creator_id, u.created_at FROM users u WHERE u.role IN ('staff', 'leader') ${statusFilter} ORDER BY u.status ASC, u.id ASC `); } else { // leader 只能看自己创建的 staff staffRows = await all(` SELECT u.id, u.username, u.name, u.phone, u.email, u.status, u.role, u.creator_id, u.created_at FROM users u WHERE u.role = 'staff' AND u.creator_id = ? ${statusFilter} ORDER BY u.status ASC, u.id ASC `, [req.user.id]); } // leader 也返回员工的客户统计数据 if (req.user.role === 'leader') { const staffIds = staffRows.map(s => s.id); if (staffIds.length === 0) { return res.json({ staff: [] }); } const clientRows = await all(`SELECT staff_id, expireDate, amount FROM clients WHERE staff_id IN (${staffIds.join(',')})`); const byStaff = new Map(); staffRows.forEach((s) => { byStaff.set(s.id, { ...s, clientsCount: 0, totalAmount: 0, expiredCount: 0, expiringCount: 0 }); }); const now = new Date(); const expireThreshold = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); clientRows.forEach((c) => { const staff = byStaff.get(c.staff_id); if (!staff) return; staff.clientsCount++; staff.totalAmount += c.amount || 0; if (c.expireDate) { const exp = new Date(c.expireDate); if (exp < now) staff.expiredCount++; else if (exp <= expireThreshold) staff.expiringCount++; } }); return res.json({ staff: Array.from(byStaff.values()) }); } // admin 返回完整统计数据 const clientRows = await all('SELECT staff_id, expireDate, amount FROM clients'); const byStaff = new Map(); staffRows.forEach((s) => { byStaff.set(s.id, { ...s, clientsCount: 0, totalAmount: 0, expiredCount: 0, expiringCount: 0 }); }); clientRows.forEach((c) => { const target = byStaff.get(c.staff_id); if (!target) return; target.clientsCount += 1; target.totalAmount += Number(c.amount || 0); if (isExpired(c.expireDate)) target.expiredCount += 1; if (isExpiringSoon(c.expireDate)) target.expiringCount += 1; }); res.json({ staff: Array.from(byStaff.values()) }); })); app.post('/api/staff', authRequired, roleRequired(['admin', 'leader']), asyncHandler(async (req, res) => { const b = req.body || {}; if (!b.username || !b.password || !b.name) { return res.status(400).json({ message: '用户名、姓名和密码不能为空' }); } const existing = await get('SELECT id FROM users WHERE username = ?', [b.username]); if (existing) return res.status(400).json({ message: '用户名已存在' }); // 只有 admin 可以创建 leader,leader 只能创建 staff let role = 'staff'; if (b.role === 'leader' && req.user.role === 'admin') { role = 'leader'; } // 记录创建者 ID const creatorId = req.user.id; const hash = await bcrypt.hash(b.password, 10); const r = await run( 'INSERT INTO users(username, name, password_hash, role, phone, email, status, creator_id) VALUES(?,?,?,?,?,?,?,?)', [b.username, b.name, hash, role, b.phone || null, b.email || null, b.status || 'active', creatorId] ); const row = await get('SELECT id, username, name, phone, email, status, role, creator_id, created_at FROM users WHERE id=?', [r.lastID]); res.json({ staff: row }); })); app.put('/api/staff/:id', authRequired, roleRequired(['admin', 'leader']), asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ message: '参数错误' }); const existing = await get('SELECT * FROM users WHERE id=?', [id]); if (!existing) return res.status(404).json({ message: '用户不存在' }); // 权限检查 if (existing.role === 'admin') return res.status(403).json({ message: '不能操作管理员账号' }); if (existing.role === 'leader' && req.user.role !== 'admin') return res.status(403).json({ message: '无权操作组长' }); // 组长只能操作自己创建的员工 if (req.user.role === 'leader' && existing.creator_id !== req.user.id) { return res.status(403).json({ message: '无权操作此员工' }); } const b = req.body || {}; const updates = []; const params = []; const fields = ['name', 'phone', 'email', 'username']; // 允许修改用户名 for (const f of fields) { if (Object.prototype.hasOwnProperty.call(b, f)) { // 如果修改用户名,检查唯一性 if (f === 'username' && b.username !== existing.username) { const uCheck = await get('SELECT id FROM users WHERE username = ?', [b.username]); if (uCheck) return res.status(400).json({ message: '用户名已存在' }); } updates.push(`${f} = ?`); params.push(b[f]); } } // 只有 admin 可以修改角色 if (req.user.role === 'admin' && b.role && ['leader', 'staff'].includes(b.role)) { updates.push('role = ?'); params.push(b.role); } // 只有 admin 可以修改所属组长 (creator_id) // 传入 id 为 leader id,如果为 'admin' 或空则设为当前 admin id if (req.user.role === 'admin' && Object.prototype.hasOwnProperty.call(b, 'creator_id')) { let newCreatorId = Number(b.creator_id); if (!newCreatorId) newCreatorId = req.user.id; // 默认为当前操作者(admin) // 验证一下是否存在且是 leader/admin if (newCreatorId !== req.user.id) { const leaderCheck = await get('SELECT id, role FROM users WHERE id=?', [newCreatorId]); if (!leaderCheck) return res.status(400).json({ message: '指定的组长不存在' }); } updates.push('creator_id = ?'); params.push(newCreatorId); } if (b.password) { const hash = await bcrypt.hash(b.password, 10); updates.push('password_hash = ?'); params.push(hash); } if (updates.length > 0) { params.push(id); await run(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, params); } const row = await get('SELECT id, username, name, phone, email, status, role, creator_id, created_at FROM users WHERE id=?', [id]); res.json({ staff: row }); })); app.post('/api/staff/:id/reset_password', authRequired, roleRequired(['admin', 'leader']), asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ message: '参数错误' }); const b = req.body || {}; if (!b.password) return res.status(400).json({ message: '新密码不能为空' }); const existing = await get('SELECT id, role, creator_id FROM users WHERE id=?', [id]); if (!existing) return res.status(404).json({ message: '用户不存在' }); if (existing.role === 'admin') return res.status(403).json({ message: '不能操作管理员账号' }); if (existing.role === 'leader' && req.user.role !== 'admin') return res.status(403).json({ message: '无权操作组长' }); // 组长只能操作自己创建的员工 if (req.user.role === 'leader' && existing.creator_id !== req.user.id) { return res.status(403).json({ message: '无权操作此员工' }); } const hash = await bcrypt.hash(b.password, 10); await run('UPDATE users SET password_hash = ? WHERE id = ?', [hash, id]); res.json({ success: true }); })); app.delete('/api/staff/:id', authRequired, roleRequired(['admin', 'leader']), asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ message: '参数错误' }); const existing = await get('SELECT * FROM users WHERE id=?', [id]); if (!existing) return res.status(404).json({ message: '用户不存在' }); if (existing.role === 'admin') return res.status(403).json({ message: '不能操作管理员账号' }); if (existing.role === 'leader' && req.user.role !== 'admin') return res.status(403).json({ message: '无权操作组长' }); // 组长只能操作自己创建的员工 if (req.user.role === 'leader' && existing.creator_id !== req.user.id) { return res.status(403).json({ message: '无权操作此员工' }); } await run('UPDATE users SET status = \'inactive\' WHERE id = ?', [id]); res.json({ success: true }); })); // 启用/禁用用户状态切换 app.post('/api/staff/:id/toggle_status', authRequired, roleRequired(['admin', 'leader']), asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ message: '参数错误' }); const existing = await get('SELECT * FROM users WHERE id=?', [id]); if (!existing) return res.status(404).json({ message: '用户不存在' }); if (existing.role === 'admin') return res.status(403).json({ message: '不能操作管理员账号' }); if (existing.role === 'leader' && req.user.role !== 'admin') return res.status(403).json({ message: '无权操作组长' }); // 组长只能操作自己创建的员工 if (req.user.role === 'leader' && existing.creator_id !== req.user.id) { return res.status(403).json({ message: '无权操作此员工' }); } const newStatus = existing.status === 'active' ? 'inactive' : 'active'; await run('UPDATE users SET status = ? WHERE id = ?', [newStatus, id]); res.json({ success: true, status: newStatus }); })); // ====================================================================== // ✅ 改造:Clients 列表支持筛选/搜索/分页(你前端筛选栏 & 客户页需要) // GET /api/clients // query: // staffId / staffName(可选) / status(active|expiring|expired) / serviceType / q // dateFrom / dateTo (按 regDate) / page / pageSize / days(默认7,expiring阈值) // ====================================================================== app.get('/api/clients', authRequired, asyncHandler(async (req, res) => { const { staffId, status = 'all', serviceType = 'all', q = '', dateFrom, dateTo, page = 1, pageSize = 20, days = 7 } = req.query; const p = Math.max(1, Number(page || 1)); const ps = Math.min(200, Math.max(1, Number(pageSize || 20))); const expDays = Number(days || 7); const where = []; const params = []; // 权限限制 if (req.user.role === 'staff') { // 员工只能看自己的客户 where.push('c.staff_id = ?'); params.push(req.user.id); } else if (req.user.role === 'leader') { // 组长只能看自己创建的员工的客户 if (staffId && staffId !== 'all') { // 验证该员工是否属于此组长 const staffUser = await get('SELECT id, creator_id FROM users WHERE id = ?', [Number(staffId)]); if (!staffUser || staffUser.creator_id !== req.user.id) { return res.status(403).json({ message: '无权查看此员工客户' }); } where.push('c.staff_id = ?'); params.push(Number(staffId)); } else { // 获取此组长创建的所有员工 ID const ownStaff = await all('SELECT id FROM users WHERE creator_id = ? AND role = ?', [req.user.id, 'staff']); const staffIds = ownStaff.map(s => s.id); if (staffIds.length === 0) { return res.json({ clients: [], pagination: { page: 1, pageSize: ps, total: 0, totalPages: 0 } }); } where.push(`c.staff_id IN (${staffIds.join(',')})`); } } else if (staffId && staffId !== 'all') { // admin 可以看任何员工 where.push('c.staff_id = ?'); params.push(Number(staffId)); } if (serviceType && serviceType !== 'all') { where.push('c.serviceType = ?'); params.push(serviceType); } if (q && String(q).trim()) { const like = `%${String(q).trim()}%`; where.push('(c.customerName LIKE ? OR c.phone LIKE ? OR c.remark LIKE ?)'); params.push(like, like, like); } // regDate 范围筛选 if (dateFrom) { where.push('date(c.regDate) >= date(?)'); params.push(dateFrom); } if (dateTo) { where.push('date(c.regDate) <= date(?)'); params.push(dateTo); } // 状态筛选(expireDate) // 注意:expireDate 允许为空,空则默认当 active if (status === 'expired') { where.push("c.expireDate IS NOT NULL AND date(c.expireDate) < date('now')"); } else if (status === 'expiring') { where.push(`c.expireDate IS NOT NULL AND date(c.expireDate) >= date('now') AND date(c.expireDate) <= date('now', ?)`); params.push(`+${expDays} day`); } else if (status === 'active') { // active: expireDate 为空 或 expireDate > now+days 或 expireDate 在未来但>days // 简化:not expired and not expiring where.push(`( c.expireDate IS NULL OR date(c.expireDate) > date('now', ?) )`); params.push(`+${expDays} day`); } const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : ''; // 总数 const totalRow = await get(`SELECT COUNT(1) AS cnt FROM clients c ${whereSql}`, params); const total = totalRow ? totalRow.cnt : 0; // 分页数据 const offset = (p - 1) * ps; const list = await all( ` SELECT c.*, u.name AS staff_name, u.username AS staff_username FROM clients c LEFT JOIN users u ON u.id = c.staff_id ${whereSql} ORDER BY c.id DESC LIMIT ? OFFSET ? `, [...params, ps, offset] ); // 补一个计算出来的 status 字段,前端更好用 const clients = list.map(row => ({ ...row, status: getClientStatus(row.expireDate, expDays) })); res.json({ page: p, pageSize: ps, total, clients }); })); // ====================================================================== // Staff clients endpoints (for staff page) // ====================================================================== app.get('/api/staff/clients', authRequired, roleRequired(['staff', 'admin']), asyncHandler(async (req, res) => { const days = Number(req.query.days || 7); const rows = await all( `SELECT c.* FROM clients c WHERE c.staff_id = ? ORDER BY c.id DESC`, [req.user.id] ); const clients = rows.map(row => ({ ...row, status: getClientStatus(row.expireDate, days) })); res.json({ clients }); })); app.post('/api/staff/clients', authRequired, roleRequired(['staff', 'admin']), asyncHandler(async (req, res) => { const b = req.body || {}; if (!b.customerName) return res.status(400).json({ message: '客户姓名不能为空' }); const amount = Number(b.amount || 0); const now = new Date().toISOString(); const r = await run(` INSERT INTO clients(staff_id, regDate, customerName, phone, expireDate, amount, serviceType, remark, lastRenewAt, created_at, updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?) `, [ req.user.id, b.regDate || null, b.customerName, b.phone || null, b.expireDate || null, amount, b.serviceType || null, b.remark || '', b.lastRenewAt || null, now, now ]); const row = await get('SELECT * FROM clients WHERE id=?', [r.lastID]); res.json({ client: { ...row, status: getClientStatus(row.expireDate) } }); })); app.put('/api/staff/clients/:id', authRequired, roleRequired(['staff', 'admin']), asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ message: '参数错误' }); const existing = await get('SELECT * FROM clients WHERE id=?', [id]); if (!existing) return res.status(404).json({ message: '客户不存在' }); if (req.user.role !== 'admin' && existing.staff_id !== req.user.id) { return res.status(403).json({ message: '无权限修改该客户' }); } const b = req.body || {}; const fields = ['regDate', 'customerName', 'phone', 'expireDate', 'amount', 'serviceType', 'remark', 'lastRenewAt']; const updates = []; const params = []; for (const f of fields) { if (!Object.prototype.hasOwnProperty.call(b, f)) continue; updates.push(`${f} = ?`); if (f === 'amount') params.push(Number(b[f] || 0)); else params.push(b[f]); } if (updates.length === 0) { const row = await get('SELECT * FROM clients WHERE id=?', [id]); return res.json({ client: { ...row, status: getClientStatus(row.expireDate) } }); } updates.push('updated_at = ?'); params.push(new Date().toISOString()); params.push(id); await run(`UPDATE clients SET ${updates.join(', ')} WHERE id = ?`, params); const row = await get('SELECT * FROM clients WHERE id=?', [id]); res.json({ client: { ...row, status: getClientStatus(row.expireDate) } }); })); // New client app.post('/api/clients', authRequired, roleRequired(['staff', 'admin']), asyncHandler(async (req, res) => { const b = req.body || {}; const staffId = (req.user.role === 'admin' && b.staff_id) ? Number(b.staff_id) : req.user.id; if (!b.customerName) return res.status(400).json({ message: '客户姓名不能为空' }); const amount = Number(b.amount || 0); const now = new Date().toISOString(); const r = await run(` INSERT INTO clients(staff_id, regDate, customerName, phone, expireDate, amount, serviceType, remark, lastRenewAt, created_at, updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?) `, [ staffId, b.regDate || null, b.customerName, b.phone || null, b.expireDate || null, amount, b.serviceType || null, b.remark || '', b.lastRenewAt || null, now, now ]); const row = await get('SELECT * FROM clients WHERE id=?', [r.lastID]); res.json({ client: { ...row, status: getClientStatus(row.expireDate) } }); })); // 更新客户 app.put('/api/clients/:id', authRequired, asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ message: '参数错误' }); const existing = await get('SELECT * FROM clients WHERE id=?', [id]); if (!existing) return res.status(404).json({ message: '客户不存在' }); if (req.user.role !== 'admin' && existing.staff_id !== req.user.id) { return res.status(403).json({ message: '无权限修改该客户' }); } const b = req.body || {}; const fields = ['regDate', 'customerName', 'phone', 'expireDate', 'amount', 'serviceType', 'remark', 'lastRenewAt', 'staff_id']; const updates = []; const params = []; for (const f of fields) { if (!Object.prototype.hasOwnProperty.call(b, f)) continue; // staff_id 只有 admin 能改 if (f === 'staff_id' && req.user.role !== 'admin') continue; updates.push(`${f} = ?`); if (f === 'amount') params.push(Number(b[f] || 0)); else if (f === 'staff_id') params.push(Number(b[f])); else params.push(b[f]); } if (updates.length === 0) return res.json({ client: { ...existing, status: getClientStatus(existing.expireDate) } }); updates.push('updated_at = ?'); params.push(new Date().toISOString()); params.push(id); await run(`UPDATE clients SET ${updates.join(', ')} WHERE id = ?`, params); const row = await get('SELECT * FROM clients WHERE id=?', [id]); res.json({ client: { ...row, status: getClientStatus(row.expireDate) } }); })); // ✅ 新增:删除客户(单个) app.delete('/api/clients/:id', authRequired, asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ message: '参数错误' }); const existing = await get('SELECT * FROM clients WHERE id=?', [id]); if (!existing) return res.status(404).json({ message: '客户不存在' }); if (req.user.role !== 'admin' && existing.staff_id !== req.user.id) { return res.status(403).json({ message: '无权限删除该客户' }); } await run('DELETE FROM clients WHERE id=?', [id]); res.json({ success: true }); })); // ====================================================================== // ✅ 新增:到期提醒列表(你左侧“到期提醒”页会用到) // GET /api/reminders?type=expiring|expired&days=7&staffId=xxx // ====================================================================== app.get('/api/reminders', authRequired, asyncHandler(async (req, res) => { const type = req.query.type || 'expiring'; const days = Number(req.query.days || 7); const staffId = req.query.staffId; const where = []; const params = []; if (req.user.role !== 'admin') { where.push('c.staff_id = ?'); params.push(req.user.id); } else if (staffId && staffId !== 'all') { where.push('c.staff_id = ?'); params.push(Number(staffId)); } if (type === 'expired') { where.push("c.expireDate IS NOT NULL AND date(c.expireDate) < date('now')"); } else { where.push(`c.expireDate IS NOT NULL AND date(c.expireDate) >= date('now') AND date(c.expireDate) <= date('now', ?)`); params.push(`+${days} day`); } const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : ''; const rows = await all(` SELECT c.*, u.name AS staff_name, u.username AS staff_username FROM clients c LEFT JOIN users u ON u.id = c.staff_id ${whereSql} ORDER BY date(c.expireDate) ASC, c.id DESC `, params); res.json({ type, days, clients: rows.map(r => ({ ...r, status: getClientStatus(r.expireDate, days) })) }); })); // ====================================================================== // ✅ 新增:导出客户 CSV(简单实用,Excel 也能打开) // GET /api/export/clients?...(同 /api/clients 的筛选参数) // ====================================================================== app.get('/api/export/clients', authRequired, asyncHandler(async (req, res) => { // 复用 /api/clients 的筛选逻辑,但这里直接查全部,不分页 const { staffId, status = 'all', serviceType = 'all', q = '', dateFrom, dateTo, days = 7 } = req.query; const expDays = Number(days || 7); const where = []; const params = []; if (req.user.role !== 'admin') { where.push('c.staff_id = ?'); params.push(req.user.id); } else if (staffId && staffId !== 'all') { where.push('c.staff_id = ?'); params.push(Number(staffId)); } if (serviceType && serviceType !== 'all') { where.push('c.serviceType = ?'); params.push(serviceType); } if (q && String(q).trim()) { const like = `%${String(q).trim()}%`; where.push('(c.customerName LIKE ? OR c.phone LIKE ? OR c.remark LIKE ?)'); params.push(like, like, like); } if (dateFrom) { where.push('date(c.regDate) >= date(?)'); params.push(dateFrom); } if (dateTo) { where.push('date(c.regDate) <= date(?)'); params.push(dateTo); } if (status === 'expired') { where.push("c.expireDate IS NOT NULL AND date(c.expireDate) < date('now')"); } else if (status === 'expiring') { where.push(`c.expireDate IS NOT NULL AND date(c.expireDate) >= date('now') AND date(c.expireDate) <= date('now', ?)`); params.push(`+${expDays} day`); } else if (status === 'active') { where.push(`( c.expireDate IS NULL OR date(c.expireDate) > date('now', ?) )`); params.push(`+${expDays} day`); } const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : ''; const rows = await all(` SELECT c.*, u.name AS staff_name, u.username AS staff_username FROM clients c LEFT JOIN users u ON u.id = c.staff_id ${whereSql} ORDER BY c.id DESC `, params); // CSV const headers = [ 'id', 'staff_name', 'staff_username', 'customerName', 'phone', 'serviceType', 'regDate', 'expireDate', 'amount', 'status', 'remark' ]; const escapeCsv = (v) => { if (v === null || v === undefined) return ''; const s = String(v); if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`; return s; }; const lines = []; lines.push(headers.join(',')); for (const r of rows) { const statusText = getClientStatus(r.expireDate, expDays); const line = [ r.id, r.staff_name || '', r.staff_username || '', r.customerName || '', r.phone || '', r.serviceType || '', r.regDate || '', r.expireDate || '', r.amount ?? 0, statusText, r.remark || '' ].map(escapeCsv).join(','); lines.push(line); } const csv = '\ufeff' + lines.join('\n'); // 带 BOM,Excel 打开中文不乱码 res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', `attachment; filename="clients_${Date.now()}.csv"`); res.send(csv); })); // ====================================================================== // 兼容保留:你原来的 admin 接口(不想改前端也能用) // ====================================================================== app.get('/api/admin/staff', authRequired, roleRequired(['admin']), asyncHandler(async (req, res) => { // 直接代理到 /api/staff req.user.role = 'admin'; return app._router.handle(req, res, () => { }, 'get', '/api/staff'); })); app.post('/api/admin/staff', authRequired, roleRequired(['admin']), asyncHandler(async (req, res) => { return app._router.handle(req, res, () => { }, 'post', '/api/staff'); })); app.put('/api/admin/staff/:id', authRequired, roleRequired(['admin']), asyncHandler(async (req, res) => { return app._router.handle(req, res, () => { }, 'put', `/api/staff/${req.params.id}`); })); app.delete('/api/admin/staff/:id', authRequired, roleRequired(['admin']), asyncHandler(async (req, res) => { return app._router.handle(req, res, () => { }, 'delete', `/api/staff/${req.params.id}`); })); app.get('/api/admin/stats', authRequired, roleRequired(['admin']), asyncHandler(async (req, res) => { // 兼容:用新 summary 的 stats const fakeReq = { ...req, query: { ...req.query } }; // 直接再算一次(避免 hack router) const rows = await all('SELECT expireDate, amount FROM clients'); let totalAmount = 0; let totalClients = rows.length; let activeClients = 0; let expiringSoonClients = 0; for (const r of rows) { totalAmount += Number(r.amount || 0); if (!isExpired(r.expireDate)) activeClients += 1; if (isExpiringSoon(r.expireDate)) expiringSoonClients += 1; } res.json({ stats: { totalAmount, totalClients, activeClients, expiringSoonClients } }); })); // ===== 静态前端文件 ===== const publicDir = path.join(__dirname, 'public'); app.use(express.static(publicDir)); app.get('/', (req, res) => res.redirect('/index.html')); // ===== error handler ===== app.use((err, req, res, next) => { console.error(err); res.status(500).json({ message: '服务器错误' }); }); async function main() { if (process.argv.includes('--initdb')) { await initDb(); process.exit(0); } await initDb(); app.listen(PORT, () => { console.log(`CRM running on http://0.0.0.0:${PORT}`); }); } main().catch(err => { console.error(err); process.exit(1); });