65b0bb04f8
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline: - server/src/ (13 个路由 + middleware + services + config) - server/migrations/ (0001~0018 共 18 个迁移 + mysql) - server/test/ (12 文件 101 测试) - client/src/ (20 个 view + components + stores + api + composables) - client/public/ + client/scripts/ - 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql) - .husky/pre-commit (git hooks) - docs/install/ (宝塔部署文档) 不含: - node_modules/ (本地 npm install) - .env (敏感, 走 .env.example) - *.zip / *.log / *.sqlite / .DS_Store 新增文档 docs/DEV-PLAN.md: - Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view) - Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单) - Phase 3: 验证 (测试 + E2E + DB 完整性) - 交付清单 + commit 模板 + 给 Mavis review 的材料 后续 Trae 实施, 提交后我 code review + 跑测试。
126 lines
5.1 KiB
JavaScript
126 lines
5.1 KiB
JavaScript
// server/test/routes.stats.test.js — /api/stats/extra 端点测试
|
|
// Trae v2.7 加的 3 个图表数据接口
|
|
// mock db() 跑纯逻辑测试
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
|
|
const mocks = vi.hoisted(() => {
|
|
const _refuels = [
|
|
{ refuel_date: '2026-04-15', liters: 50, total_cost: 400, is_deleted: 0, vehicle_id: 1 },
|
|
{ refuel_date: '2026-05-20', liters: 60, total_cost: 480, is_deleted: 0, vehicle_id: 1 },
|
|
{ refuel_date: '2026-06-10', liters: 45, total_cost: 360, is_deleted: 0, vehicle_id: 1 },
|
|
];
|
|
const _vehicles = [
|
|
{ id: 1, name: '我的 Tiguan', plate: '粤B12345', created_at: '2026-04-01', is_active: 1 },
|
|
];
|
|
const _washes = [
|
|
{ wash_date: '2026-05-10', cost: 100, vehicle_id: 1, is_deleted: 0 },
|
|
{ wash_date: '2026-06-01', cost: 150, vehicle_id: 1, is_deleted: 0 },
|
|
];
|
|
const _maintenances = [
|
|
{ maint_date: '2026-06-15', total_cost: 500, vehicle_id: 1, is_deleted: 0 },
|
|
];
|
|
const _insurances = [
|
|
{ start_date: '2026-01-01', end_date: '2027-01-01', premium: 3000, vehicle_id: 1, is_deleted: 0 },
|
|
];
|
|
const _chargings = [];
|
|
return { _refuels, _vehicles, _washes, _maintenances, _insurances, _chargings };
|
|
});
|
|
|
|
vi.mock('../src/db.js', () => ({
|
|
db: () => ({
|
|
all: vi.fn(async (sql) => {
|
|
if (/refuel_records/.test(sql) && /substr.*refuel_date/.test(sql)) {
|
|
// 油价趋势:按月聚合
|
|
const map = new Map();
|
|
for (const r of mocks._refuels) {
|
|
const ym = r.refuel_date.slice(0, 7);
|
|
if (!map.has(ym)) map.set(ym, { ym, sum: 0, lit: 0, cnt: 0 });
|
|
const m = map.get(ym);
|
|
m.sum += r.total_cost; m.lit += r.liters; m.cnt++;
|
|
}
|
|
return [...map.values()].map(m => ({
|
|
ym: m.ym,
|
|
derived_unit_price: m.lit > 0 ? Math.round(m.sum / m.lit * 1000) / 1000 : null,
|
|
cnt: m.cnt,
|
|
total_amount: m.sum,
|
|
total_liters: Math.round(m.lit * 100) / 100,
|
|
}));
|
|
}
|
|
if (/WITH owned/i.test(sql)) {
|
|
// 车辆成本 CTE
|
|
return mocks._vehicles.map(v => ({
|
|
id: v.id, name: v.name, plate: v.plate,
|
|
days_owned: 100,
|
|
lifetime_cost: 400 + 500 + 3000,
|
|
annual_cost: 400 * 365 / 100 + 500 * 365 / 100 + 3000 * 365 / 100,
|
|
}));
|
|
}
|
|
if (/mo AS month/.test(sql)) {
|
|
const map = new Map();
|
|
for (const w of mocks._washes) {
|
|
const ym = w.wash_date.slice(0, 7);
|
|
const mo = Number(w.wash_date.slice(5, 7));
|
|
const k = ym + '-' + mo;
|
|
if (!map.has(k)) map.set(k, { ym, month: mo, cnt: 0, sum: 0 });
|
|
const m = map.get(k);
|
|
m.cnt++; m.sum += w.cost;
|
|
}
|
|
return [...map.values()].map(m => ({
|
|
ym: m.ym, month: m.month, cnt: m.cnt,
|
|
avg_cost: m.cnt > 0 ? Math.round(m.sum / m.cnt * 100) / 100 : null,
|
|
total_cost: m.sum,
|
|
}));
|
|
}
|
|
return [];
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
import statsRouter from '../src/routes/settings.js';
|
|
|
|
describe('GET /api/stats/extra', () => {
|
|
let app;
|
|
beforeEach(() => {
|
|
app = express();
|
|
app.use('/api', statsRouter);
|
|
});
|
|
|
|
it('返 {ok, data} 包装(前端 axios interceptor 才能解包)', async () => {
|
|
const r = await request(app).get('/api/stats/extra');
|
|
expect(r.status).toBe(200);
|
|
expect(r.body.ok).toBe(true);
|
|
expect(r.body.data).toBeDefined();
|
|
expect(Array.isArray(r.body.data.fuelTrend)).toBe(true);
|
|
expect(Array.isArray(r.body.data.costPerVehicle)).toBe(true);
|
|
expect(Array.isArray(r.body.data.washSeason)).toBe(true);
|
|
});
|
|
|
|
it('油价趋势字段正确', async () => {
|
|
const r = await request(app).get('/api/stats/extra');
|
|
const ft = r.body.data.fuelTrend;
|
|
expect(ft.length).toBeGreaterThan(0);
|
|
expect(ft[0].ym).toMatch(/^\d{4}-\d{2}$/);
|
|
expect(ft[0].cnt).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('车辆成本包含必要字段', async () => {
|
|
const r = await request(app).get('/api/stats/extra');
|
|
const cpv = r.body.data.costPerVehicle;
|
|
expect(cpv.length).toBeGreaterThan(0);
|
|
const row = cpv[0];
|
|
expect(row).toHaveProperty('id');
|
|
expect(row).toHaveProperty('days_owned');
|
|
expect(row).toHaveProperty('lifetime_cost');
|
|
expect(row).toHaveProperty('annual_cost');
|
|
});
|
|
|
|
it('洗车季节按月聚合', async () => {
|
|
const r = await request(app).get('/api/stats/extra');
|
|
const ws = r.body.data.washSeason;
|
|
expect(ws.length).toBeGreaterThan(0);
|
|
expect(ws[0].month).toBeGreaterThanOrEqual(1);
|
|
expect(ws[0].month).toBeLessThanOrEqual(12);
|
|
});
|
|
}); |