refactor: centralize wechat notification dispatch
This commit is contained in:
+9
-4
@@ -21,6 +21,7 @@ import { createPlazaInteractionService } from './plaza-interactions.mjs';
|
||||
import { createPlazaOpsService } from './plaza-ops.mjs';
|
||||
import { createNoopPlazaRedis } from './plaza-redis.mjs';
|
||||
import { ensureAlgorithmConfig, loadAlgorithmConfig } from './plaza-algorithm.mjs';
|
||||
import { createNotificationDispatcher } from './notification-dispatcher.mjs';
|
||||
import { createWechatAdminService } from './wechat-admin.mjs';
|
||||
import { createWechatMpService, loadWechatMpConfig } from './wechat-mp.mjs';
|
||||
|
||||
@@ -98,16 +99,20 @@ export async function createAdminServices(env = {}) {
|
||||
throw new Error('admin bootstrap does not proxy WeChat chat sessions');
|
||||
},
|
||||
});
|
||||
userAuth.setRechargeNotifier(async ({ userId, title, body }) => {
|
||||
if (!wechatMpService?.enabled) return;
|
||||
await wechatMpService.sendTextToUser(userId, `${title}\n${body}`.trim());
|
||||
const notificationDispatcher = createNotificationDispatcher({
|
||||
sendWechatTextToUser: wechatMpService?.enabled
|
||||
? (userId, text) => wechatMpService.sendTextToUser(userId, text)
|
||||
: null,
|
||||
});
|
||||
userAuth.setRechargeNotifier(async ({ userId, title, body, dedupeKey }) => {
|
||||
await notificationDispatcher.sendRechargeSuccess({ userId, title, body, dedupeKey });
|
||||
});
|
||||
const wechatAdmin = createWechatAdminService(pool, {
|
||||
config: wechatMpConfig,
|
||||
scheduleEnabled: process.env.H5_SCHEDULE_ENABLED === '1',
|
||||
reminderWorkerEnabled: process.env.H5_REMINDER_WORKER_ENABLED === '1',
|
||||
sendWechatTextToUser: wechatMpService?.enabled
|
||||
? (userId, text) => wechatMpService.sendTextToUser(userId, text)
|
||||
? (userId, text) => notificationDispatcher.sendScheduleNotification({ userId, text })
|
||||
: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export function createNotificationDispatcher({ sendWechatTextToUser, logger = console } = {}) {
|
||||
const sendWechat = async (userId, text) => {
|
||||
if (typeof sendWechatTextToUser !== 'function') return false;
|
||||
await sendWechatTextToUser(userId, text);
|
||||
return true;
|
||||
};
|
||||
|
||||
return {
|
||||
async sendRechargeSuccess({ userId, title, body, dedupeKey = null }) {
|
||||
const sent = await sendWechat(userId, `${title}\n${body}`.trim());
|
||||
logger.info?.('Notification dispatch:', {
|
||||
type: 'recharge_success',
|
||||
userId,
|
||||
dedupeKey,
|
||||
sent,
|
||||
});
|
||||
return sent;
|
||||
},
|
||||
async sendScheduleNotification({ userId, text }) {
|
||||
const sent = await sendWechat(userId, text);
|
||||
logger.info?.('Notification dispatch:', {
|
||||
type: 'schedule_notification',
|
||||
userId,
|
||||
dedupeKey: null,
|
||||
sent,
|
||||
});
|
||||
return sent;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createNotificationDispatcher } from './notification-dispatcher.mjs';
|
||||
|
||||
test('notification dispatcher forwards recharge success text unchanged', async () => {
|
||||
const sent = [];
|
||||
const dedupeKeys = [];
|
||||
const logs = [];
|
||||
const dispatcher = createNotificationDispatcher({
|
||||
async sendWechatTextToUser(userId, text) {
|
||||
sent.push({ userId, text });
|
||||
},
|
||||
logger: {
|
||||
info(message, payload) {
|
||||
logs.push({ message, payload });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sentOk = await dispatcher.sendRechargeSuccess({
|
||||
userId: 'user-1',
|
||||
title: '充值成功',
|
||||
body: '你已成功充值 ¥25.00,余额已更新。',
|
||||
dedupeKey: (dedupeKeys.push('recharge:order-1'), 'recharge:order-1'),
|
||||
});
|
||||
|
||||
assert.equal(sentOk, true);
|
||||
assert.deepEqual(dedupeKeys, ['recharge:order-1']);
|
||||
assert.deepEqual(sent, [
|
||||
{ userId: 'user-1', text: '充值成功\n你已成功充值 ¥25.00,余额已更新。' },
|
||||
]);
|
||||
assert.deepEqual(logs, [
|
||||
{
|
||||
message: 'Notification dispatch:',
|
||||
payload: {
|
||||
type: 'recharge_success',
|
||||
userId: 'user-1',
|
||||
dedupeKey: 'recharge:order-1',
|
||||
sent: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('notification dispatcher forwards schedule notification text unchanged', async () => {
|
||||
const sent = [];
|
||||
const logs = [];
|
||||
const dispatcher = createNotificationDispatcher({
|
||||
async sendWechatTextToUser(userId, text) {
|
||||
sent.push({ userId, text });
|
||||
},
|
||||
logger: {
|
||||
info(message, payload) {
|
||||
logs.push({ message, payload });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sentOk = await dispatcher.sendScheduleNotification({
|
||||
userId: 'user-2',
|
||||
text: '【待办提醒】开会\n提醒时间:09:00',
|
||||
});
|
||||
|
||||
assert.equal(sentOk, true);
|
||||
assert.deepEqual(sent, [
|
||||
{ userId: 'user-2', text: '【待办提醒】开会\n提醒时间:09:00' },
|
||||
]);
|
||||
assert.deepEqual(logs, [
|
||||
{
|
||||
message: 'Notification dispatch:',
|
||||
payload: {
|
||||
type: 'schedule_notification',
|
||||
userId: 'user-2',
|
||||
dedupeKey: null,
|
||||
sent: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('notification dispatcher returns false when wechat sender is unavailable', async () => {
|
||||
const logs = [];
|
||||
const dispatcher = createNotificationDispatcher({
|
||||
logger: {
|
||||
info(message, payload) {
|
||||
logs.push({ message, payload });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
await dispatcher.sendRechargeSuccess({
|
||||
userId: 'user-3',
|
||||
title: '充值成功',
|
||||
body: '余额已更新。',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
await dispatcher.sendScheduleNotification({
|
||||
userId: 'user-3',
|
||||
text: '【待办提醒】开会',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(logs, [
|
||||
{
|
||||
message: 'Notification dispatch:',
|
||||
payload: {
|
||||
type: 'recharge_success',
|
||||
userId: 'user-3',
|
||||
dedupeKey: null,
|
||||
sent: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Notification dispatch:',
|
||||
payload: {
|
||||
type: 'schedule_notification',
|
||||
userId: 'user-3',
|
||||
dedupeKey: null,
|
||||
sent: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -1,12 +1,18 @@
|
||||
export function startScheduleReminderWorker({
|
||||
scheduleService,
|
||||
sendWechatTextToUser,
|
||||
notificationDispatcher,
|
||||
logger = console,
|
||||
intervalMs = Number(process.env.H5_REMINDER_SCAN_INTERVAL_MS ?? 30_000),
|
||||
maxAttempts = Number(process.env.H5_REMINDER_MAX_ATTEMPTS ?? 5),
|
||||
runOnStart = true,
|
||||
} = {}) {
|
||||
if (!scheduleService || typeof sendWechatTextToUser !== 'function') {
|
||||
const sendScheduleNotification =
|
||||
notificationDispatcher?.sendScheduleNotification?.bind(notificationDispatcher) ??
|
||||
(typeof sendWechatTextToUser === 'function'
|
||||
? async ({ userId, text }) => sendWechatTextToUser(userId, text)
|
||||
: null);
|
||||
if (!scheduleService || typeof sendScheduleNotification !== 'function') {
|
||||
return { stop() {} };
|
||||
}
|
||||
|
||||
@@ -39,7 +45,7 @@ export function startScheduleReminderWorker({
|
||||
},
|
||||
});
|
||||
if (reminder.channel === 'wechat') {
|
||||
await sendWechatTextToUser(reminder.userId, text);
|
||||
await sendScheduleNotification({ userId: reminder.userId, text });
|
||||
}
|
||||
await scheduleService.logDelivery({
|
||||
reminderId: reminder.id,
|
||||
@@ -81,7 +87,7 @@ export function startScheduleReminderWorker({
|
||||
timezone: subscription.timezone,
|
||||
},
|
||||
});
|
||||
await sendWechatTextToUser(subscription.userId, text);
|
||||
await sendScheduleNotification({ userId: subscription.userId, text });
|
||||
await scheduleService.logDelivery({
|
||||
subscriptionId: subscription.id,
|
||||
userId: subscription.userId,
|
||||
@@ -128,7 +134,7 @@ export function startScheduleReminderWorker({
|
||||
balanceCents,
|
||||
},
|
||||
});
|
||||
await sendWechatTextToUser(subscription.userId, text);
|
||||
await sendScheduleNotification({ userId: subscription.userId, text });
|
||||
await scheduleService.logDelivery({
|
||||
subscriptionId: subscription.id,
|
||||
userId: subscription.userId,
|
||||
|
||||
+10
-4
@@ -34,6 +34,7 @@ import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID, PUBLIC_ZONE_DIR, resolvePublishDir,
|
||||
import { ensureWorkspaceHtmlThumbnail, startWorkspaceThumbnailWatcher, workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
|
||||
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
|
||||
import { attachRequestId, sendData, sendError } from './api-response.mjs';
|
||||
import { createNotificationDispatcher } from './notification-dispatcher.mjs';
|
||||
import { createMindSpaceAuditWriter } from './mindspace-audit.mjs';
|
||||
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
|
||||
import { createMindSpaceService, DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
|
||||
@@ -258,6 +259,7 @@ let subscriptionService = null;
|
||||
let wechatPayClient = null;
|
||||
let wechatOAuthService = null;
|
||||
let wechatMpService = null;
|
||||
let notificationDispatcher = null;
|
||||
let scheduleService = null;
|
||||
let feedbackService = null;
|
||||
let scheduleReminderWorker = null;
|
||||
@@ -592,9 +594,13 @@ async function bootstrapUserAuth() {
|
||||
})
|
||||
: null,
|
||||
});
|
||||
userAuth.setRechargeNotifier(async ({ userId, title, body }) => {
|
||||
if (!wechatMpService?.enabled) return;
|
||||
await wechatMpService.sendTextToUser(userId, `${title}\n${body}`.trim());
|
||||
notificationDispatcher = createNotificationDispatcher({
|
||||
sendWechatTextToUser: wechatMpService?.enabled
|
||||
? (userId, text) => wechatMpService.sendTextToUser(userId, text)
|
||||
: null,
|
||||
});
|
||||
userAuth.setRechargeNotifier(async ({ userId, title, body, dedupeKey }) => {
|
||||
await notificationDispatcher.sendRechargeSuccess({ userId, title, body, dedupeKey });
|
||||
});
|
||||
if (
|
||||
process.env.H5_REMINDER_WORKER_ENABLED === '1' &&
|
||||
@@ -603,7 +609,7 @@ async function bootstrapUserAuth() {
|
||||
) {
|
||||
scheduleReminderWorker = startScheduleReminderWorker({
|
||||
scheduleService,
|
||||
sendWechatTextToUser: (userId, text) => wechatMpService.sendTextToUser(userId, text),
|
||||
notificationDispatcher,
|
||||
});
|
||||
console.log('Schedule reminder worker enabled');
|
||||
}
|
||||
|
||||
@@ -1215,6 +1215,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
amountCents: amount,
|
||||
operatorId: operatorId ?? null,
|
||||
paymentOrderId,
|
||||
dedupeKey: paymentOrderId ? `recharge:${paymentOrderId}` : null,
|
||||
notificationType: rechargeType,
|
||||
title,
|
||||
body,
|
||||
|
||||
Reference in New Issue
Block a user