优化固定资产钉钉通知排版
This commit is contained in:
parent
d25d600c7a
commit
6a38680aa7
@ -2,7 +2,14 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import prisma from '../../utils/prisma'
|
||||
import { approveDisposal, createDisposal } from '../disposalController'
|
||||
import {
|
||||
approveDisposal,
|
||||
buildDisposalAppliedDingTalkContent,
|
||||
buildDisposalApprovedDingTalkContent,
|
||||
buildDisposalCompletedDingTalkContent,
|
||||
buildDisposalRejectedDingTalkContent,
|
||||
createDisposal
|
||||
} from '../disposalController'
|
||||
|
||||
const now = new Date('2026-07-14T03:00:00.000Z')
|
||||
const rootEquipment = {
|
||||
@ -50,6 +57,62 @@ const createResponse = (role = 'it') => {
|
||||
return { res, state }
|
||||
}
|
||||
|
||||
test('报废流程钉钉正文不重复项目标题、时间优先且字段间保留空行', () => {
|
||||
const cases = [
|
||||
{
|
||||
body: buildDisposalAppliedDingTalkContent({
|
||||
appliedAt: now,
|
||||
equipmentName: '整机',
|
||||
equipmentCode: 'JS-PC-0010',
|
||||
applicantName: '申请人',
|
||||
reason: '设备老化'
|
||||
}),
|
||||
labels: ['申请时间', '资产', '申请人', '报废原因']
|
||||
},
|
||||
{
|
||||
body: buildDisposalApprovedDingTalkContent({
|
||||
approvedAt: now,
|
||||
equipmentName: '整机',
|
||||
equipmentCode: 'JS-PC-0010',
|
||||
approverName: '审批人'
|
||||
}),
|
||||
labels: ['审批时间', '资产', '审批人', '审批结果']
|
||||
},
|
||||
{
|
||||
body: buildDisposalRejectedDingTalkContent({
|
||||
rejectedAt: now,
|
||||
equipmentName: '整机',
|
||||
equipmentCode: 'JS-PC-0010',
|
||||
rejectorName: '驳回人',
|
||||
reason: '尚可继续使用'
|
||||
}),
|
||||
labels: ['驳回时间', '资产', '驳回人', '驳回原因']
|
||||
},
|
||||
{
|
||||
body: buildDisposalCompletedDingTalkContent({
|
||||
completedAt: now,
|
||||
equipmentName: '整机',
|
||||
equipmentCode: 'JS-PC-0010',
|
||||
completerName: '完成人',
|
||||
notes: '已环保处置'
|
||||
}),
|
||||
labels: ['完成时间', '资产', '完成人', '处置结果', '完成说明']
|
||||
}
|
||||
]
|
||||
|
||||
for (const item of cases) {
|
||||
assert.doesNotMatch(item.body, /(^|\n)###\s/)
|
||||
assert.doesNotMatch(item.body, /【固定资产】/)
|
||||
const paragraphs = item.body.split('\n\n')
|
||||
assert.equal(paragraphs.length, item.labels.length + 1)
|
||||
item.labels.forEach((label, index) => {
|
||||
assert.match(paragraphs[index], new RegExp(`^\\*\\*${label}:\\*\\* `))
|
||||
assert.doesNotMatch(paragraphs[index], /\n/)
|
||||
})
|
||||
assert.match(paragraphs.at(-1) || '', /^> /)
|
||||
}
|
||||
})
|
||||
|
||||
test('主资产仍挂载未勾选配件时申请报废返回 409 且不写状态', async () => {
|
||||
const originalTransaction = prisma.$transaction
|
||||
let stateWriteCount = 0
|
||||
|
||||
@ -141,6 +141,62 @@ const getResultRecipients = async (tx: Prisma.TransactionClient, applierId: numb
|
||||
const assetNotificationName = (name: unknown, code: unknown) =>
|
||||
`${escapeNotificationMarkdownInline(name, '资产')}(${escapeNotificationMarkdownInline(code, '无编码')})`
|
||||
|
||||
export const buildDisposalAppliedDingTalkContent = (input: {
|
||||
appliedAt: Date
|
||||
equipmentName: unknown
|
||||
equipmentCode: unknown
|
||||
applicantName: unknown
|
||||
reason: unknown
|
||||
}) => [
|
||||
`**申请时间:** ${formatBeijingDateTime(input.appliedAt)}`,
|
||||
`**资产:** ${assetNotificationName(input.equipmentName, input.equipmentCode)}`,
|
||||
`**申请人:** ${escapeNotificationMarkdownInline(input.applicantName)}`,
|
||||
`**报废原因:** ${escapeNotificationMarkdownInline(input.reason)}`,
|
||||
'> 请核对资产及配件明细后处理。'
|
||||
].join('\n\n')
|
||||
|
||||
export const buildDisposalApprovedDingTalkContent = (input: {
|
||||
approvedAt: Date
|
||||
equipmentName: unknown
|
||||
equipmentCode: unknown
|
||||
approverName: unknown
|
||||
}) => [
|
||||
`**审批时间:** ${formatBeijingDateTime(input.approvedAt)}`,
|
||||
`**资产:** ${assetNotificationName(input.equipmentName, input.equipmentCode)}`,
|
||||
`**审批人:** ${escapeNotificationMarkdownInline(input.approverName)}`,
|
||||
'**审批结果:** 已通过',
|
||||
'> 报废单现已进入待处置阶段。'
|
||||
].join('\n\n')
|
||||
|
||||
export const buildDisposalRejectedDingTalkContent = (input: {
|
||||
rejectedAt: Date
|
||||
equipmentName: unknown
|
||||
equipmentCode: unknown
|
||||
rejectorName: unknown
|
||||
reason: unknown
|
||||
}) => [
|
||||
`**驳回时间:** ${formatBeijingDateTime(input.rejectedAt)}`,
|
||||
`**资产:** ${assetNotificationName(input.equipmentName, input.equipmentCode)}`,
|
||||
`**驳回人:** ${escapeNotificationMarkdownInline(input.rejectorName)}`,
|
||||
`**驳回原因:** ${escapeNotificationMarkdownInline(input.reason)}`,
|
||||
'> 资产状态已恢复为申请前状态。'
|
||||
].join('\n\n')
|
||||
|
||||
export const buildDisposalCompletedDingTalkContent = (input: {
|
||||
completedAt: Date
|
||||
equipmentName: unknown
|
||||
equipmentCode: unknown
|
||||
completerName: unknown
|
||||
notes: unknown
|
||||
}) => [
|
||||
`**完成时间:** ${formatBeijingDateTime(input.completedAt)}`,
|
||||
`**资产:** ${assetNotificationName(input.equipmentName, input.equipmentCode)}`,
|
||||
`**完成人:** ${escapeNotificationMarkdownInline(input.completerName)}`,
|
||||
'**处置结果:** 已完成报废',
|
||||
`**完成说明:** ${escapeNotificationMarkdownInline(input.notes, '无')}`,
|
||||
'> 相关资产现已统一标记为已报废。'
|
||||
].join('\n\n')
|
||||
|
||||
const getCurrentUser = (res: Response) => ({
|
||||
id: Number(res.locals.user?.id),
|
||||
role: String(res.locals.user?.role || '')
|
||||
@ -282,16 +338,13 @@ export const createDisposal = async (req: Request, res: Response) => {
|
||||
metadata: { equipment_code: root.equipment_code },
|
||||
dingTalk: {
|
||||
title: '资产报废待审批',
|
||||
content: [
|
||||
'### 待你审批',
|
||||
'',
|
||||
`**资产:** ${assetNotificationName(root.equipment_name, root.equipment_code)}`,
|
||||
`**申请人:** ${escapeNotificationMarkdownInline(res.locals.user?.name || res.locals.user?.username)}`,
|
||||
`**申请时间:** ${formatBeijingDateTime(disposal.applied_at)}`,
|
||||
`**报废原因:** ${escapeNotificationMarkdownInline(disposal_reason)}`,
|
||||
'',
|
||||
'> 请核对资产及配件明细后处理。'
|
||||
].join('\n'),
|
||||
content: buildDisposalAppliedDingTalkContent({
|
||||
appliedAt: disposal.applied_at,
|
||||
equipmentName: root.equipment_name,
|
||||
equipmentCode: root.equipment_code,
|
||||
applicantName: res.locals.user?.name || res.locals.user?.username,
|
||||
reason: disposal_reason
|
||||
}),
|
||||
actionTitle: '立即审批',
|
||||
messageVersion: 'v1'
|
||||
}
|
||||
@ -484,32 +537,24 @@ export const approveDisposal = async (req: Request, res: Response) => {
|
||||
dingTalk: approved
|
||||
? {
|
||||
title: '资产报废审批已通过',
|
||||
content: [
|
||||
'### 处理完成',
|
||||
'',
|
||||
`**资产:** ${assetNotificationName(rootItem?.equipment.equipment_name, disposal.equipment_code)}`,
|
||||
'**审批结果:** 已通过',
|
||||
`**审批人:** ${escapeNotificationMarkdownInline(res.locals.user?.name || res.locals.user?.username)}`,
|
||||
`**审批时间:** ${formatBeijingDateTime(approvedAt)}`,
|
||||
'',
|
||||
'> 报废单现已进入待处置阶段。'
|
||||
].join('\n'),
|
||||
content: buildDisposalApprovedDingTalkContent({
|
||||
approvedAt,
|
||||
equipmentName: rootItem?.equipment.equipment_name,
|
||||
equipmentCode: disposal.equipment_code,
|
||||
approverName: res.locals.user?.name || res.locals.user?.username
|
||||
}),
|
||||
actionTitle: '查看审批结果',
|
||||
messageVersion: 'v1'
|
||||
}
|
||||
: {
|
||||
title: '资产报废申请已驳回',
|
||||
content: [
|
||||
'### 结果已更新',
|
||||
'',
|
||||
`**资产:** ${assetNotificationName(rootItem?.equipment.equipment_name, disposal.equipment_code)}`,
|
||||
'**审批结果:** 已驳回',
|
||||
`**驳回原因:** ${escapeNotificationMarkdownInline(reject_reason)}`,
|
||||
`**审批人:** ${escapeNotificationMarkdownInline(res.locals.user?.name || res.locals.user?.username)}`,
|
||||
`**审批时间:** ${formatBeijingDateTime(approvedAt)}`,
|
||||
'',
|
||||
'> 资产状态已恢复为申请前状态。'
|
||||
].join('\n'),
|
||||
content: buildDisposalRejectedDingTalkContent({
|
||||
rejectedAt: approvedAt,
|
||||
equipmentName: rootItem?.equipment.equipment_name,
|
||||
equipmentCode: disposal.equipment_code,
|
||||
rejectorName: res.locals.user?.name || res.locals.user?.username,
|
||||
reason: reject_reason
|
||||
}),
|
||||
actionTitle: '查看申请',
|
||||
messageVersion: 'v1'
|
||||
}
|
||||
@ -598,17 +643,13 @@ export const completeDisposal = async (req: Request, res: Response) => {
|
||||
dingTalkRecipients: recipients.dingTalkRecipients,
|
||||
dingTalk: {
|
||||
title: '资产报废处置已完成',
|
||||
content: [
|
||||
'### 处理完成',
|
||||
'',
|
||||
`**资产:** ${assetNotificationName(rootItem?.equipment.equipment_name, disposal.equipment_code)}`,
|
||||
'**处置结果:** 已完成报废',
|
||||
`**完成人:** ${escapeNotificationMarkdownInline(res.locals.user?.name || res.locals.user?.username)}`,
|
||||
`**完成时间:** ${formatBeijingDateTime(completedAt)}`,
|
||||
`**完成说明:** ${escapeNotificationMarkdownInline(completion_notes?.trim() || notes?.trim(), '无')}`,
|
||||
'',
|
||||
'> 相关资产现已统一标记为已报废。'
|
||||
].join('\n'),
|
||||
content: buildDisposalCompletedDingTalkContent({
|
||||
completedAt,
|
||||
equipmentName: rootItem?.equipment.equipment_name,
|
||||
equipmentCode: disposal.equipment_code,
|
||||
completerName: res.locals.user?.name || res.locals.user?.username,
|
||||
notes: completion_notes?.trim() || notes?.trim()
|
||||
}),
|
||||
actionTitle: '查看结果',
|
||||
messageVersion: 'v1'
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ test('业务通知按事件、业务ID、钉钉userId和消息版本生成稳定
|
||||
dingTalkRecipients: [{ id: 7, person: { source_userid: 'ding-user-7' } }],
|
||||
dingTalk: {
|
||||
title: '资产报废待审批',
|
||||
content: '### 待你审批\n\n**资产:** 主机(JS-PC-0053)',
|
||||
content: '**申请时间:** 2026年7月16日 10:00\n\n**资产:** 主机(JS-PC-0053)',
|
||||
actionTitle: '立即审批',
|
||||
messageVersion: 'v1'
|
||||
}
|
||||
@ -102,7 +102,7 @@ test('业务通知按事件、业务ID、钉钉userId和消息版本生成稳定
|
||||
assert.equal(deliveryRows[0].recipient_dingtalk_userid, 'ding-user-7')
|
||||
assert.deepEqual(JSON.parse(deliveryRows[0].payload_json), {
|
||||
title: '资产报废待审批',
|
||||
content: '### 待你审批\n\n**资产:** 主机(JS-PC-0053)',
|
||||
content: '**申请时间:** 2026年7月16日 10:00\n\n**资产:** 主机(JS-PC-0053)',
|
||||
action_title: '立即审批',
|
||||
action_url: '/workflows/disposals/53',
|
||||
message_version: 'v1'
|
||||
|
||||
@ -2,7 +2,10 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import prisma from '../../utils/prisma'
|
||||
import { createOverdueDisposalReminders } from '../dingtalkDeliveryService'
|
||||
import {
|
||||
buildOverdueDisposalReminderDingTalkContent,
|
||||
createOverdueDisposalReminders
|
||||
} from '../dingtalkDeliveryService'
|
||||
|
||||
const candidate = {
|
||||
id: 71,
|
||||
@ -12,6 +15,24 @@ const candidate = {
|
||||
reminded_at: null
|
||||
}
|
||||
|
||||
test('24小时催办正文不重复项目标题、时间优先且字段间保留空行', () => {
|
||||
const body = buildOverdueDisposalReminderDingTalkContent({
|
||||
appliedAt: candidate.applied_at,
|
||||
equipmentName: '整机',
|
||||
equipmentCode: candidate.equipment_code,
|
||||
applicantName: '申请人'
|
||||
})
|
||||
|
||||
assert.doesNotMatch(body, /(^|\n)###\s/)
|
||||
assert.doesNotMatch(body, /【固定资产】/)
|
||||
const paragraphs = body.split('\n\n')
|
||||
assert.deepEqual(
|
||||
paragraphs.map((paragraph) => paragraph.match(/^\*\*(.+?):\*\*/)?.[1] || '>'),
|
||||
['申请时间', '资产', '申请人', '等待时长', '>']
|
||||
)
|
||||
assert.ok(paragraphs.every((paragraph) => !paragraph.includes('\n')))
|
||||
})
|
||||
|
||||
test('24小时催办在无行政时回退通知启用管理员', async () => {
|
||||
const originalFindMany = prisma.disposalRecord.findMany
|
||||
const originalTransaction = prisma.$transaction
|
||||
|
||||
@ -378,6 +378,19 @@ const refreshOneDeliveryResult = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const buildOverdueDisposalReminderDingTalkContent = (input: {
|
||||
appliedAt: Date
|
||||
equipmentName: unknown
|
||||
equipmentCode: unknown
|
||||
applicantName: unknown
|
||||
}) => [
|
||||
`**申请时间:** ${formatBeijingDateTime(input.appliedAt)}`,
|
||||
`**资产:** ${escapeNotificationMarkdownInline(input.equipmentName, '资产')}(${escapeNotificationMarkdownInline(input.equipmentCode, '无编码')})`,
|
||||
`**申请人:** ${escapeNotificationMarkdownInline(input.applicantName)}`,
|
||||
'**等待时长:** 已超过24小时',
|
||||
'> 请核对资产及配件明细后尽快处理。'
|
||||
].join('\n\n')
|
||||
|
||||
export const createOverdueDisposalReminders = async (
|
||||
rollout: DingTalkRecipientRollout = getCurrentDingTalkRecipientRollout()
|
||||
) => {
|
||||
@ -427,8 +440,6 @@ export const createOverdueDisposalReminders = async (
|
||||
detail: '提交满24小时仍未审批,系统自动催办一次'
|
||||
}
|
||||
})
|
||||
const assetName = `${escapeNotificationMarkdownInline(candidate.equipment?.equipment_name, '资产')}(${escapeNotificationMarkdownInline(candidate.equipment_code, '无编码')})`
|
||||
const applierName = escapeNotificationMarkdownInline(candidate.applier?.name || candidate.applier?.username)
|
||||
await createBusinessNotification(tx, {
|
||||
eventType: 'disposal_overdue_reminder',
|
||||
businessType: 'disposal',
|
||||
@ -441,16 +452,12 @@ export const createOverdueDisposalReminders = async (
|
||||
dingTalkRecipients,
|
||||
dingTalk: {
|
||||
title: '资产报废待审批',
|
||||
content: [
|
||||
'### 待你审批',
|
||||
'',
|
||||
`**资产:** ${assetName}`,
|
||||
`**申请人:** ${applierName}`,
|
||||
`**申请时间:** ${formatBeijingDateTime(candidate.applied_at)}`,
|
||||
`**等待时长:** 已超过24小时`,
|
||||
'',
|
||||
'> 请核对资产及配件明细后尽快处理。'
|
||||
].join('\n'),
|
||||
content: buildOverdueDisposalReminderDingTalkContent({
|
||||
appliedAt: candidate.applied_at,
|
||||
equipmentName: candidate.equipment?.equipment_name,
|
||||
equipmentCode: candidate.equipment_code,
|
||||
applicantName: candidate.applier?.name || candidate.applier?.username
|
||||
}),
|
||||
actionTitle: '立即审批',
|
||||
messageVersion: 'reminder-v1'
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user