const MODULE = 'notifications' const OPTIONS = { scenario: 'platform-notification-administration', deploymentBlocker: true } const TEMPLATE_BASE = '/api/platform/v1/notification-templates' const TASK_BASE = '/api/platform/v1/notification-tasks' const CREATE_PATH = '/api/platform/v1/notifications' function id(value) { return value == null ? '' : String(value) } function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } function records(page) { return Array.isArray(page?.records) ? page.records : [] } function hasTrace(response) { return Boolean(response?.requestId) && Boolean(response?.traceId) } function isConflict(response) { return response?.http === 200 && ['A0409', '409'].includes(codeOf(response)) } function query(base, values) { const params = new URLSearchParams() for (const [key, value] of Object.entries(values)) if (value !== '' && value != null) params.set(key, String(value)) return `${base}?${params}` } export async function run(context) { const suffix = context.marker.replace(/[^0-9A-Za-z]/g, '').slice(-20).toUpperCase() const templateCode = `ACPT_NOTIFICATION_${suffix}`.slice(0, 64) const category = 'ACCEPTANCE' const templateCreate = { templateCode, templateName: `全量验收消息模板-${suffix}`, category, titleTemplate: '全量验收通知 {{marker}}', bodyTemplate: '这是 {{marker}} 的消息中心全量验收定时任务。', variableWhitelist: ['marker'], } const baselineResponse = await context.request('GET', query(TEMPLATE_BASE, { page: 1, size: 20, templateCode })) const baseline = context.apiSuccess(MODULE, '按 CODE 查询消息模板基线', baselineResponse, `templateCode=${templateCode}`, OPTIONS) context.assert(MODULE, '消息模板分页结构完整', baselineResponse, Array.isArray(baseline?.records) && Number(baseline?.page) === 1 && Number(baseline?.size) === 20, `records=${records(baseline).length}, total=${baseline?.total ?? 'missing'}`, OPTIONS) const invalidResponse = await context.request('POST', TEMPLATE_BASE, { ...templateCreate, templateCode: `ACPT_INVALID_${suffix}`.slice(0, 64), variableWhitelist: [], }) context.assert(MODULE, '模板占位变量未列入白名单时被拒绝且返回追踪信息', invalidResponse, invalidResponse.http === 200 && ['A0422', '422'].includes(codeOf(invalidResponse)) && hasTrace(invalidResponse), `HTTP=${invalidResponse.http}, code=${codeOf(invalidResponse)}, requestId=${invalidResponse.requestId || 'missing'}, traceId=${invalidResponse.traceId || 'missing'}`, OPTIONS) const createResponse = await context.request('POST', TEMPLATE_BASE, templateCreate) let template = context.apiSuccess(MODULE, '创建消息模板草稿', createResponse, `templateCode=${templateCode}`, OPTIONS) const templateId = id(template?.id) context.assert(MODULE, '新建模板为 DRAFT 且仅支持站内信', createResponse, Boolean(templateId) && template?.status === 'DRAFT' && template?.channel === 'IN_APP' && Number(template?.version) === 1, `templateId=${templateId || 'missing'}, status=${template?.status || 'missing'}, channel=${template?.channel || 'missing'}`, OPTIONS) if (!templateId) return let taskId = '' let taskStatus = '' let finalTemplateStatus = template?.status || 'UNKNOWN' try { const duplicateResponse = await context.request('POST', TEMPLATE_BASE, templateCreate) context.assert(MODULE, '重复模板 CODE 返回冲突并保留追踪信息', duplicateResponse, isConflict(duplicateResponse) && hasTrace(duplicateResponse), `code=${codeOf(duplicateResponse)}, requestId=${duplicateResponse.requestId || 'missing'}, traceId=${duplicateResponse.traceId || 'missing'}`, OPTIONS) const updateResponse = await context.request('PUT', `${TEMPLATE_BASE}/${encodeURIComponent(templateId)}`, { templateName: `全量验收消息模板已编辑-${suffix}`, category, titleTemplate: '全量验收通知已编辑 {{marker}}', bodyTemplate: '这是 {{marker}} 的消息中心全量验收定时任务,模板已经编辑。', variableWhitelist: ['marker'], version: template.version, }) template = context.apiSuccess(MODULE, '编辑 DRAFT 消息模板', updateResponse, '', OPTIONS) context.assert(MODULE, '模板编辑结果及版本已持久化', updateResponse, template?.templateName === `全量验收消息模板已编辑-${suffix}` && Number(template?.version) === 2, `name=${template?.templateName || 'missing'}, version=${template?.version ?? 'missing'}`, OPTIONS) const staleUpdate = await context.request('PUT', `${TEMPLATE_BASE}/${encodeURIComponent(templateId)}`, { templateName: templateCreate.templateName, category, titleTemplate: templateCreate.titleTemplate, bodyTemplate: templateCreate.bodyTemplate, variableWhitelist: ['marker'], version: 1, }) context.assert(MODULE, '旧模板版本编辑被拒绝且返回追踪信息', staleUpdate, isConflict(staleUpdate) && hasTrace(staleUpdate), `code=${codeOf(staleUpdate)}, requestId=${staleUpdate.requestId || 'missing'}, traceId=${staleUpdate.traceId || 'missing'}`, OPTIONS) const activateResponse = await context.request('PUT', `${TEMPLATE_BASE}/${encodeURIComponent(templateId)}/status`, { targetStatus: 'ACTIVE', version: template?.version, }) template = context.apiSuccess(MODULE, '激活消息模板', activateResponse, '', OPTIONS) finalTemplateStatus = template?.status || finalTemplateStatus context.assert(MODULE, '消息模板激活且版本递增', activateResponse, template?.status === 'ACTIVE' && Number(template?.version) === 3, `status=${template?.status || 'missing'}, version=${template?.version ?? 'missing'}`, OPTIONS) const scheduledAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19) const idempotencyKey = `${context.marker}-notification-scheduled`.slice(0, 128) const taskCreate = { tenantId: 0, templateCode, recipientType: 'PLATFORM_ROLE', roleCode: 'PLATFORM_ADMIN', variables: { marker: context.marker }, scheduledAt, idempotencyKey, sourceEventId: `${context.marker}-notification-acceptance`.slice(0, 128), } const taskResponse = await context.request('POST', CREATE_PATH, taskCreate) let task = context.apiSuccess(MODULE, '创建未来定时消息任务', taskResponse, `scheduledAt=${scheduledAt}`, OPTIONS) taskId = id(task?.id); taskStatus = task?.status || '' context.assert(MODULE, '定时任务创建为 SCHEDULED 且解析平台角色接收人', taskResponse, Boolean(taskId) && task?.status === 'SCHEDULED' && Number(task?.recipientCount) > 0 && task?.duplicated === false, `taskId=${taskId || 'missing'}, status=${task?.status || 'missing'}, recipientCount=${task?.recipientCount ?? 'missing'}`, OPTIONS) const replayResponse = await context.request('POST', CREATE_PATH, taskCreate) const replay = context.apiSuccess(MODULE, '同幂等键重放定时消息任务', replayResponse, '', OPTIONS) context.assert(MODULE, '幂等重放返回同一任务且标记 duplicated', replayResponse, id(replay?.id) === taskId && replay?.duplicated === true, `original=${taskId || 'missing'}, replay=${id(replay?.id) || 'missing'}, duplicated=${replay?.duplicated ?? 'missing'}`, OPTIONS) const taskListResponse = await context.request('GET', query(TASK_BASE, { page: 1, size: 20, taskNo: task?.taskNo, category, status: 'SCHEDULED' })) const taskPage = context.apiSuccess(MODULE, '按任务号和状态查询消息任务', taskListResponse, `taskNo=${task?.taskNo || 'missing'}`, OPTIONS) context.assert(MODULE, '任务列表命中本轮定时任务', taskListResponse, records(taskPage).some((row) => id(row?.id) === taskId && row?.status === 'SCHEDULED'), `taskId=${taskId || 'missing'}, records=${records(taskPage).length}`, OPTIONS) if (taskId) { const cancelResponse = await context.request('PUT', `${TASK_BASE}/${encodeURIComponent(taskId)}/cancel`) task = context.apiSuccess(MODULE, '取消未来定时消息任务', cancelResponse, '', OPTIONS) taskStatus = task?.status || taskStatus context.assert(MODULE, '定时消息任务最终为 CANCELLED', cancelResponse, task?.status === 'CANCELLED', `status=${task?.status || 'missing'}`, OPTIONS) const retryResponse = await context.request('PUT', `${TASK_BASE}/${encodeURIComponent(taskId)}/retry`) context.assert(MODULE, '已取消任务禁止重试且返回追踪信息', retryResponse, isConflict(retryResponse) && hasTrace(retryResponse), `code=${codeOf(retryResponse)}, requestId=${retryResponse.requestId || 'missing'}, traceId=${retryResponse.traceId || 'missing'}`, OPTIONS) } } finally { if (taskId && taskStatus === 'SCHEDULED') { const cleanupTask = await context.request('PUT', `${TASK_BASE}/${encodeURIComponent(taskId)}/cancel`) const cleanedTask = context.apiSuccess(MODULE, '异常路径取消未来定时消息任务', cleanupTask, `taskId=${taskId}`, OPTIONS) taskStatus = cleanedTask?.status || taskStatus } if (template?.status === 'ACTIVE') { const disableResponse = await context.request('PUT', `${TEMPLATE_BASE}/${encodeURIComponent(templateId)}/status`, { targetStatus: 'DISABLED', version: template.version, }) template = context.apiSuccess(MODULE, '停用本轮消息模板', disableResponse, `templateId=${templateId}`, OPTIONS) finalTemplateStatus = template?.status || finalTemplateStatus context.assert(MODULE, '消息模板最终为 DISABLED', disableResponse, template?.status === 'DISABLED', `status=${template?.status || 'missing'}`, OPTIONS) } context.createdResources.notifications = { marker: context.marker, templateId, templateCode, finalTemplateStatus, taskId: taskId || null, taskStatus: taskStatus || null, retained: true, } } }