fix(治理): 完成任务 8 安全与通知闭环修复

- 串行化拉黑、处罚、投瓶和匹配策略检查\n- 完成异步站内通知、未读统计和偏好并发语义\n- 补齐后台查询审计、处罚恢复和隐私测试\n- 稳定 Redis 恢复、匹配锁序及超时测试
This commit is contained in:
root
2026-09-17 13:08:55 +08:00
parent ce8e1db279
commit fa0fa78312
29 changed files with 1231 additions and 153 deletions
+81 -48
View File
@@ -6,13 +6,17 @@ import { DomainException } from "../common/domain.exception.js";
import { PrismaService } from "../database/prisma.service.js";
import { SafetyLockService } from "../safety/safety-lock.service.js";
import type { ResolveReportDto, SanctionDto } from "./dto.js";
import { NotificationService } from "../notification/notification.service.js";
@Injectable()
export class AdminService {
constructor(
@Inject(PrismaService) private readonly prisma: PrismaService,
@Inject(SafetyLockService) private readonly locks: SafetyLockService,
@Inject(NotificationService)
private readonly notifications: NotificationService,
) {}
async reports(
actorId: string,
status: "PENDING" | "REVIEWING" | "RESOLVED" | "DISMISSED" | undefined,
limit: number,
) {
@@ -21,6 +25,14 @@ export class AdminService {
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
take: limit,
});
await this.prisma.auditLog.create({
data: {
actorId,
action: "REPORTS_QUERIED",
entityType: "REPORT",
metadata: { status: status ?? null, limit, resultCount: rows.length },
},
});
return {
items: rows.map((x) => ({
id: x.id,
@@ -66,8 +78,20 @@ export class AdminService {
})
)?.authorId
: report.conversationId
? (report.targetSnapshot as { reportedAccountId?: string })
.reportedAccountId
? (
await tx.anonymousProfile.findUnique({
where: {
publicId:
(
report.targetSnapshot as {
subjectPublicId?: string;
}
).subjectPublicId ??
"00000000-0000-0000-0000-000000000000",
},
select: { accountId: true },
})
)?.accountId
: undefined);
await this.locks.lockAccounts(tx, [
actorId,
@@ -112,31 +136,21 @@ export class AdminService {
},
},
});
await tx.notification.create({
data: {
accountId: report.reporterId,
dedupeKey: `report:${id}:reporter`,
type: "REPORT_RESOLVED",
payload: { reportId: id, decision: dto.decision },
status: "SENT",
sentAt: new Date(),
},
});
if (accountId)
await tx.notification.create({
data: {
accountId,
dedupeKey: `report:${id}:target`,
type: "MODERATION_DECISION",
payload: {
reportId: id,
decision: dto.decision,
sanctionType: dto.sanction?.type ?? null,
},
status: "SENT",
sentAt: new Date(),
},
});
await this.notifications.createInApp(
tx,
report.reporterId,
`report:${id}:reporter`,
"REPORT_RESOLVED",
{ reportId: id, decision: dto.decision },
);
if (dto.decision === "UPHELD" && dto.sanction && accountId)
await this.notifications.createInApp(
tx,
accountId,
`report:${id}:target`,
"ACCOUNT_ACTION",
{ type: "ACCOUNT_ACTION" },
);
return tx.report.findUniqueOrThrow({
where: { id },
select: { id: true, status: true, resolution: true },
@@ -158,6 +172,13 @@ export class AdminService {
return this.prisma.$transaction(async (tx) => {
await this.locks.lockAccounts(tx, [actorId, profile.accountId]);
const sanction = await this.issue(tx, actorId, profile.accountId, dto);
await this.notifications.createInApp(
tx,
profile.accountId,
`sanction:${sanction.id}:account-action`,
"ACCOUNT_ACTION",
{ type: "ACCOUNT_ACTION" },
);
await tx.auditLog.create({
data: {
actorId,
@@ -170,25 +191,32 @@ export class AdminService {
return sanction;
});
}
async moderation(limit: number) {
return {
items: await this.prisma.moderationTask.findMany({
where: {
status: "COMPLETED",
decision: "MANUAL_REVIEW",
targetType: { in: ["PROFILE", "BOTTLE"] },
},
select: {
id: true,
targetType: true,
targetId: true,
riskLabels: true,
createdAt: true,
},
orderBy: { createdAt: "asc" },
take: limit,
}),
};
async moderation(actorId: string, limit: number) {
const items = await this.prisma.moderationTask.findMany({
where: {
status: "COMPLETED",
decision: "MANUAL_REVIEW",
targetType: { in: ["PROFILE", "BOTTLE"] },
},
select: {
id: true,
targetType: true,
targetId: true,
riskLabels: true,
createdAt: true,
},
orderBy: { createdAt: "asc" },
take: limit,
});
await this.prisma.auditLog.create({
data: {
actorId,
action: "MODERATION_QUERIED",
entityType: "MODERATION_TASK",
metadata: { limit, resultCount: items.length },
},
});
return { items };
}
async moderate(
actorId: string,
@@ -369,11 +397,16 @@ export class AdminService {
expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : null,
},
});
if (dto.type === "SUSPENSION" || dto.type === "BAN")
if (dto.type === "SUSPENSION" || dto.type === "BAN") {
await tx.account.update({
where: { id: accountId },
data: { status: "SUSPENDED", tokenVersion: { increment: 1 } },
data: { tokenVersion: { increment: 1 } },
});
await tx.session.updateMany({
where: { accountId, revokedAt: null },
data: { revokedAt: new Date() },
});
}
return sanction;
}
}