feat(store): 添加门店、门店验证
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import type { Queue } from 'bullmq';
|
||||
import type { Database } from '@atlas/database';
|
||||
|
||||
export type OutboxJob = {
|
||||
eventId: string;
|
||||
workspaceId: string;
|
||||
eventType: string;
|
||||
payload: { store_id: string; operation_id: string; secret_id?: string };
|
||||
};
|
||||
|
||||
type EventRow = Omit<OutboxJob, 'eventId' | 'workspaceId' | 'eventType'> & {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
event_type: string;
|
||||
event_payload: OutboxJob['payload'];
|
||||
};
|
||||
|
||||
export class OutboxDispatcher {
|
||||
public constructor(
|
||||
private readonly database: Database,
|
||||
private readonly queue: Queue<OutboxJob>,
|
||||
private readonly workerId: string,
|
||||
) {}
|
||||
|
||||
public async dispatchBatch() {
|
||||
// 业务表受 RLS 保护;先枚举工作空间,再在各自租户事务中认领事件。
|
||||
const workspaces = await this.database.query<{ id: string }>(
|
||||
'SELECT id FROM atlas.workspaces',
|
||||
);
|
||||
for (const workspace of workspaces.rows) {
|
||||
const events = await this.claim(workspace.id);
|
||||
for (const event of events) await this.dispatch(event);
|
||||
}
|
||||
}
|
||||
|
||||
private async claim(workspaceId: string): Promise<OutboxJob[]> {
|
||||
return this.database.workspaceTransaction(workspaceId, async (tx) => {
|
||||
const events = await tx.query<EventRow>(
|
||||
`WITH candidates AS (
|
||||
SELECT id FROM atlas.outbox_events
|
||||
WHERE (status = 'pending' AND next_attempt_at <= now())
|
||||
OR (status = 'dispatching' AND locked_until < now())
|
||||
ORDER BY created_at
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 20
|
||||
)
|
||||
UPDATE atlas.outbox_events event
|
||||
SET status = 'dispatching', locked_by = $1,
|
||||
locked_until = now() + interval '2 minutes', updated_at = now()
|
||||
FROM candidates
|
||||
WHERE event.id = candidates.id
|
||||
RETURNING event.id, event.workspace_id, event.event_type, event.event_payload`,
|
||||
[this.workerId],
|
||||
);
|
||||
return events.rows.map((event) => ({
|
||||
eventId: event.id,
|
||||
workspaceId: event.workspace_id,
|
||||
eventType: event.event_type,
|
||||
payload: event.event_payload,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private async dispatch(event: OutboxJob) {
|
||||
try {
|
||||
await this.queue.add(event.eventType, event, {
|
||||
jobId: event.eventId,
|
||||
attempts: 5,
|
||||
backoff: { type: 'exponential', delay: 5_000 },
|
||||
removeOnComplete: 1_000,
|
||||
removeOnFail: 1_000,
|
||||
});
|
||||
await this.database.workspaceTransaction(event.workspaceId, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.outbox_events SET status = 'dispatched', dispatched_at = now(),
|
||||
locked_by = NULL, locked_until = NULL, updated_at = now()
|
||||
WHERE id = $1 AND status = 'dispatching' AND locked_by = $2`,
|
||||
[event.eventId, this.workerId],
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
await this.database.workspaceTransaction(event.workspaceId, (tx) =>
|
||||
tx.query(
|
||||
`UPDATE atlas.outbox_events SET status = 'pending', attempt_count = attempt_count + 1,
|
||||
next_attempt_at = now() + interval '30 seconds', locked_by = NULL, locked_until = NULL,
|
||||
last_error_code = 'QUEUE_UNAVAILABLE', last_error_message = '任务队列暂不可用', updated_at = now()
|
||||
WHERE id = $1 AND status = 'dispatching' AND locked_by = $2`,
|
||||
[event.eventId, this.workerId],
|
||||
),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user