import { Injectable, NotFoundException } from '@nestjs/common'; import { DatabaseService } from '../db/database.service'; import { WritableDbService } from '../db/writable-db.service'; import { EDITABLE_MAP } from '../contrib/editable-fields'; import { QueryModelsDto, SORTABLE_FIELDS } from './query-models.dto'; export interface PagedResult { total: number; page: number; pageSize: number; items: T[]; } @Injectable() export class ModelsService { constructor( private readonly database: DatabaseService, private readonly wdb: WritableDbService, ) {} /** 构造 WHERE 子句与绑定参数(参数化,避免注入)。*/ private buildWhere(q: QueryModelsDto): { clause: string; params: any[] } { const conds: string[] = []; const params: any[] = []; if (q.category) { if (/^\d+$/.test(q.category)) { conds.push('m.category_id = ?'); params.push(Number(q.category)); } else { conds.push('c.name LIKE ?'); params.push(`%${q.category}%`); } } if (q.yearFrom != null) { conds.push('m.first_year >= ?'); params.push(q.yearFrom); } if (q.yearTo != null) { conds.push('m.first_year <= ?'); params.push(q.yearTo); } if (q.speedMin != null) { conds.push('m.max_speed_value >= ?'); params.push(q.speedMin); } if (q.speedMax != null) { conds.push('m.max_speed_value <= ?'); params.push(q.speedMax); } if (q.manufacturer) { conds.push('m.manufacturer LIKE ?'); params.push(`%${q.manufacturer}%`); } if (q.country) { conds.push('m.country_type = ?'); params.push(q.country); } if (q.status) { conds.push('m.status = ?'); params.push(q.status); } if (q.q) { conds.push('(m.model_code LIKE ? OR m.full_name LIKE ? OR m.series LIKE ?)'); params.push(`%${q.q}%`, `%${q.q}%`, `%${q.q}%`); } const clause = conds.length ? `WHERE ${conds.join(' AND ')}` : ''; return { clause, params }; } list(q: QueryModelsDto): PagedResult { const { clause, params } = this.buildWhere(q); const db = this.database.db; const total = ( db .prepare( `SELECT COUNT(*) AS n FROM model m JOIN category c ON c.id = m.category_id ${clause}`, ) .get(...params) as { n: number } ).n; const sort = (SORTABLE_FIELDS as readonly string[]).includes(q.sort) ? q.sort : 'first_year'; const order = q.order === 'desc' ? 'DESC' : 'ASC'; const offset = (q.page - 1) * q.pageSize; const items = db .prepare( `SELECT m.id, m.model_code, m.full_name, m.series, m.manufacturer, m.country, m.country_type, m.first_year, m.last_year, m.status, m.usage, m.max_speed_value, m.max_speed_unit, m.weight_value, m.weight_unit, m.axle_arrangement, c.name AS category, c.subcat FROM model m JOIN category c ON c.id = m.category_id ${clause} ORDER BY (m.${sort} IS NULL), m.${sort} ${order}, m.id ASC LIMIT ? OFFSET ?`, ) .all(...params, q.pageSize, offset) as any[]; // 附加共享图库封面(每个车型最早一张照片) if (items.length) { const ids = items.map((i) => i.id); const ph = ids.map(() => '?').join(','); const covers = this.wdb.db .prepare( `SELECT p.model_id, p.filename FROM photos p WHERE p.status='confirmed' AND p.model_id IN (${ph}) AND p.id = (SELECT p2.id FROM photos p2 WHERE p2.model_id = p.model_id AND p2.status='confirmed' ORDER BY p2.featured DESC, p2.id ASC LIMIT 1)`, ) .all(...ids) as { model_id: number; filename: string }[]; const map = new Map(covers.map((c) => [c.model_id, c.filename])); for (const it of items) { const f = map.get(it.id); it.cover_url = f ? `/uploads/${f}` : null; } } return { total, page: q.page, pageSize: q.pageSize, items }; } getById(id: number): any { const db = this.database.db; const row: any = db .prepare( `SELECT m.*, c.name AS category, c.subcat FROM model m JOIN category c ON c.id = m.category_id WHERE m.id = ?`, ) .get(id); if (!row) { throw new NotFoundException(`车型 ${id} 不存在`); } if (row.raw_json) { try { row.raw = JSON.parse(row.raw_json); } catch { row.raw = {}; } } // 叠加众包字段覆盖(已审核通过的编辑) const overrides = this.wdb.db .prepare('SELECT field, value FROM model_overrides WHERE model_id = ?') .all(id) as { field: string; value: string }[]; if (overrides.length) { row.overridden = []; for (const o of overrides) { const def = EDITABLE_MAP.get(o.field); row[o.field] = def?.type === 'int' ? (o.value === '' ? null : Number(o.value)) : o.value; row.overridden.push(o.field); } } return row; } /** 技术族谱:按分类→系列聚合现有车型(series 字段)。*/ families(category?: string) { const rows = this.database.db .prepare( `SELECT m.id, m.model_code, m.series, m.first_year, m.last_year, m.country_type, m.max_speed_value, m.max_speed_unit, c.name AS category FROM model m JOIN category c ON c.id = m.category_id ${category ? 'WHERE c.name = ?' : ''} ORDER BY c.name, m.series, (m.first_year IS NULL), m.first_year`, ) .all(...(category ? [category] : [])) as any[]; const cats = new Map>(); for (const r of rows) { if (!cats.has(r.category)) cats.set(r.category, new Map()); const series = r.series || '(未归类)'; const sm = cats.get(r.category)!; if (!sm.has(series)) sm.set(series, []); sm.get(series)!.push(r); } return [...cats.entries()].map(([cat, sm]) => ({ category: cat, series: [...sm.entries()].map(([name, models]) => ({ name, models })), })); } }