fix(治理): 完成任务 8 安全与通知闭环修复
- 串行化拉黑、处罚、投瓶和匹配策略检查\n- 完成异步站内通知、未读统计和偏好并发语义\n- 补齐后台查询审计、处罚恢复和隐私测试\n- 稳定 Redis 恢复、匹配锁序及超时测试
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user