293 lines
9.5 KiB
TypeScript
293 lines
9.5 KiB
TypeScript
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 || '-'
|
||
}
|