Files
order_site/apps/backend/src/services/admin/admin-notification-service.ts
T
2026-08-20 19:56:10 +08:00

293 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
bumpAdminNotificationReminder,
countPendingAdminNotifications,
createAdminNotification,
getPendingAdminNotificationByDedupeKey,
listPendingAdminNotifications,
resolveAdminNotificationsByEntity,
type AdminNotificationRow,
} from '../../repositories/admin-notification-repo.js'
import { notifyInternalSafely } from '../notification/domain-notifications.js'
import {
getWorkerPlatformNotificationConfig,
type WorkerPlatformNotificationEventKey,
} from '../worker-platform/worker-platform-notification-config-service.js'
import { publishRealtimeEvent } from '../realtime/realtime-event-service.js'
const ADMIN_WORKER_PLATFORM_PATH = '/admin/worker-platform'
export async function createWorkerWithdrawAdminNotification(input: {
requestId: number
workerName: string
amountFen: number
channel: string
}) {
return createConfiguredWorkerPlatformNotification('withdraw_requested', {
notificationType: 'worker_withdraw_requested',
priority: 'high',
entityType: 'worker_finance_request',
entityId: input.requestId,
dedupeKey: `worker_withdraw_request:${input.requestId}`,
title: '有新的提现申请',
body: `打手:${input.workerName || '-'};金额:${formatAmount(input.amountFen)};渠道:${formatChannel(input.channel)}`,
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=finance`,
now: new Date().toISOString(),
})
}
export async function createWorkerRechargeAdminNotification(input: {
requestId: number
workerName: string
amountFen: number
}) {
return createConfiguredWorkerPlatformNotification('recharge_requested', {
notificationType: 'worker_recharge_requested',
priority: 'normal',
entityType: 'worker_finance_request',
entityId: input.requestId,
dedupeKey: `worker_recharge_request:${input.requestId}`,
title: '有新的充值申请',
body: `打手:${input.workerName || '-'};金额:${formatAmount(input.amountFen)}`,
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=finance`,
now: new Date().toISOString(),
})
}
export async function createWorkOrderMaterialAdminNotification(input: {
workOrderId: number
workOrderNo: string
productName: string
}) {
return createConfiguredWorkerPlatformNotification('material_required', {
notificationType: 'work_order_material_required',
priority: 'normal',
entityType: 'work_order',
entityId: input.workOrderId,
dedupeKey: `work_order_material:${input.workOrderId}`,
title: '有工单待补资料',
body: `订单:${input.workOrderNo || input.workOrderId};商品:${input.productName || '-'}`,
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=orders`,
now: new Date().toISOString(),
})
}
export async function createWorkerAcceptanceAdminNotification(input: {
workOrderId: number
workOrderNo: string
productName: string
workerName: string
imageCount: number
}) {
return createConfiguredWorkerPlatformNotification('acceptance_submitted', {
notificationType: 'worker_acceptance_submitted',
priority: 'normal',
entityType: 'work_order',
entityId: input.workOrderId,
dedupeKey: acceptanceNotificationKey(input.workOrderId),
title: '有订单待验收',
body: [
`订单:${input.workOrderNo || input.workOrderId}`,
`商品:${input.productName || '-'}`,
`打手:${input.workerName || '-'}`,
`验收图片:${input.imageCount} 张`,
].join(''),
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=orders`,
now: new Date().toISOString(),
})
}
export async function createWorkerCancelRequestAdminNotification(input: {
requestId: number
workOrderId: number
workOrderNo: string
productName: string
workerName: string
reason: string
}) {
return createConfiguredWorkerPlatformNotification('cancel_requested', {
notificationType: 'worker_cancel_requested',
priority: 'high',
entityType: 'worker_cancel_request',
entityId: input.requestId,
dedupeKey: `worker_cancel_request:${input.requestId}`,
title: '有打手撤单申请待审核',
body: [
`订单:${input.workOrderNo || input.workOrderId}`,
`商品:${input.productName || '-'}`,
`打手:${input.workerName || '-'}`,
`原因:${input.reason || '-'}`,
].join(''),
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=cancel-requests&cancelRequestId=${input.requestId}`,
now: new Date().toISOString(),
})
}
export async function createWorkerOrderFeedbackAdminNotification(input: {
feedbackId: number
workOrderId: number
workOrderNo: string
productName: string
workerName: string
content: string
}) {
return createConfiguredWorkerPlatformNotification('feedback_submitted', {
notificationType: 'worker_order_feedback_submitted',
priority: 'high',
entityType: 'worker_order_feedback',
entityId: input.feedbackId,
dedupeKey: `worker_order_feedback:${input.feedbackId}`,
title: '有打手问题反馈待处理',
body: [
`订单:${input.workOrderNo || input.workOrderId}`,
`商品:${input.productName || '-'}`,
`打手:${input.workerName || '-'}`,
`反馈:${input.content || '-'}`,
].join(''),
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=feedbacks&feedbackId=${input.feedbackId}`,
now: new Date().toISOString(),
})
}
export async function remindWorkerAcceptanceAdminNotification(workOrderId: number) {
const now = new Date().toISOString()
const eventConfig = getWorkerPlatformNotificationConfig().events.acceptance_reminded
const notification = await bumpAdminNotificationReminder({
dedupeKey: acceptanceNotificationKey(workOrderId),
notificationType: 'worker_acceptance_reminded',
title: '有打手催验收',
visible: eventConfig.todoEnabled,
soundEnabled: eventConfig.todoEnabled && eventConfig.soundEnabled,
now,
})
if (notification && eventConfig.externalPushEnabled) {
void notifyInternalSafely({
title: '打手催促验收',
body: `${notification.body};第 ${notification.reminder_count} 次催验收`,
category: 'worker_acceptance_reminded',
url: notification.action_path,
cooldownKey: `worker_acceptance_reminded:${workOrderId}:${notification.reminder_count}`,
cooldownMs: 30 * 60 * 1000,
})
}
if (notification?.visible) {
publishRealtimeEvent({
type: 'admin_notification.changed',
entityId: Number(notification.id),
scopes: ['admin_notifications'],
operation: 'upsert',
data: mapAdminNotification(notification),
admin: true,
})
}
return notification
}
export async function getWorkerAcceptanceReminder(workOrderId: number) {
return getPendingAdminNotificationByDedupeKey(acceptanceNotificationKey(workOrderId))
}
export async function resolveAdminNotificationEntity(
entityType: string,
entityId: number,
reason: string,
) {
await resolveAdminNotificationsByEntity({
entityType,
entityId,
reason,
now: new Date().toISOString(),
})
publishRealtimeEvent({
type: 'admin_notification.changed',
entityId,
scopes: ['admin_notifications'],
operation: 'remove',
data: { notificationId: entityId },
admin: true,
})
}
export async function listAdminPendingNotifications(limit = 20) {
const normalizedLimit = Math.min(50, Math.max(1, Math.floor(Number(limit) || 20)))
const [items, pendingCount] = await Promise.all([
listPendingAdminNotifications(normalizedLimit),
countPendingAdminNotifications(),
])
return {
pendingCount,
items: items.map(mapAdminNotification),
}
}
export function mapAdminNotification(row: AdminNotificationRow) {
return {
notificationId: Number(row.id),
notificationType: row.notification_type,
priority: row.priority,
entityType: row.entity_type,
entityId: Number(row.entity_id),
title: row.title,
body: row.body,
actionPath: row.action_path,
reminderCount: Number(row.reminder_count || 0),
soundEnabled: row.sound_enabled !== false,
lastRemindedAt: row.last_reminded_at,
createdAt: row.created_at,
updatedAt: row.updated_at,
}
}
async function createConfiguredWorkerPlatformNotification(
eventKey: WorkerPlatformNotificationEventKey,
input: {
notificationType: string
priority: string
entityType: string
entityId: number
dedupeKey: string
title: string
body: string
actionPath: string
now: string
},
) {
const eventConfig = getWorkerPlatformNotificationConfig().events[eventKey]
const notification = await createAdminNotification({
...input,
visible: eventConfig.todoEnabled,
soundEnabled: eventConfig.todoEnabled && eventConfig.soundEnabled,
})
if (notification && eventConfig.externalPushEnabled) {
void notifyInternalSafely({
title: notification.title,
body: notification.body,
category: notification.notification_type,
url: notification.action_path,
cooldownKey: notification.dedupe_key,
})
}
if (notification?.visible) {
publishRealtimeEvent({
type: 'admin_notification.changed',
entityId: Number(notification.id),
scopes: ['admin_notifications'],
operation: 'upsert',
data: mapAdminNotification(notification),
admin: true,
})
}
return notification
}
function acceptanceNotificationKey(workOrderId: number) {
return `work_order_acceptance:${workOrderId}`
}
function formatAmount(amountFen: number) {
return `¥${(Math.max(0, Number(amountFen) || 0) / 100).toFixed(2)}`
}
function formatChannel(channel: string) {
return channel === 'wechat' ? '微信' : channel === 'alipay' ? '支付宝' : channel || '-'
}