feat: add public bank sync failure ops

This commit is contained in:
Codex
2026-06-30 09:12:45 +08:00
parent d1cb341351
commit 959e612211
14 changed files with 484 additions and 44 deletions

View File

@@ -39,6 +39,14 @@ function truncate(value: unknown, max = 1900) {
return String(value ?? '').slice(0, max);
}
function publicBankFailureDedupeKey(candidate: PublicBankSyncCandidate, code: string) {
return [
'public-bank-sync-failed',
candidate.id,
crypto.createHash('sha256').update(code).digest('hex').slice(0, 16),
].join(':');
}
async function claimPublicBankSyncCandidates(limit: number, claimId: string) {
const client = await pool.connect();
try {
@@ -126,38 +134,133 @@ async function markPublicBankSyncFailed(candidate: PublicBankSyncCandidate, erro
workerId: config.publicBankSyncWorkerId,
failedAt: nowIso(),
};
await pool.query(
`
update public.tenant_question_bank_adoptions
set status = 'active',
sync_status = 'failed',
metadata = jsonb_set(
coalesce(metadata, '{}'::jsonb),
'{publicBankSyncWorker}',
coalesce(metadata->'publicBankSyncWorker', '{}'::jsonb) || $3::jsonb,
true
),
updated_at = now()
where tenant_id = $1 and id = $2
`,
[
candidate.tenantId,
candidate.id,
JSON.stringify({
lastStatus: 'failed',
lastError: details,
lastWorkerId: config.publicBankSyncWorkerId,
lastFinishedAt: nowIso(),
}),
],
);
await pool.query(
`
insert into public.audit_logs (tenant_id, actor_user_id, action, target_type, target_id, details)
values ($1, null, 'content.public_question_bank.sync_worker_failed', 'tenant_question_bank_adoption', $2, $3::jsonb)
`,
[candidate.tenantId, candidate.id, JSON.stringify(details)],
);
const client = await pool.connect();
try {
await client.query('begin');
await client.query(
`
update public.tenant_question_bank_adoptions
set status = 'active',
sync_status = 'failed',
metadata = jsonb_set(
coalesce(metadata, '{}'::jsonb),
'{publicBankSyncWorker}',
coalesce(metadata->'publicBankSyncWorker', '{}'::jsonb) || $3::jsonb,
true
),
updated_at = now()
where tenant_id = $1 and id = $2
`,
[
candidate.tenantId,
candidate.id,
JSON.stringify({
lastStatus: 'failed',
lastError: details,
lastWorkerId: config.publicBankSyncWorkerId,
lastFinishedAt: nowIso(),
}),
],
);
const adoptionResult = await client.query<{
source_question_bank_id: string;
target_question_bank_id: string | null;
target_entry_id: string | null;
target_collection_id: string | null;
source_question_bank_name: string | null;
target_question_bank_name: string | null;
}>(
`
select a.source_question_bank_id,
a.target_question_bank_id,
a.target_entry_id,
a.target_collection_id,
source_qb.name as source_question_bank_name,
target_qb.name as target_question_bank_name
from public.tenant_question_bank_adoptions a
join public.question_banks source_qb
on source_qb.id = a.source_question_bank_id
left join public.question_banks target_qb
on target_qb.id = a.target_question_bank_id
and target_qb.tenant_id = a.tenant_id
where a.tenant_id = $1
and a.id = $2
`,
[candidate.tenantId, candidate.id],
);
const adoption = adoptionResult.rows[0] || null;
await client.query(
`
insert into public.audit_logs (tenant_id, actor_user_id, action, target_type, target_id, details)
values ($1, null, 'content.public_question_bank.sync_worker_failed', 'tenant_question_bank_adoption', $2, $3::jsonb)
`,
[candidate.tenantId, candidate.id, JSON.stringify(details)],
);
if (adoption) {
await client.query(
`
insert into public.tenant_content_notifications (
tenant_id, notification_type, status, severity,
adoption_id, source_question_bank_id,
title, message, action_label, action_path,
dedupe_key, metadata, created_by
)
values (
$1, 'public_question_bank_sync_failed', 'unread', 'error',
$2, $3,
$4, $5, '查看同步状态', $6,
$7, $8::jsonb, null
)
on conflict (tenant_id, notification_type, dedupe_key)
where dedupe_key is not null
do update set status = case
when public.tenant_content_notifications.status = 'dismissed' then public.tenant_content_notifications.status
else 'unread'
end,
severity = excluded.severity,
title = excluded.title,
message = excluded.message,
action_label = excluded.action_label,
action_path = excluded.action_path,
metadata = excluded.metadata,
resolved_at = null,
updated_at = now()
`,
[
candidate.tenantId,
candidate.id,
adoption.source_question_bank_id,
`${adoption.source_question_bank_name || '公共题库'} 同步失败`,
`公共题库自动同步失败,错误码:${details.code}。请稍后重试或联系平台处理。`,
`/tenant-admin/content?adoptionId=${candidate.id}&panel=public-banks`,
publicBankFailureDedupeKey(candidate, details.code),
JSON.stringify({
adoptionId: candidate.id,
sourceQuestionBankId: adoption.source_question_bank_id,
sourceQuestionBankName: adoption.source_question_bank_name,
targetQuestionBankId: adoption.target_question_bank_id,
targetQuestionBankName: adoption.target_question_bank_name,
targetEntryId: adoption.target_entry_id,
targetCollectionId: adoption.target_collection_id,
syncStatus: 'failed',
errorCode: details.code,
errorMessage: details.message,
triggeredBy: 'worker',
workerId: config.publicBankSyncWorkerId,
failedAt: details.failedAt,
}),
],
);
}
await client.query('commit');
} catch (failure) {
await client.query('rollback');
throw failure;
} finally {
client.release();
}
}
export async function processPublicBankSyncBatch(limit = config.publicBankSyncBatchSize): Promise<PublicBankSyncWorkerResult> {