优化固定资产钉钉通知排版

This commit is contained in:
WuYongJie 2026-07-16 21:16:47 +08:00
parent d25d600c7a
commit 6a38680aa7
5 changed files with 190 additions and 58 deletions

View File

@ -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

View File

@ -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'
}

View File

@ -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'

View File

@ -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

View File

@ -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'
}