init: AI培训与智能巡检系统
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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<T> {
|
||||
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<any> {
|
||||
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<string, Map<string, any[]>>();
|
||||
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 })),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user