Initial commit: gamer project
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
|
||||
const db = require('./db');
|
||||
const { router: authRouter, auth } = require('./routes/auth');
|
||||
const statsRouter = require('./routes/stats');
|
||||
const recordsRouter = require('./routes/records');
|
||||
const ioRouter = require('./routes/io');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||
|
||||
// 联系人自动补全
|
||||
app.get('/api/contacts', auth, (req, res) => {
|
||||
const field = req.query.field;
|
||||
const allowed = ['boss', 'partner', 'source', 'destination'];
|
||||
if (!field || !allowed.includes(field)) return res.status(400).json({ error: '无效字段' });
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT ${field} as name, MAX(created_at) as last_used, COUNT(*) as freq
|
||||
FROM records WHERE user_id = ? AND ${field} IS NOT NULL AND ${field} != ''
|
||||
GROUP BY ${field} ORDER BY MAX(created_at) DESC
|
||||
`).all(req.user.id);
|
||||
res.json(rows.map(r => r.name));
|
||||
});
|
||||
|
||||
// 路由挂载
|
||||
app.use('/api', authRouter);
|
||||
app.use('/api/stats', auth, statsRouter);
|
||||
app.use('/api/records', auth, recordsRouter);
|
||||
app.use('/api', auth, ioRouter);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`服务器运行在 http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
|
||||
const db = new Database(path.join(__dirname, '..', 'data.db'));
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
`);
|
||||
|
||||
// 迁移:去掉 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;
|
||||
`);
|
||||
}
|
||||
} 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");
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
module.exports = db;
|
||||
@@ -0,0 +1,32 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const db = require('../db');
|
||||
|
||||
const router = express.Router();
|
||||
const SECRET = 'gamer-order-secret-2024';
|
||||
|
||||
// JWT 中间件
|
||||
function auth(req, res, next) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: '未登录' });
|
||||
try {
|
||||
req.user = jwt.verify(token, SECRET);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: '登录已过期' });
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
router.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||
if (!user || !bcrypt.compareSync(password, user.password)) {
|
||||
return res.status(401).json({ error: '用户名或密码错误' });
|
||||
}
|
||||
const token = jwt.sign({ id: user.id, username: user.username }, SECRET, { expiresIn: '7d' });
|
||||
res.json({ token, username: user.username });
|
||||
});
|
||||
|
||||
module.exports = { router, auth };
|
||||
@@ -0,0 +1,136 @@
|
||||
const express = require('express');
|
||||
const db = require('../db');
|
||||
const { parseCSVLine } = require('../utils/helpers');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 导出数据
|
||||
router.get('/export', (req, res) => {
|
||||
const format = req.query.format;
|
||||
if (!format || !['json', 'csv'].includes(format)) {
|
||||
return res.status(400).json({ error: '请指定格式: json 或 csv' });
|
||||
}
|
||||
|
||||
const records = db.prepare(
|
||||
'SELECT type, category, amount, quantity, unit_price, rebate, boss, partner, source, destination, note, created_at FROM records WHERE user_id = ? ORDER BY created_at DESC'
|
||||
).all(req.user.id);
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const filename = `gamer-export-${timestamp}`;
|
||||
|
||||
if (format === 'json') {
|
||||
const exportData = {
|
||||
exportVersion: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
username: req.user.username,
|
||||
recordCount: records.length,
|
||||
records
|
||||
};
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}.json"`);
|
||||
return res.send(JSON.stringify(exportData, null, 2));
|
||||
}
|
||||
|
||||
// CSV with UTF-8 BOM
|
||||
const BOM = '\uFEFF';
|
||||
const cols = ['type', 'category', 'amount', 'quantity', 'unit_price', 'rebate', 'boss', 'partner', 'source', 'destination', 'note', 'created_at'];
|
||||
const csvRows = records.map(r => {
|
||||
return cols.map(col => {
|
||||
const val = r[col];
|
||||
if (val === null || val === undefined) return '';
|
||||
const str = String(val);
|
||||
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
||||
return '"' + str.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return str;
|
||||
}).join(',');
|
||||
});
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}.csv"`);
|
||||
res.send(BOM + cols.join(',') + '\n' + csvRows.join('\n'));
|
||||
});
|
||||
|
||||
// 导入数据
|
||||
router.post('/import', express.text({ type: '*/*', limit: '10mb' }), (req, res) => {
|
||||
try {
|
||||
const raw = req.body;
|
||||
if (!raw || typeof raw !== 'string') {
|
||||
return res.status(400).json({ error: '未收到文件内容' });
|
||||
}
|
||||
|
||||
let records = [];
|
||||
const requiredFields = ['type', 'category', 'amount', 'created_at'];
|
||||
const allFields = ['type', 'category', 'amount', 'quantity', 'unit_price', 'rebate', 'boss', 'partner', 'source', 'destination', 'note', 'created_at'];
|
||||
const trimmed = raw.replace(/^\uFEFF/, '').trim();
|
||||
|
||||
if (trimmed.startsWith('{')) {
|
||||
const data = JSON.parse(trimmed);
|
||||
if (!data.exportVersion || !Array.isArray(data.records)) {
|
||||
return res.status(400).json({ error: '无效的JSON导出文件格式' });
|
||||
}
|
||||
records = data.records;
|
||||
} else {
|
||||
const lines = trimmed.split('\n').map(l => l.replace(/\r$/, ''));
|
||||
if (lines.length < 2) {
|
||||
return res.status(400).json({ error: 'CSV文件为空或格式不正确' });
|
||||
}
|
||||
if (lines[0] !== allFields.join(',')) {
|
||||
return res.status(400).json({ error: 'CSV列头不匹配,请使用本系统导出的文件' });
|
||||
}
|
||||
const csvHeaders = lines[0].split(',');
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (!lines[i].trim()) continue;
|
||||
const values = parseCSVLine(lines[i]);
|
||||
const record = {};
|
||||
csvHeaders.forEach((h, idx) => { record[h] = values[idx] || null; });
|
||||
if (record.amount) record.amount = parseFloat(record.amount);
|
||||
if (record.quantity) record.quantity = parseInt(record.quantity) || null;
|
||||
if (record.unit_price) record.unit_price = parseFloat(record.unit_price) || null;
|
||||
if (record.rebate) record.rebate = parseFloat(record.rebate) || null;
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of records) {
|
||||
for (const f of requiredFields) {
|
||||
if (!r[f] && r[f] !== 0) {
|
||||
return res.status(400).json({ error: `记录缺少必填字段: ${f}` });
|
||||
}
|
||||
}
|
||||
if (!['income', 'expense'].includes(r.type)) {
|
||||
return res.status(400).json({ error: `无效的类型: ${r.type}` });
|
||||
}
|
||||
}
|
||||
|
||||
const existingCheck = db.prepare(
|
||||
'SELECT id FROM records WHERE user_id = ? AND created_at = ? AND category = ? AND amount = ?'
|
||||
);
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT INTO records (user_id, type, category, amount, quantity, unit_price, rebate, boss, partner, source, destination, note, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
let imported = 0, skipped = 0;
|
||||
const insertMany = db.transaction((recs) => {
|
||||
for (const r of recs) {
|
||||
if (existingCheck.get(req.user.id, r.created_at, r.category, r.amount)) {
|
||||
skipped++; continue;
|
||||
}
|
||||
insertStmt.run(req.user.id, r.type, r.category, r.amount,
|
||||
r.quantity || null, r.unit_price || null, r.rebate || null,
|
||||
r.boss || null, r.partner || null,
|
||||
r.source || null, r.destination || null,
|
||||
r.note || null, r.created_at);
|
||||
imported++;
|
||||
}
|
||||
});
|
||||
insertMany(records);
|
||||
|
||||
res.json({ imported, skipped, total: records.length });
|
||||
} catch (e) {
|
||||
console.error('Import error:', e);
|
||||
res.status(400).json({ error: '文件解析失败: ' + e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,134 @@
|
||||
const express = require('express');
|
||||
const db = require('../db');
|
||||
const { getMonthRange } = require('../utils/helpers');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 新增记录
|
||||
router.post('/', (req, res) => {
|
||||
const { type, category, amount, quantity, unit_price, rebate, boss, partner, source, destination, note, created_at } = req.body;
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO records (user_id, type, category, amount, quantity, unit_price, rebate, boss, partner, source, destination, note, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
const result = stmt.run(req.user.id, type, category, amount, quantity || null, unit_price || null, rebate || null, boss || null, partner || null, source || null, destination || null, note || null, created_at || new Date().toISOString());
|
||||
res.json({ id: result.lastInsertRowid });
|
||||
});
|
||||
|
||||
// 接单列表
|
||||
router.get('/orders', (req, res) => {
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
const offset = (Number(page) - 1) * Number(limit);
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
|
||||
const total = db.prepare(
|
||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND category = '接单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
||||
).get(req.user.id, startDate, endDate).count;
|
||||
|
||||
const records = db.prepare(
|
||||
`SELECT * FROM records WHERE user_id = ? AND category = '接单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
||||
|
||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
||||
});
|
||||
|
||||
// 红包及其他收入列表
|
||||
router.get('/other-income', (req, res) => {
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
const offset = (Number(page) - 1) * Number(limit);
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
|
||||
const total = db.prepare(
|
||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND category != '接单' AND category != '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
||||
).get(req.user.id, startDate, endDate).count;
|
||||
|
||||
const records = db.prepare(
|
||||
`SELECT * FROM records WHERE user_id = ? AND type = 'income' AND category != '接单' AND category != '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
||||
|
||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
||||
});
|
||||
|
||||
// 红包收入列表
|
||||
router.get('/red-envelope', (req, res) => {
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
const offset = (Number(page) - 1) * Number(limit);
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
|
||||
const total = db.prepare(
|
||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND category = '红包收入' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
||||
).get(req.user.id, startDate, endDate).count;
|
||||
|
||||
const records = db.prepare(
|
||||
`SELECT * FROM records WHERE user_id = ? AND type = 'income' AND category = '红包收入' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
||||
|
||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
||||
});
|
||||
|
||||
// 奶茶收入列表
|
||||
router.get('/milk-tea', (req, res) => {
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
const offset = (Number(page) - 1) * Number(limit);
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
|
||||
const total = db.prepare(
|
||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND category = '奶茶' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
||||
).get(req.user.id, startDate, endDate).count;
|
||||
|
||||
const records = db.prepare(
|
||||
`SELECT * FROM records WHERE user_id = ? AND type = 'income' AND category = '奶茶' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
||||
|
||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
||||
});
|
||||
|
||||
// 派单列表
|
||||
router.get('/dispatches', (req, res) => {
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
const offset = (Number(page) - 1) * Number(limit);
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
|
||||
const total = db.prepare(
|
||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND category = '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
||||
).get(req.user.id, startDate, endDate).count;
|
||||
|
||||
const records = db.prepare(
|
||||
`SELECT * FROM records WHERE user_id = ? AND category = '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
||||
|
||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
||||
});
|
||||
|
||||
// 获取记录列表
|
||||
router.get('/', (req, res) => {
|
||||
const { type, page = 1, limit = 20 } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
let sql = 'SELECT * FROM records WHERE user_id = ?';
|
||||
const params = [req.user.id];
|
||||
if (type) {
|
||||
sql += ' AND type = ?';
|
||||
params.push(type);
|
||||
}
|
||||
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(Number(limit), Number(offset));
|
||||
res.json(db.prepare(sql).all(...params));
|
||||
});
|
||||
|
||||
// 更新记录
|
||||
router.put('/:id', (req, res) => {
|
||||
const { quantity, unit_price, rebate, amount, boss, partner, source, destination, note, created_at } = req.body;
|
||||
db.prepare(`
|
||||
UPDATE records SET quantity = ?, unit_price = ?, rebate = ?, amount = ?, boss = ?, partner = ?, source = ?, destination = ?, note = ?, created_at = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
`).run(quantity || null, unit_price || null, rebate || null, amount, boss || null, partner || null, source || null, destination || null, note || null, created_at || null, req.params.id, req.user.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// 删除记录
|
||||
router.delete('/:id', (req, res) => {
|
||||
db.prepare('DELETE FROM records WHERE id = ? AND user_id = ?').run(req.params.id, req.user.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,423 @@
|
||||
const express = require('express');
|
||||
const db = require('../db');
|
||||
const { getMonthRange } = require('../utils/helpers');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 格式化本地日期为 YYYY-MM-DD
|
||||
function formatLocalDate(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
// 本月统计
|
||||
router.get('/monthly', (req, res) => {
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
const stats = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_order_count,
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as take_order_income,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_order_count,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as dispatch_order_income,
|
||||
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income,
|
||||
COALESCE(SUM(CASE WHEN category = '红包收入' THEN 1 ELSE 0 END), 0) as redEnvelope_count,
|
||||
COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as milkTea_income,
|
||||
COALESCE(SUM(CASE WHEN category = '奶茶' THEN 1 ELSE 0 END), 0) as milkTea_count,
|
||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
||||
FROM records
|
||||
WHERE user_id = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
`).get(req.user.id, startDate, endDate);
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
// 月收入/支出记录列表
|
||||
router.get('/monthly-records', (req, res) => {
|
||||
const { type } = req.query;
|
||||
if (!type || !['income', 'expense'].includes(type)) return res.status(400).json({ error: '缺少类型参数' });
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
|
||||
const summary = db.prepare(`
|
||||
SELECT COALESCE(SUM(amount), 0) as total
|
||||
FROM records WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
`).get(req.user.id, type, startDate, endDate);
|
||||
|
||||
const records = db.prepare(`
|
||||
SELECT * FROM records
|
||||
WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
ORDER BY created_at DESC
|
||||
`).all(req.user.id, type, startDate, endDate);
|
||||
|
||||
res.json({ total: summary.total, records });
|
||||
});
|
||||
|
||||
// 年度净收入按年统计
|
||||
router.get('/yearly-net-income', (req, res) => {
|
||||
// 获取所有年份数据
|
||||
const yearlyStats = db.prepare(`
|
||||
SELECT
|
||||
strftime('%Y', created_at) as year,
|
||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
||||
FROM records
|
||||
WHERE user_id = ?
|
||||
GROUP BY year ORDER BY year DESC
|
||||
`).all(req.user.id);
|
||||
|
||||
// 获取所有年份的每月数据
|
||||
const monthlyByYear = db.prepare(`
|
||||
SELECT
|
||||
strftime('%Y', created_at) as year,
|
||||
strftime('%m', created_at) as month,
|
||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
||||
FROM records
|
||||
WHERE user_id = ?
|
||||
GROUP BY year, month ORDER BY year DESC, month
|
||||
`).all(req.user.id);
|
||||
|
||||
// 按年份分组每月数据
|
||||
const monthlyData = {};
|
||||
monthlyByYear.forEach(m => {
|
||||
if (!monthlyData[m.year]) monthlyData[m.year] = [];
|
||||
monthlyData[m.year].push({
|
||||
month: m.month,
|
||||
total_income: m.total_income,
|
||||
total_expense: m.total_expense
|
||||
});
|
||||
});
|
||||
|
||||
const grandTotal = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
||||
FROM records WHERE user_id = ?
|
||||
`).get(req.user.id);
|
||||
|
||||
res.json({ yearlyStats, monthlyData, grandTotal });
|
||||
});
|
||||
|
||||
// 陪玩派单榜预览
|
||||
router.get('/partner-ranking/preview', (req, res) => {
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
const rows = db.prepare(`
|
||||
SELECT partner, COALESCE(SUM(quantity), 0) as dispatch_count, COALESCE(SUM(amount), 0) as dispatch_income
|
||||
FROM records
|
||||
WHERE user_id = ? AND category = '派单' AND partner IS NOT NULL AND partner != ''
|
||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
GROUP BY partner ORDER BY dispatch_count DESC LIMIT 10
|
||||
`).all(req.user.id, startDate, endDate);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// 陪玩派单榜详情
|
||||
router.get('/partner-ranking', (req, res) => {
|
||||
const { startDate, endDate } = req.query;
|
||||
let start, end;
|
||||
if (startDate && endDate) {
|
||||
start = startDate;
|
||||
end = endDate;
|
||||
} else {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
start = `${year}-${month}-01`;
|
||||
const endMonth = now.getMonth() + 2 > 12 ? 1 : now.getMonth() + 2;
|
||||
const endYear = endMonth === 1 ? year + 1 : year;
|
||||
end = `${endYear}-${String(endMonth).padStart(2, '0')}-01`;
|
||||
}
|
||||
const rows = db.prepare(`
|
||||
SELECT partner, COALESCE(SUM(quantity), 0) as dispatch_count, COALESCE(SUM(quantity * COALESCE(unit_price, 0)), 0) as dispatch_income, COALESCE(SUM(amount), 0) as rebate_income
|
||||
FROM records
|
||||
WHERE user_id = ? AND category = '派单' AND partner IS NOT NULL AND partner != ''
|
||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
GROUP BY partner ORDER BY dispatch_count DESC
|
||||
`).all(req.user.id, start, end);
|
||||
// 计算每个陪玩的陪玩总收入:(单价-返点)*单数 = 单价*单数 - 返点
|
||||
rows.forEach(row => {
|
||||
row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0);
|
||||
});
|
||||
const summary = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(quantity), 0) as total_dispatch_count,
|
||||
COALESCE(SUM(quantity * COALESCE(unit_price, 0)), 0) as total_dispatch_income,
|
||||
COALESCE(SUM(amount), 0) as total_rebate_income,
|
||||
COALESCE(SUM(quantity * COALESCE(unit_price, 0)), 0) - COALESCE(SUM(amount), 0) as total_partner_income
|
||||
FROM records
|
||||
WHERE user_id = ? AND category = '派单' AND partner IS NOT NULL AND partner != ''
|
||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
`).get(req.user.id, start, end);
|
||||
// 获取奶茶收入
|
||||
const milkTeaStats = db.prepare(`
|
||||
SELECT COALESCE(SUM(amount), 0) as total_milkTea_income
|
||||
FROM records
|
||||
WHERE user_id = ? AND type = 'income' AND category = '奶茶'
|
||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
`).get(req.user.id, start, end);
|
||||
summary.total_milkTea_income = milkTeaStats.total_milkTea_income;
|
||||
res.json({ rows, summary });
|
||||
});
|
||||
|
||||
// 总接单数按老板排行
|
||||
router.get('/take-ranking', (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT boss as name, COALESCE(SUM(quantity), 0) as count, COALESCE(SUM(amount), 0) as income
|
||||
FROM records WHERE user_id = ? AND category = '接单' AND boss IS NOT NULL AND boss != ''
|
||||
GROUP BY boss ORDER BY count DESC
|
||||
`).all(req.user.id);
|
||||
// 获取接单总收入和红包收入
|
||||
const stats = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(quantity), 0) as total_take_count,
|
||||
COALESCE(SUM(amount), 0) as total_take_income
|
||||
FROM records WHERE user_id = ? AND category = '接单'
|
||||
`).get(req.user.id);
|
||||
const redEnvelopeStats = db.prepare(`
|
||||
SELECT COALESCE(SUM(amount), 0) as total_redEnvelope_income
|
||||
FROM records WHERE user_id = ? AND type = 'income' AND category = '红包收入'
|
||||
`).get(req.user.id);
|
||||
const result = {
|
||||
rows: rows,
|
||||
total_take_count: stats.total_take_count,
|
||||
total_take_income: stats.total_take_income,
|
||||
total_redEnvelope_income: redEnvelopeStats.total_redEnvelope_income
|
||||
};
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// 总派单数按陪玩排行
|
||||
router.get('/dispatch-ranking', (req, res) => {
|
||||
const rows = db.prepare(`
|
||||
SELECT
|
||||
partner as name,
|
||||
COALESCE(SUM(quantity), 0) as dispatch_count,
|
||||
COALESCE(SUM(quantity * COALESCE(unit_price, 0)), 0) as dispatch_income,
|
||||
COALESCE(SUM(amount), 0) as rebate_income
|
||||
FROM records WHERE user_id = ? AND category = '派单' AND partner IS NOT NULL AND partner != ''
|
||||
GROUP BY partner ORDER BY dispatch_count DESC
|
||||
`).all(req.user.id);
|
||||
// 计算每个陪玩的陪玩总收入:(单价-返点)*单数 = 单价*单数 - 返点
|
||||
rows.forEach(row => {
|
||||
row.partner_income = (row.dispatch_income || 0) - (row.rebate_income || 0);
|
||||
});
|
||||
// 获取派单总数和奶茶收入
|
||||
const dispatchTotal = db.prepare(`
|
||||
SELECT COALESCE(SUM(quantity), 0) as total_dispatch_count
|
||||
FROM records WHERE user_id = ? AND category = '派单'
|
||||
`).get(req.user.id);
|
||||
const milkTeaStats = db.prepare(`
|
||||
SELECT COALESCE(SUM(amount), 0) as total_milkTea_income
|
||||
FROM records WHERE user_id = ? AND type = 'income' AND category = '奶茶'
|
||||
`).get(req.user.id);
|
||||
const result = {
|
||||
rows: rows,
|
||||
total_dispatch_count: dispatchTotal.total_dispatch_count,
|
||||
total_milkTea_income: milkTeaStats.total_milkTea_income
|
||||
};
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// 用户总统计
|
||||
router.get('/total', (req, res) => {
|
||||
const stats = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
||||
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as total_income,
|
||||
COALESCE(SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END), 0) as total_expense
|
||||
FROM records WHERE user_id = ?
|
||||
`).get(req.user.id);
|
||||
stats.net_income = stats.total_income - stats.total_expense;
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
// 老板交易记录
|
||||
router.get('/boss-records', (req, res) => {
|
||||
const boss = req.query.boss;
|
||||
if (!boss) return res.status(400).json({ error: '缺少老板参数' });
|
||||
|
||||
// 我的收入:只统计 type='income' 的记录(接单、派单返点、红包收入等)
|
||||
// 使用 COALESCE(boss, source) 兼容旧数据(之前的红包收入只有 source 字段)
|
||||
const myIncomeSummary = db.prepare(`
|
||||
SELECT COALESCE(SUM(amount), 0) as total
|
||||
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income'
|
||||
`).get(req.user.id, boss);
|
||||
|
||||
// 老板支出:派单支出 + 接单支出 + 红包收入(老板发的红包也算老板支出)
|
||||
const expenseSummary = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as dispatch_expense,
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as order_expense,
|
||||
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_expense
|
||||
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?
|
||||
`).get(req.user.id, boss);
|
||||
|
||||
const boss_total_expense = expenseSummary.dispatch_expense + expenseSummary.order_expense + expenseSummary.redEnvelope_expense;
|
||||
|
||||
const records = db.prepare(`
|
||||
SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? ORDER BY created_at DESC
|
||||
`).all(req.user.id, boss);
|
||||
|
||||
res.json({
|
||||
my_income: myIncomeSummary.total,
|
||||
boss_total_expense: boss_total_expense,
|
||||
records
|
||||
});
|
||||
});
|
||||
|
||||
// 陪玩交易记录
|
||||
router.get('/partner-records', (req, res) => {
|
||||
const partner = req.query.partner;
|
||||
if (!partner) return res.status(400).json({ error: '缺少陪玩参数' });
|
||||
|
||||
const dispatchStats = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(SUM(quantity), 0) as dispatch_count,
|
||||
COALESCE(SUM(quantity * (COALESCE(unit_price, 0) - COALESCE(rebate, amount * 1.0 / CASE WHEN quantity > 0 THEN quantity ELSE 1 END))), 0) as spread_income,
|
||||
COALESCE(SUM(amount), 0) as rebate_income
|
||||
FROM records WHERE user_id = ? AND category = '派单' AND partner = ?
|
||||
`).get(req.user.id, partner);
|
||||
|
||||
const otherIncome = db.prepare(`
|
||||
SELECT COALESCE(SUM(amount), 0) as total
|
||||
FROM records WHERE user_id = ? AND type = 'income' AND category != '派单' AND category != '接单' AND source = ?
|
||||
`).get(req.user.id, partner);
|
||||
|
||||
const records = db.prepare(`
|
||||
SELECT * FROM records
|
||||
WHERE user_id = ? AND (
|
||||
(category = '派单' AND partner = ?) OR
|
||||
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
||||
) ORDER BY created_at DESC
|
||||
`).all(req.user.id, partner, partner);
|
||||
|
||||
res.json({
|
||||
dispatch_count: dispatchStats.dispatch_count,
|
||||
spread_income: dispatchStats.spread_income,
|
||||
rebate_income: dispatchStats.rebate_income,
|
||||
other_income: otherIncome.total,
|
||||
records
|
||||
});
|
||||
});
|
||||
|
||||
// 最近7天每日收入
|
||||
router.get('/daily-income', (req, res) => {
|
||||
const today = new Date();
|
||||
const days = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
days.push(formatLocalDate(d));
|
||||
}
|
||||
const startDate = days[0];
|
||||
const endDay = new Date(today);
|
||||
endDay.setDate(endDay.getDate() + 1);
|
||||
const endDate = formatLocalDate(endDay);
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT DATE(created_at, 'localtime') as date, COALESCE(SUM(amount), 0) as income
|
||||
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
GROUP BY DATE(created_at, 'localtime')
|
||||
`).all(req.user.id, startDate, endDate);
|
||||
|
||||
const map = {};
|
||||
rows.forEach(r => { map[r.date] = r.income; });
|
||||
res.json(days.map(d => ({ date: d, income: map[d] || 0 })));
|
||||
});
|
||||
|
||||
// 某天收入记录详情
|
||||
router.get('/daily-detail', (req, res) => {
|
||||
const { date } = req.query;
|
||||
if (!date) return res.status(400).json({ error: '缺少日期参数' });
|
||||
const nextDay = new Date(date);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
const endDate = formatLocalDate(nextDay);
|
||||
|
||||
const summary = db.prepare(`
|
||||
SELECT COALESCE(SUM(amount), 0) as total_income
|
||||
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
`).get(req.user.id, date, endDate);
|
||||
|
||||
const records = db.prepare(`
|
||||
SELECT * FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
ORDER BY created_at DESC
|
||||
`).all(req.user.id, date, endDate);
|
||||
|
||||
res.json({ total_income: summary.total_income, records });
|
||||
});
|
||||
|
||||
// 老板收入排行 - 首页预览
|
||||
router.get('/boss-ranking/preview', (req, res) => {
|
||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||
const rows = db.prepare(`
|
||||
SELECT COALESCE(boss, source) as boss, SUM(amount) as total_income,
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
||||
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income
|
||||
FROM records
|
||||
WHERE user_id = ? AND type = 'income' AND (boss IS NOT NULL AND boss != '' OR source IS NOT NULL AND source != '')
|
||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
GROUP BY COALESCE(boss, source) ORDER BY total_income DESC LIMIT 10
|
||||
`).all(req.user.id, startDate, endDate);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// 老板收入排行 - 详情页(支持筛选)
|
||||
router.get('/boss-ranking', (req, res) => {
|
||||
const { startDate, endDate, sortBy = 'total_income' } = req.query;
|
||||
let start, end;
|
||||
if (startDate && endDate) {
|
||||
start = startDate;
|
||||
end = endDate;
|
||||
} else {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
start = `${year}-${month}-01`;
|
||||
const endMonth = now.getMonth() + 2 > 12 ? 1 : now.getMonth() + 2;
|
||||
const endYear = endMonth === 1 ? year + 1 : year;
|
||||
end = `${endYear}-${String(endMonth).padStart(2, '0')}-01`;
|
||||
}
|
||||
|
||||
const allowedSorts = ['total_income', 'take_count', 'dispatch_count'];
|
||||
const orderCol = allowedSorts.includes(sortBy) ? sortBy : 'total_income';
|
||||
|
||||
if (sortBy === 'dispatch_count') {
|
||||
const rows = db.prepare(`
|
||||
SELECT COALESCE(boss, source) as boss,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as dispatch_income,
|
||||
SUM(amount) as total_income,
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count,
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as take_income,
|
||||
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income
|
||||
FROM records
|
||||
WHERE user_id = ? AND type = 'income' AND (boss IS NOT NULL AND boss != '' OR source IS NOT NULL AND source != '')
|
||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
GROUP BY COALESCE(boss, source) ORDER BY dispatch_count DESC
|
||||
`).all(req.user.id, start, end);
|
||||
return res.json(rows.filter(r => r.dispatch_count > 0));
|
||||
}
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT COALESCE(boss, source) as boss, SUM(amount) as total_income,
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count,
|
||||
COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as take_income,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
||||
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as dispatch_income,
|
||||
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_income
|
||||
FROM records
|
||||
WHERE user_id = ? AND type = 'income' AND (boss IS NOT NULL AND boss != '' OR source IS NOT NULL AND source != '')
|
||||
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||
GROUP BY COALESCE(boss, source) ORDER BY ${orderCol} DESC
|
||||
`).all(req.user.id, start, end);
|
||||
|
||||
if (sortBy === 'take_count') {
|
||||
return res.json(rows.filter(r => r.take_count > 0));
|
||||
}
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,34 @@
|
||||
// 日期范围辅助函数
|
||||
function getMonthRange(qYear, qMonth) {
|
||||
const now = new Date();
|
||||
const y = Number(qYear) || now.getFullYear();
|
||||
const m = Number(qMonth) || (now.getMonth() + 1);
|
||||
const startDate = `${y}-${String(m).padStart(2, '0')}-01`;
|
||||
const nm = m + 1 > 12 ? 1 : m + 1;
|
||||
const ny = m + 1 > 12 ? y + 1 : y;
|
||||
const endDate = `${ny}-${String(nm).padStart(2, '0')}-01`;
|
||||
return { startDate, endDate };
|
||||
}
|
||||
|
||||
// CSV行解析(处理引号内的逗号)
|
||||
function parseCSVLine(line) {
|
||||
const result = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i];
|
||||
if (inQuotes) {
|
||||
if (ch === '"' && line[i + 1] === '"') { current += '"'; i++; }
|
||||
else if (ch === '"') { inQuotes = false; }
|
||||
else { current += ch; }
|
||||
} else {
|
||||
if (ch === '"') { inQuotes = true; }
|
||||
else if (ch === ',') { result.push(current); current = ''; }
|
||||
else { current += ch; }
|
||||
}
|
||||
}
|
||||
result.push(current);
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = { getMonthRange, parseCSVLine };
|
||||
Reference in New Issue
Block a user