import { BadRequestException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { DatabaseService } from '../db/database.service'; import { WritableDbService } from '../db/writable-db.service'; import { ROLE_RANK, type JwtPayload } from '../auth/roles'; import { validateField, EDITABLE_FIELDS } from './editable-fields'; const POINTS_PER_REVISION = 5; @Injectable() export class RevisionsService { constructor( private readonly ro: DatabaseService, private readonly wdb: WritableDbService, ) {} /** 某车型字段的当前生效值(覆盖优先于基础数据),字符串化。*/ private effectiveValue(modelId: number, field: string, baseRow: any): string { const ov = this.wdb.db .prepare('SELECT value FROM model_overrides WHERE model_id = ? AND field = ?') .get(modelId, field) as { value: string } | undefined; if (ov) return ov.value ?? ''; const v = baseRow?.[field]; return v == null ? '' : String(v); } private baseRow(modelId: number): any { const row = this.ro.db.prepare('SELECT * FROM model WHERE id = ?').get(modelId); if (!row) throw new NotFoundException(`车型 ${modelId} 不存在`); return row; } /** 提交编辑:生成修订。信任用户及以上自动通过并应用。*/ submitEdit( modelId: number, user: JwtPayload, changes: Record, note = '', ) { const base = this.baseRow(modelId); const entries = Object.entries(changes || {}); if (entries.length === 0) throw new BadRequestException('没有任何改动'); const diffs: { field: string; oldValue: string; newValue: string }[] = []; for (const [field, raw] of entries) { const newValue = validateField(field, raw); const oldValue = this.effectiveValue(modelId, field, base); if (newValue !== oldValue) diffs.push({ field, oldValue, newValue }); } if (diffs.length === 0) throw new BadRequestException('与当前值相同,无需提交'); const autoApprove = ROLE_RANK[user.role] >= ROLE_RANK.trusted; const db = this.wdb.db; const tx = db.transaction(() => { const info = db .prepare( `INSERT INTO revisions (model_id, author_id, status, note, reviewed_by, reviewed_at) VALUES (?, ?, ?, ?, ?, ?)`, ) .run( modelId, user.sub, autoApprove ? 'approved' : 'pending', note, autoApprove ? user.sub : null, autoApprove ? new Date().toISOString() : null, ); const revId = Number(info.lastInsertRowid); const ins = db.prepare( `INSERT INTO revision_changes (revision_id, field, old_value, new_value) VALUES (?, ?, ?, ?)`, ); for (const d of diffs) ins.run(revId, d.field, d.oldValue, d.newValue); if (autoApprove) { this.applyChanges(modelId, diffs, revId); this.award(user.sub); } return revId; }); const revId = tx(); return this.getRevision(revId); } private applyChanges( modelId: number, diffs: { field: string; newValue: string }[], revId: number, ) { const up = this.wdb.db.prepare( `INSERT INTO model_overrides (model_id, field, value, revision_id, updated_at) VALUES (?, ?, ?, ?, datetime('now')) ON CONFLICT(model_id, field) DO UPDATE SET value = excluded.value, revision_id = excluded.revision_id, updated_at = datetime('now')`, ); for (const d of diffs) up.run(modelId, d.field, d.newValue, revId); } private award(userId: number, points = POINTS_PER_REVISION) { this.wdb.db .prepare('UPDATE users SET contribution_points = contribution_points + ? WHERE id = ?') .run(points, userId); } approve(revId: number, reviewer: JwtPayload) { const db = this.wdb.db; const rev: any = db.prepare('SELECT * FROM revisions WHERE id = ?').get(revId); if (!rev) throw new NotFoundException('修订不存在'); if (rev.status !== 'pending') throw new BadRequestException('该修订已处理'); const changes = db .prepare('SELECT field, new_value AS newValue FROM revision_changes WHERE revision_id = ?') .all(revId) as { field: string; newValue: string }[]; const tx = db.transaction(() => { db.prepare( `UPDATE revisions SET status='approved', reviewed_by=?, reviewed_at=datetime('now') WHERE id=?`, ).run(reviewer.sub, revId); this.applyChanges(rev.model_id, changes, revId); this.award(rev.author_id); }); tx(); return this.getRevision(revId); } reject(revId: number, reviewer: JwtPayload) { const db = this.wdb.db; const rev: any = db.prepare('SELECT * FROM revisions WHERE id = ?').get(revId); if (!rev) throw new NotFoundException('修订不存在'); if (rev.status !== 'pending') throw new BadRequestException('该修订已处理'); db.prepare( `UPDATE revisions SET status='rejected', reviewed_by=?, reviewed_at=datetime('now') WHERE id=?`, ).run(reviewer.sub, revId); return this.getRevision(revId); } getRevision(revId: number) { const db = this.wdb.db; const rev: any = db .prepare( `SELECT r.*, u.display_name AS author_name FROM revisions r JOIN users u ON u.id = r.author_id WHERE r.id = ?`, ) .get(revId); if (!rev) throw new NotFoundException('修订不存在'); rev.changes = db .prepare('SELECT field, old_value, new_value FROM revision_changes WHERE revision_id = ?') .all(revId); return rev; } listForModel(modelId: number) { const db = this.wdb.db; const revs = db .prepare( `SELECT r.*, u.display_name AS author_name FROM revisions r JOIN users u ON u.id = r.author_id WHERE r.model_id = ? ORDER BY r.created_at DESC, r.id DESC`, ) .all(modelId) as any[]; const stmt = db.prepare( 'SELECT field, old_value, new_value FROM revision_changes WHERE revision_id = ?', ); for (const r of revs) r.changes = stmt.all(r.id); return revs; } listPending() { const db = this.wdb.db; const revs = db .prepare( `SELECT r.*, u.display_name AS author_name FROM revisions r JOIN users u ON u.id = r.author_id WHERE r.status = 'pending' ORDER BY r.created_at ASC`, ) .all() as any[]; const stmt = db.prepare( 'SELECT field, old_value, new_value FROM revision_changes WHERE revision_id = ?', ); for (const r of revs) r.changes = stmt.all(r.id); return revs; } /** 供 ModelsService 合并:返回 model 的字段覆盖 map。*/ overridesFor(modelId: number): Record { const rows = this.wdb.db .prepare('SELECT field, value FROM model_overrides WHERE model_id = ?') .all(modelId) as { field: string; value: string }[]; const out: Record = {}; for (const r of rows) out[r.field] = r.value; return out; } editableFields() { return EDITABLE_FIELDS; } }