fix(治理): 完成任务 8 安全与通知闭环修复
- 串行化拉黑、处罚、投瓶和匹配策略检查\n- 完成异步站内通知、未读统计和偏好并发语义\n- 补齐后台查询审计、处罚恢复和隐私测试\n- 稳定 Redis 恢复、匹配锁序及超时测试
This commit is contained in:
@@ -166,6 +166,42 @@ describe("conversation API with real PostgreSQL", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("queues a first-reply notification without message text or internal account ids", async () => {
|
||||
const author = await actor("author");
|
||||
const picker = await actor("picker");
|
||||
const { bottle, lease } = await leased(author.id, picker);
|
||||
const key = randomUUID();
|
||||
const first = await reply(
|
||||
picker.authorization,
|
||||
bottle.id,
|
||||
lease,
|
||||
key,
|
||||
"very private body",
|
||||
).expect(201);
|
||||
await reply(
|
||||
picker.authorization,
|
||||
bottle.id,
|
||||
lease,
|
||||
key,
|
||||
"very private body",
|
||||
).expect(201);
|
||||
const rows = await prisma.notification.findMany({
|
||||
where: { accountId: author.id },
|
||||
});
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toMatchObject({
|
||||
type: "FIRST_REPLY",
|
||||
status: "PENDING",
|
||||
sentAt: null,
|
||||
});
|
||||
expect(rows[0]!.payload).toEqual({
|
||||
conversationId: first.body.data.conversationId,
|
||||
});
|
||||
expect(JSON.stringify(rows[0]!.payload)).not.toContain(picker.id);
|
||||
expect(JSON.stringify(rows[0]!.payload)).not.toContain(author.id);
|
||||
expect(JSON.stringify(rows[0]!.payload)).not.toContain("very private body");
|
||||
});
|
||||
|
||||
it("atomically consumes a lease and deduplicates concurrent first replies", async () => {
|
||||
const author = await actor("author");
|
||||
const picker = await actor("picker");
|
||||
@@ -274,7 +310,7 @@ describe("conversation API with real PostgreSQL", () => {
|
||||
randomUUID(),
|
||||
text,
|
||||
);
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.status).toBe(kind === "sanction" ? 401 : 403);
|
||||
expect(await prisma.conversation.count()).toBe(0);
|
||||
expect(
|
||||
(await prisma.bottle.findUniqueOrThrow({ where: { id: bottle.id } }))
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AuthModule } from "../auth/auth.module.js";
|
||||
import { DatabaseModule } from "../database/database.module.js";
|
||||
import { RedisModule } from "../redis/redis.module.js";
|
||||
import { SafetyModule } from "../safety/safety.module.js";
|
||||
import { NotificationModule } from "../notification/notification.module.js";
|
||||
import { ChatGateway } from "./chat.gateway.js";
|
||||
import { ChatRateLimiter } from "./chat-rate-limiter.js";
|
||||
import { ConversationController } from "./conversation.controller.js";
|
||||
@@ -11,7 +12,13 @@ import { CHAT_PUBLISHER, OutboxMessageRelay } from "./outbox-message-relay.js";
|
||||
import { StateChangingOriginGuard } from "./state-changing-origin.guard.js";
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, RedisModule, AuthModule, SafetyModule],
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
RedisModule,
|
||||
AuthModule,
|
||||
SafetyModule,
|
||||
NotificationModule,
|
||||
],
|
||||
controllers: [ConversationController],
|
||||
providers: [
|
||||
ConversationService,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { leaseHmac } from "../auth/auth.config.js";
|
||||
import { DomainException } from "../common/domain.exception.js";
|
||||
import { PrismaService } from "../database/prisma.service.js";
|
||||
import { SafetyLockService } from "../safety/safety-lock.service.js";
|
||||
import { NotificationService } from "../notification/notification.service.js";
|
||||
|
||||
type Db = Prisma.TransactionClient | PrismaClient;
|
||||
type MessageRow = {
|
||||
@@ -46,6 +47,8 @@ export class ConversationService {
|
||||
constructor(
|
||||
@Inject(PrismaService) private readonly prisma: PrismaService,
|
||||
@Inject(SafetyLockService) private readonly safetyLocks: SafetyLockService,
|
||||
@Inject(NotificationService)
|
||||
private readonly notifications: NotificationService,
|
||||
) {}
|
||||
|
||||
async reply(
|
||||
@@ -154,6 +157,13 @@ export class ConversationService {
|
||||
});
|
||||
const message = conversation.messages[0]!;
|
||||
await this.createMessageOutbox(tx, message.id, conversation.id);
|
||||
await this.notifications.createInApp(
|
||||
tx,
|
||||
authorId,
|
||||
`first-reply:${conversation.id}`,
|
||||
"FIRST_REPLY",
|
||||
{ conversationId: conversation.id },
|
||||
);
|
||||
const consumed = await tx.bottle.updateMany({
|
||||
where: {
|
||||
id: bottleId,
|
||||
|
||||
Reference in New Issue
Block a user