201 lines
6.9 KiB
TypeScript
201 lines
6.9 KiB
TypeScript
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<string, unknown>,
|
|
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<string, string> {
|
|
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<string, string> = {};
|
|
for (const r of rows) out[r.field] = r.value;
|
|
return out;
|
|
}
|
|
|
|
editableFields() {
|
|
return EDITABLE_FIELDS;
|
|
}
|
|
}
|