2026-03-12 11:23:10 +08:00
|
|
|
const express = require('express');
|
|
|
|
|
const db = require('../db');
|
2026-03-26 10:39:58 +08:00
|
|
|
const config = require('../config');
|
|
|
|
|
const logger = require('../utils/logger');
|
|
|
|
|
const { getMonthRange, AppError, validateRequired, validateNumber, validateInList } = require('../utils/helpers');
|
2026-03-12 11:23:10 +08:00
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
2026-03-25 10:25:37 +08:00
|
|
|
// 内存缓存(限制大小,基于LRU)
|
|
|
|
|
const statsCache = new Map();
|
2026-03-26 10:39:58 +08:00
|
|
|
const CACHE_TTL = config.cache.ttl;
|
|
|
|
|
const MAX_CACHE_SIZE = config.cache.maxSize;
|
2026-03-25 10:25:37 +08:00
|
|
|
|
|
|
|
|
function getCached(key, fetchFn) {
|
|
|
|
|
const cached = statsCache.get(key);
|
|
|
|
|
if (cached && Date.now() - cached.ts < CACHE_TTL) {
|
|
|
|
|
// LRU: move to end
|
|
|
|
|
statsCache.delete(key);
|
|
|
|
|
statsCache.set(key, cached);
|
|
|
|
|
return Promise.resolve(cached.data);
|
|
|
|
|
}
|
|
|
|
|
if (statsCache.size >= MAX_CACHE_SIZE) {
|
|
|
|
|
// Delete oldest entry
|
|
|
|
|
const firstKey = statsCache.keys().next().value;
|
|
|
|
|
statsCache.delete(firstKey);
|
|
|
|
|
}
|
2026-03-25 22:12:11 +08:00
|
|
|
return Promise.resolve(fetchFn()).then(data => {
|
2026-03-25 10:25:37 +08:00
|
|
|
statsCache.set(key, { data, ts: Date.now() });
|
2026-03-26 10:39:58 +08:00
|
|
|
logger.debug(`缓存更新: ${key}`);
|
2026-03-25 10:25:37 +08:00
|
|
|
return data;
|
|
|
|
|
}).catch(err => {
|
2026-03-26 10:39:58 +08:00
|
|
|
logger.error('缓存获取失败:', err.message);
|
2026-03-25 22:12:11 +08:00
|
|
|
throw err;
|
2026-03-25 10:25:37 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// POST/DELETE/PUT/PATCH 后清除当前用户的缓存
|
|
|
|
|
router.use((req, res, next) => {
|
|
|
|
|
res.once('finish', () => {
|
|
|
|
|
if (['POST', 'DELETE', 'PUT', 'PATCH'].includes(req.method) && req.user?.id) {
|
|
|
|
|
for (const key of statsCache.keys()) {
|
|
|
|
|
if (key.includes(`:${req.user.id}:`)) {
|
|
|
|
|
statsCache.delete(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
// 格式化本地日期为 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}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 本月统计
|
2026-03-26 10:39:58 +08:00
|
|
|
router.get('/monthly', (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
|
|
|
|
const cacheKey = `monthly:${req.user.id}:${startDate}:${endDate}`;
|
|
|
|
|
getCached(cacheKey, () => {
|
|
|
|
|
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);
|
|
|
|
|
return stats;
|
|
|
|
|
}).then(stats => res.json(stats)).catch(next);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 月收入/支出记录列表
|
2026-03-26 10:39:58 +08:00
|
|
|
router.get('/monthly-records', (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const { type, page = 1, limit = 20 } = req.query;
|
|
|
|
|
if (!type || !['income', 'expense'].includes(type)) {
|
|
|
|
|
throw new AppError('缺少类型参数或类型无效', 400);
|
|
|
|
|
}
|
|
|
|
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
|
|
|
|
const validatedPage = Math.max(1, Number(page) || 1);
|
|
|
|
|
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
|
|
|
|
|
const offset = (validatedPage - 1) * validatedLimit;
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
const stats = db.prepare(`
|
|
|
|
|
SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as count
|
|
|
|
|
FROM records WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
|
|
|
|
`).get(req.user.id, type, startDate, endDate);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
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 LIMIT ? OFFSET ?
|
|
|
|
|
`).all(req.user.id, type, startDate, endDate, validatedLimit, offset);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
res.json({ total: stats.total, records, page: validatedPage, limit: validatedLimit, totalCount: stats.count });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 年度净收入按年统计
|
|
|
|
|
router.get('/yearly-net-income', (req, res) => {
|
2026-03-25 10:25:37 +08:00
|
|
|
const combined = db.prepare(`
|
2026-03-12 11:23:10 +08:00
|
|
|
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 = ?
|
2026-03-25 10:25:37 +08:00
|
|
|
GROUP BY year, month
|
|
|
|
|
UNION ALL
|
2026-03-12 11:23:10 +08:00
|
|
|
SELECT
|
2026-03-25 10:25:37 +08:00
|
|
|
strftime('%Y', created_at) as year,
|
|
|
|
|
'00' as month,
|
2026-03-12 11:23:10 +08:00
|
|
|
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
|
2026-03-25 10:25:37 +08:00
|
|
|
FROM records
|
|
|
|
|
WHERE user_id = ?
|
|
|
|
|
GROUP BY year
|
|
|
|
|
`).all(req.user.id, req.user.id);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-25 10:25:37 +08:00
|
|
|
const yearlyStats = [];
|
|
|
|
|
const monthlyData = {};
|
|
|
|
|
let grandTotal = { total_income: 0, total_expense: 0 };
|
|
|
|
|
|
|
|
|
|
combined.forEach(row => {
|
|
|
|
|
grandTotal.total_income += row.total_income;
|
|
|
|
|
grandTotal.total_expense += row.total_expense;
|
|
|
|
|
if (row.month === '00') {
|
|
|
|
|
yearlyStats.push({
|
|
|
|
|
year: row.year,
|
|
|
|
|
total_income: row.total_income,
|
|
|
|
|
total_expense: row.total_expense
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
if (!monthlyData[row.year]) monthlyData[row.year] = [];
|
|
|
|
|
monthlyData[row.year].push({
|
|
|
|
|
month: row.month,
|
|
|
|
|
total_income: row.total_income,
|
|
|
|
|
total_expense: row.total_expense
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
yearlyStats.sort((a, b) => b.year - a.year);
|
2026-03-12 11:23:10 +08:00
|
|
|
res.json({ yearlyStats, monthlyData, grandTotal });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 陪玩派单榜预览
|
|
|
|
|
router.get('/partner-ranking/preview', (req, res) => {
|
|
|
|
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
2026-03-25 10:25:37 +08:00
|
|
|
const cacheKey = `partner-ranking-preview:${req.user.id}:${startDate}:${endDate}`;
|
|
|
|
|
getCached(cacheKey, () => {
|
|
|
|
|
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);
|
|
|
|
|
return rows;
|
|
|
|
|
}).then(rows => res.json(rows));
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 陪玩派单榜详情
|
|
|
|
|
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);
|
|
|
|
|
});
|
2026-03-25 10:25:37 +08:00
|
|
|
const summaryStats = db.prepare(`
|
2026-03-12 11:23:10 +08:00
|
|
|
SELECT
|
2026-03-25 10:25:37 +08:00
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as total_dispatch_count,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as total_dispatch_income,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as total_rebate_income,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as total_milkTea_income
|
2026-03-12 11:23:10 +08:00
|
|
|
FROM records
|
2026-03-25 10:25:37 +08:00
|
|
|
WHERE user_id = ? AND type = 'income' AND category IN ('派单', '奶茶')
|
2026-03-12 11:23:10 +08:00
|
|
|
AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
|
|
|
|
`).get(req.user.id, start, end);
|
2026-03-25 10:25:37 +08:00
|
|
|
const summary = {
|
|
|
|
|
total_dispatch_count: summaryStats.total_dispatch_count,
|
|
|
|
|
total_dispatch_income: summaryStats.total_dispatch_income,
|
|
|
|
|
total_rebate_income: summaryStats.total_rebate_income,
|
|
|
|
|
total_partner_income: summaryStats.total_dispatch_income - summaryStats.total_rebate_income,
|
|
|
|
|
total_milkTea_income: summaryStats.total_milkTea_income
|
|
|
|
|
};
|
2026-03-12 11:23:10 +08:00
|
|
|
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
|
2026-03-25 10:25:37 +08:00
|
|
|
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as total_take_count,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '接单' THEN amount ELSE 0 END), 0) as total_take_income,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as total_redEnvelope_income
|
|
|
|
|
FROM records WHERE user_id = ? AND type = 'income' AND category IN ('接单', '红包收入')
|
2026-03-12 11:23:10 +08:00
|
|
|
`).get(req.user.id);
|
|
|
|
|
const result = {
|
|
|
|
|
rows: rows,
|
|
|
|
|
total_take_count: stats.total_take_count,
|
|
|
|
|
total_take_income: stats.total_take_income,
|
2026-03-25 10:25:37 +08:00
|
|
|
total_redEnvelope_income: stats.total_redEnvelope_income
|
2026-03-12 11:23:10 +08:00
|
|
|
};
|
|
|
|
|
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);
|
|
|
|
|
});
|
2026-03-25 10:25:37 +08:00
|
|
|
const stats = db.prepare(`
|
|
|
|
|
SELECT
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as total_dispatch_count,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '奶茶' THEN amount ELSE 0 END), 0) as total_milkTea_income
|
|
|
|
|
FROM records WHERE user_id = ? AND type = 'income' AND category IN ('派单', '奶茶')
|
2026-03-12 11:23:10 +08:00
|
|
|
`).get(req.user.id);
|
|
|
|
|
const result = {
|
|
|
|
|
rows: rows,
|
2026-03-25 10:25:37 +08:00
|
|
|
total_dispatch_count: stats.total_dispatch_count,
|
|
|
|
|
total_milkTea_income: stats.total_milkTea_income
|
2026-03-12 11:23:10 +08:00
|
|
|
};
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 老板交易记录
|
2026-03-26 10:39:58 +08:00
|
|
|
router.get('/boss-records', (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const boss = req.query.boss;
|
|
|
|
|
const { page = 1, limit = 20, year, month } = req.query;
|
|
|
|
|
if (!boss) throw new AppError('缺少老板参数', 400);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
const validatedPage = Math.max(1, Number(page) || 1);
|
|
|
|
|
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
|
|
|
|
|
const offset = (validatedPage - 1) * validatedLimit;
|
|
|
|
|
|
|
|
|
|
// 日期过滤条件
|
|
|
|
|
let dateFilter = '';
|
|
|
|
|
let dateParams = [];
|
|
|
|
|
if (year && month) {
|
|
|
|
|
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
|
|
|
|
|
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
|
|
|
|
|
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
|
|
|
|
|
dateParams = [startDate, endDate];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const summary = db.prepare(`
|
|
|
|
|
SELECT
|
|
|
|
|
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as my_income,
|
|
|
|
|
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,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count,
|
|
|
|
|
COUNT(*) as count
|
|
|
|
|
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter}
|
|
|
|
|
`).get(req.user.id, boss, ...dateParams);
|
|
|
|
|
|
|
|
|
|
const records = db.prepare(`
|
|
|
|
|
SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
|
|
|
|
|
`).all(req.user.id, boss, ...dateParams, validatedLimit, offset);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
my_income: summary.my_income,
|
|
|
|
|
boss_total_expense: summary.dispatch_expense + summary.order_expense + summary.redEnvelope_expense,
|
|
|
|
|
take_count: summary.take_count,
|
|
|
|
|
dispatch_count: summary.dispatch_count,
|
|
|
|
|
records,
|
|
|
|
|
page: validatedPage,
|
|
|
|
|
limit: validatedLimit,
|
|
|
|
|
totalCount: summary.count
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
2026-03-19 10:13:02 +08:00
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 陪玩交易记录
|
2026-03-26 10:39:58 +08:00
|
|
|
router.get('/partner-records', (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const partner = req.query.partner;
|
|
|
|
|
const { page = 1, limit = 20, year, month } = req.query;
|
|
|
|
|
if (!partner) throw new AppError('缺少陪玩参数', 400);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
const validatedPage = Math.max(1, Number(page) || 1);
|
|
|
|
|
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
|
|
|
|
|
const offset = (validatedPage - 1) * validatedLimit;
|
|
|
|
|
|
|
|
|
|
// 日期过滤条件
|
|
|
|
|
let dateFilter = '';
|
|
|
|
|
let dateParams = [];
|
|
|
|
|
if (year && month) {
|
|
|
|
|
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
|
|
|
|
|
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
|
|
|
|
|
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
|
|
|
|
|
dateParams = [startDate, endDate];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const summary = db.prepare(`
|
|
|
|
|
SELECT
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * (COALESCE(unit_price, 0) - COALESCE(rebate, amount * 1.0 / CASE WHEN quantity > 0 THEN quantity ELSE 1 END)) ELSE 0 END), 0) as spread_income,
|
|
|
|
|
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as rebate_income,
|
|
|
|
|
COALESCE(SUM(CASE WHEN type = 'income' AND category != '派单' AND category != '接单' AND source = ? THEN amount ELSE 0 END), 0) as other_income,
|
|
|
|
|
COUNT(*) as count
|
|
|
|
|
FROM records WHERE user_id = ? AND (
|
|
|
|
|
(category = '派单' AND partner = ?) OR
|
|
|
|
|
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
|
|
|
|
)${dateFilter}
|
|
|
|
|
`).get(partner, req.user.id, partner, partner, ...dateParams);
|
|
|
|
|
|
|
|
|
|
const records = db.prepare(`
|
|
|
|
|
SELECT * FROM records
|
|
|
|
|
WHERE user_id = ? AND (
|
|
|
|
|
(category = '派单' AND partner = ?) OR
|
|
|
|
|
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
|
|
|
|
)${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
|
|
|
|
|
`).all(req.user.id, partner, partner, ...dateParams, validatedLimit, offset);
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
dispatch_count: summary.dispatch_count,
|
|
|
|
|
spread_income: summary.spread_income,
|
|
|
|
|
rebate_income: summary.rebate_income,
|
|
|
|
|
other_income: summary.other_income,
|
|
|
|
|
records,
|
|
|
|
|
page: validatedPage,
|
|
|
|
|
limit: validatedLimit,
|
|
|
|
|
totalCount: summary.count
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
2026-03-19 10:13:02 +08:00
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 最近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);
|
|
|
|
|
|
2026-03-25 10:25:37 +08:00
|
|
|
const cacheKey = `daily-income:${req.user.id}:${startDate}`;
|
|
|
|
|
getCached(cacheKey, () => {
|
|
|
|
|
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; });
|
|
|
|
|
return days.map(d => ({ date: d, income: map[d] || 0 }));
|
|
|
|
|
}).then(data => res.json(data));
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 某天收入记录详情
|
2026-03-26 10:39:58 +08:00
|
|
|
router.get('/daily-detail', (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const { date, page = 1, limit = 20 } = req.query;
|
|
|
|
|
if (!date) throw new AppError('缺少日期参数', 400);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
const validatedPage = Math.max(1, Number(page) || 1);
|
|
|
|
|
const validatedLimit = Math.min(Math.max(1, Number(limit) || 20), config.pagination.maxLimit);
|
|
|
|
|
const offset = (validatedPage - 1) * validatedLimit;
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
const nextDay = new Date(date);
|
|
|
|
|
nextDay.setDate(nextDay.getDate() + 1);
|
|
|
|
|
const endDate = formatLocalDate(nextDay);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
2026-03-26 10:39:58 +08:00
|
|
|
const stats = db.prepare(`
|
|
|
|
|
SELECT COALESCE(SUM(amount), 0) as total_income, COUNT(*) as count
|
|
|
|
|
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 LIMIT ? OFFSET ?
|
|
|
|
|
`).all(req.user.id, date, endDate, validatedLimit, offset);
|
|
|
|
|
|
|
|
|
|
res.json({ total_income: stats.total_income, records, page: validatedPage, limit: validatedLimit, totalCount: stats.count });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 老板收入排行 - 首页预览
|
|
|
|
|
router.get('/boss-ranking/preview', (req, res) => {
|
|
|
|
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
2026-03-25 10:25:37 +08:00
|
|
|
const cacheKey = `boss-ranking-preview:${req.user.id}:${startDate}:${endDate}`;
|
|
|
|
|
getCached(cacheKey, () => {
|
|
|
|
|
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);
|
|
|
|
|
return rows;
|
|
|
|
|
}).then(rows => res.json(rows));
|
2026-03-12 11:23:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 老板收入排行 - 详情页(支持筛选)
|
|
|
|
|
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;
|