161 lines
9.9 KiB
JavaScript
161 lines
9.9 KiB
JavaScript
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() + 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,
|
|
}
|
|
}
|
|
}
|