feat: 优化考核调研钉钉通知卡片
This commit is contained in:
parent
14a7a65ce3
commit
8f964baee5
@ -110,11 +110,15 @@ describe('dingtalk client', () => {
|
||||
title: '测试通知',
|
||||
text: '这是一条测试通知',
|
||||
actionUrl: 'https://sc.jiansun.vip/dingtalk-entry?redirect=%2Fperformance-survey',
|
||||
actionTitle: '立即填写',
|
||||
})
|
||||
|
||||
const [, sendOptions] = (global.fetch as jest.Mock).mock.calls[1]
|
||||
const body = JSON.parse(sendOptions.body)
|
||||
expect(body.msg.msgtype).toBe('action_card')
|
||||
expect(body.msg.action_card.title).toBe('测试通知')
|
||||
expect(body.msg.action_card.markdown).toBe('这是一条测试通知')
|
||||
expect(body.msg.action_card.single_title).toBe('立即填写')
|
||||
expect(body.msg.action_card.single_url).toContain('dingtalk://dingtalkclient/action/openapp')
|
||||
expect(body.msg.action_card.single_url).toContain('corpid=ding-test-corp')
|
||||
expect(body.msg.action_card.single_url).toContain('app_id=0_4279191869')
|
||||
|
||||
@ -51,6 +51,7 @@ function notification(overrides: Record<string, unknown> = {}) {
|
||||
title: '你有新的考核调研待填写',
|
||||
content: '请填写考核调研',
|
||||
actionUrl: 'https://sc.jiansun.vip/performance-survey/survey/survey-1',
|
||||
eventType: 'survey.assignment.created',
|
||||
status: 'pending',
|
||||
attemptCount: 0,
|
||||
maxAttempts: 5,
|
||||
@ -116,6 +117,7 @@ describe('performance survey notifications', () => {
|
||||
title: '你有新的考核调研待填写',
|
||||
text: '请填写考核调研',
|
||||
actionUrl: 'https://sc.jiansun.vip/performance-survey/survey/survey-1',
|
||||
actionTitle: '立即填写',
|
||||
})
|
||||
expect(prisma.performanceSurveyNotification.update).toHaveBeenCalledWith({
|
||||
where: { id: 'notification-1' },
|
||||
@ -206,7 +208,7 @@ describe('performance survey notifications', () => {
|
||||
recipientName: '被代发起人',
|
||||
eventType: 'survey.target.confirmation.required',
|
||||
businessKey: 'target-confirmation:survey-1:target-other',
|
||||
title: '请确认一项代发起调研',
|
||||
title: '待确认|七月课程分享',
|
||||
actionUrl: expect.stringContaining('/dingtalk-entry?redirect='),
|
||||
status: 'pending',
|
||||
}),
|
||||
@ -245,8 +247,8 @@ describe('performance survey notifications', () => {
|
||||
|
||||
expect(prisma.performanceSurveyNotification.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
title: '请确认并提交视觉评分作品',
|
||||
content: expect.stringContaining('提交 3 个案例作品'),
|
||||
title: '待提交作品|七月视觉评分',
|
||||
content: expect.stringContaining('**作品要求:** 3 个案例作品'),
|
||||
actionUrl: expect.stringContaining(
|
||||
'%2Fperformance-survey%2Fsurvey%2Fsurvey-visual%3FtargetId%3Dtarget-other',
|
||||
),
|
||||
|
||||
@ -3,6 +3,7 @@ type DingtalkWorkNoticeInput = {
|
||||
title: string
|
||||
text: string
|
||||
actionUrl?: string | null
|
||||
actionTitle?: string | null
|
||||
}
|
||||
|
||||
type DingtalkWorkNoticeResult = {
|
||||
@ -224,6 +225,7 @@ export async function sendDingtalkWorkNotice(
|
||||
|
||||
const token = await getDingtalkWorkNoticeAccessToken()
|
||||
const messageTitle = truncateText(input.title, 120)
|
||||
const actionTitle = truncateText(input.actionTitle || '查看详情', 20)
|
||||
const actionUrl = input.actionUrl
|
||||
? buildDingtalkWorkNoticeActionUrl(input.actionUrl, numericAgentId)
|
||||
: null
|
||||
@ -233,7 +235,7 @@ export async function sendDingtalkWorkNotice(
|
||||
action_card: {
|
||||
title: messageTitle,
|
||||
markdown: truncateText(input.text),
|
||||
single_title: '打开素材库查看',
|
||||
single_title: actionTitle,
|
||||
single_url: actionUrl,
|
||||
},
|
||||
}
|
||||
|
||||
@ -58,6 +58,42 @@ function clean(value: unknown) {
|
||||
return normalized.length > 0 ? normalized : null
|
||||
}
|
||||
|
||||
function escapeDingtalkMarkdown(value: unknown) {
|
||||
return String(value ?? '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/([`*_\[\]#>])/g, '\\$1')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function buildNotificationCard(input: {
|
||||
heading: string
|
||||
surveyTitle: string
|
||||
fields: Array<{ label: string; value: string | null | undefined }>
|
||||
note?: string | null
|
||||
}) {
|
||||
const fieldLines = input.fields
|
||||
.filter((field) => clean(field.value))
|
||||
.map((field) => `**${escapeDingtalkMarkdown(field.label)}:** ${escapeDingtalkMarkdown(field.value)} `)
|
||||
|
||||
return [
|
||||
`### ${escapeDingtalkMarkdown(input.heading)}`,
|
||||
'',
|
||||
`**${escapeDingtalkMarkdown(input.surveyTitle)}**`,
|
||||
'',
|
||||
...fieldLines,
|
||||
input.note ? `\n> ${escapeDingtalkMarkdown(input.note)}` : null,
|
||||
].filter((line): line is string => line !== null).join('\n').trim()
|
||||
}
|
||||
|
||||
function getNotificationActionTitle(eventType: string) {
|
||||
if (eventType === 'survey.assignment.created') return '立即填写'
|
||||
if (eventType === 'survey.assignment.due_reminder') return '继续填写'
|
||||
if (eventType === 'survey.target.confirmation.required') return '确认并提交'
|
||||
if (eventType === 'survey.result.available') return '查看结果'
|
||||
if (eventType.startsWith('survey.creator.')) return '查看结果'
|
||||
return '查看详情'
|
||||
}
|
||||
|
||||
async function resolveNotificationDingtalkUserId(notification: {
|
||||
dingtalkUserId?: string | null
|
||||
recipientUserId?: string | null
|
||||
@ -219,8 +255,16 @@ export async function queueSurveyAssignmentNotifications(surveyId: string) {
|
||||
recipientName: assignment.evaluatorName,
|
||||
eventType: 'survey.assignment.created',
|
||||
businessKey: `assignment:${assignment.id}`,
|
||||
title: '你有新的考核调研待填写',
|
||||
content: `你有一份新的考核调研需要填写。\n\n调研:${survey.title}\n对象:${targetName}${survey.dueAt ? `\n截止:${formatDateTime(survey.dueAt)}` : ''}`,
|
||||
title: `待填写|${survey.title}`,
|
||||
content: buildNotificationCard({
|
||||
heading: '新的考核调研待填写',
|
||||
surveyTitle: survey.title,
|
||||
fields: [
|
||||
{ label: '评价对象', value: targetName },
|
||||
{ label: '截止时间', value: formatDateTime(survey.dueAt) },
|
||||
],
|
||||
note: '请在截止时间前完成评分',
|
||||
}),
|
||||
actionUrl: getSurveyUrl(`/performance-survey/survey/${survey.id}?targetId=${assignment.targetId}`),
|
||||
}),
|
||||
)
|
||||
@ -273,10 +317,20 @@ export async function queueSurveyTargetConfirmationNotifications(surveyId: strin
|
||||
recipientName: target.targetName,
|
||||
eventType: 'survey.target.confirmation.required',
|
||||
businessKey: `target-confirmation:${survey.id}:${target.id}`,
|
||||
title: isVisualSurvey ? '请确认并提交视觉评分作品' : '请确认一项代发起调研',
|
||||
content: isVisualSurvey
|
||||
? `${survey.creatorName} 为你发起了一项视觉评分调研,请确认被考核人信息并提交 ${requiredWorkItemCount} 个案例作品。\n\n调研:${survey.title}\n被考核人:${target.targetName}\n考核月份:${formatAssessmentMonth(survey.assessmentMonth, survey.createdAt)}\n\n如信息有误,请联系发起人。`
|
||||
: `${survey.creatorName} 为你发起了一项调研,请确认被考核人信息。\n\n调研:${survey.title}\n被考核人:${target.targetName}\n考核月份:${formatAssessmentMonth(survey.assessmentMonth, survey.createdAt)}\n\n如信息有误,请联系发起人。`,
|
||||
title: isVisualSurvey ? `待提交作品|${survey.title}` : `待确认|${survey.title}`,
|
||||
content: buildNotificationCard({
|
||||
heading: isVisualSurvey ? '请确认并提交视觉评分作品' : '请确认代发起调研',
|
||||
surveyTitle: survey.title,
|
||||
fields: [
|
||||
{ label: '发起人', value: survey.creatorName },
|
||||
{ label: '被考核人', value: target.targetName },
|
||||
{ label: '考核月份', value: formatAssessmentMonth(survey.assessmentMonth, survey.createdAt) },
|
||||
isVisualSurvey
|
||||
? { label: '作品要求', value: `${requiredWorkItemCount} 个案例作品` }
|
||||
: { label: '确认事项', value: '被考核人信息' },
|
||||
],
|
||||
note: '如信息有误,请联系发起人',
|
||||
}),
|
||||
actionUrl: getSurveyUrl(
|
||||
isVisualSurvey
|
||||
? `/performance-survey/survey/${survey.id}?targetId=${target.id}`
|
||||
@ -311,8 +365,13 @@ export async function queueSurveyResultNotifications(surveyId: string) {
|
||||
recipientName: target.targetName,
|
||||
eventType: 'survey.result.available',
|
||||
businessKey: `result:${survey.id}:${target.id}`,
|
||||
title: '考核调研结果已可查看',
|
||||
content: `你参与的考核调研结果已经生成。\n\n调研:${survey.title}\n对象:${target.targetName}`,
|
||||
title: `结果已生成|${survey.title}`,
|
||||
content: buildNotificationCard({
|
||||
heading: '考核调研结果已生成',
|
||||
surveyTitle: survey.title,
|
||||
fields: [{ label: '评价对象', value: target.targetName }],
|
||||
note: '点击下方按钮查看完整结果',
|
||||
}),
|
||||
actionUrl: getSurveyUrl(`/performance-survey/results/${survey.id}?targetId=${target.id}`),
|
||||
}),
|
||||
)
|
||||
@ -346,8 +405,13 @@ export async function queueSurveyCreatorNotification(surveyId: string, eventType
|
||||
recipientName: creator?.realName || creator?.username || survey.creatorName,
|
||||
eventType,
|
||||
businessKey: `${eventType}:${survey.id}`,
|
||||
title: eventType === 'survey.creator.closed' ? '考核调研已关闭' : '考核调研已完成',
|
||||
content: `你发起的考核调研有新的进度。\n\n调研:${survey.title}\n提交进度:${submitted}/${total}`,
|
||||
title: `${eventType === 'survey.creator.closed' ? '已关闭' : '已完成'}|${survey.title}`,
|
||||
content: buildNotificationCard({
|
||||
heading: eventType === 'survey.creator.closed' ? '考核调研已关闭' : '考核调研已完成',
|
||||
surveyTitle: survey.title,
|
||||
fields: [{ label: '提交进度', value: `${submitted}/${total}` }],
|
||||
note: '点击下方按钮查看调研结果',
|
||||
}),
|
||||
actionUrl: getSurveyUrl(`/performance-survey/results/${survey.id}`),
|
||||
})
|
||||
}
|
||||
@ -388,8 +452,16 @@ export async function queueDuePerformanceSurveyReminders() {
|
||||
recipientName: assignment.evaluatorName,
|
||||
eventType: 'survey.assignment.due_reminder',
|
||||
businessKey: `due:${dueKey}:${assignment.id}`,
|
||||
title: '考核调研即将截止',
|
||||
content: `你还有一份考核调研未填写。\n\n调研:${survey.title}\n对象:${assignment.target.targetName}\n截止:${formatDateTime(survey.dueAt)}`,
|
||||
title: `即将截止|${survey.title}`,
|
||||
content: buildNotificationCard({
|
||||
heading: '考核调研即将截止',
|
||||
surveyTitle: survey.title,
|
||||
fields: [
|
||||
{ label: '评价对象', value: assignment.target.targetName },
|
||||
{ label: '截止时间', value: formatDateTime(survey.dueAt) },
|
||||
],
|
||||
note: '请尽快完成评分,避免错过截止时间',
|
||||
}),
|
||||
actionUrl: getSurveyUrl(`/performance-survey/survey/${survey.id}?targetId=${assignment.targetId}`),
|
||||
}),
|
||||
)
|
||||
@ -515,6 +587,7 @@ export async function processPerformanceSurveyNotification(notificationId: strin
|
||||
title: notification.title,
|
||||
text: notification.content,
|
||||
actionUrl: notification.actionUrl,
|
||||
actionTitle: getNotificationActionTitle(notification.eventType),
|
||||
})
|
||||
|
||||
if (result.skipped) {
|
||||
@ -644,7 +717,9 @@ function formatDateTime(value: Date | string | null | undefined) {
|
||||
if (!value) return ''
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
return date.toLocaleString('zh-CN', { hour12: false })
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${hour}:${minute}`
|
||||
}
|
||||
|
||||
function formatAssessmentMonth(value: string | null | undefined, fallback: Date | string) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user