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 { createPlazaOpsService } from './plaza-ops.mjs';
|
||||||
import { createNoopPlazaRedis } from './plaza-redis.mjs';
|
import { createNoopPlazaRedis } from './plaza-redis.mjs';
|
||||||
import { ensureAlgorithmConfig, loadAlgorithmConfig } from './plaza-algorithm.mjs';
|
import { ensureAlgorithmConfig, loadAlgorithmConfig } from './plaza-algorithm.mjs';
|
||||||
|
import { createNotificationDispatcher } from './notification-dispatcher.mjs';
|
||||||
import { createWechatAdminService } from './wechat-admin.mjs';
|
import { createWechatAdminService } from './wechat-admin.mjs';
|
||||||
import { createWechatMpService, loadWechatMpConfig } from './wechat-mp.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');
|
throw new Error('admin bootstrap does not proxy WeChat chat sessions');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
userAuth.setRechargeNotifier(async ({ userId, title, body }) => {
|
const notificationDispatcher = createNotificationDispatcher({
|
||||||
if (!wechatMpService?.enabled) return;
|
sendWechatTextToUser: wechatMpService?.enabled
|
||||||
await wechatMpService.sendTextToUser(userId, `${title}\n${body}`.trim());
|
? (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, {
|
const wechatAdmin = createWechatAdminService(pool, {
|
||||||
config: wechatMpConfig,
|
config: wechatMpConfig,
|
||||||
scheduleEnabled: process.env.H5_SCHEDULE_ENABLED === '1',
|
scheduleEnabled: process.env.H5_SCHEDULE_ENABLED === '1',
|
||||||
reminderWorkerEnabled: process.env.H5_REMINDER_WORKER_ENABLED === '1',
|
reminderWorkerEnabled: process.env.H5_REMINDER_WORKER_ENABLED === '1',
|
||||||
sendWechatTextToUser: wechatMpService?.enabled
|
sendWechatTextToUser: wechatMpService?.enabled
|
||||||
? (userId, text) => wechatMpService.sendTextToUser(userId, text)
|
? (userId, text) => notificationDispatcher.sendScheduleNotification({ userId, text })
|
||||||
: null,
|
: 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({
|
export function startScheduleReminderWorker({
|
||||||
scheduleService,
|
scheduleService,
|
||||||
sendWechatTextToUser,
|
sendWechatTextToUser,
|
||||||
|
notificationDispatcher,
|
||||||
logger = console,
|
logger = console,
|
||||||
intervalMs = Number(process.env.H5_REMINDER_SCAN_INTERVAL_MS ?? 30_000),
|
intervalMs = Number(process.env.H5_REMINDER_SCAN_INTERVAL_MS ?? 30_000),
|
||||||
maxAttempts = Number(process.env.H5_REMINDER_MAX_ATTEMPTS ?? 5),
|
maxAttempts = Number(process.env.H5_REMINDER_MAX_ATTEMPTS ?? 5),
|
||||||
runOnStart = true,
|
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() {} };
|
return { stop() {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +45,7 @@ export function startScheduleReminderWorker({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (reminder.channel === 'wechat') {
|
if (reminder.channel === 'wechat') {
|
||||||
await sendWechatTextToUser(reminder.userId, text);
|
await sendScheduleNotification({ userId: reminder.userId, text });
|
||||||
}
|
}
|
||||||
await scheduleService.logDelivery({
|
await scheduleService.logDelivery({
|
||||||
reminderId: reminder.id,
|
reminderId: reminder.id,
|
||||||
@@ -81,7 +87,7 @@ export function startScheduleReminderWorker({
|
|||||||
timezone: subscription.timezone,
|
timezone: subscription.timezone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await sendWechatTextToUser(subscription.userId, text);
|
await sendScheduleNotification({ userId: subscription.userId, text });
|
||||||
await scheduleService.logDelivery({
|
await scheduleService.logDelivery({
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
userId: subscription.userId,
|
userId: subscription.userId,
|
||||||
@@ -128,7 +134,7 @@ export function startScheduleReminderWorker({
|
|||||||
balanceCents,
|
balanceCents,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await sendWechatTextToUser(subscription.userId, text);
|
await sendScheduleNotification({ userId: subscription.userId, text });
|
||||||
await scheduleService.logDelivery({
|
await scheduleService.logDelivery({
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
userId: subscription.userId,
|
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 { ensureWorkspaceHtmlThumbnail, startWorkspaceThumbnailWatcher, workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
|
||||||
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
|
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
|
||||||
import { attachRequestId, sendData, sendError } from './api-response.mjs';
|
import { attachRequestId, sendData, sendError } from './api-response.mjs';
|
||||||
|
import { createNotificationDispatcher } from './notification-dispatcher.mjs';
|
||||||
import { createMindSpaceAuditWriter } from './mindspace-audit.mjs';
|
import { createMindSpaceAuditWriter } from './mindspace-audit.mjs';
|
||||||
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
|
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
|
||||||
import { createMindSpaceService, DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
|
import { createMindSpaceService, DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
|
||||||
@@ -258,6 +259,7 @@ let subscriptionService = null;
|
|||||||
let wechatPayClient = null;
|
let wechatPayClient = null;
|
||||||
let wechatOAuthService = null;
|
let wechatOAuthService = null;
|
||||||
let wechatMpService = null;
|
let wechatMpService = null;
|
||||||
|
let notificationDispatcher = null;
|
||||||
let scheduleService = null;
|
let scheduleService = null;
|
||||||
let feedbackService = null;
|
let feedbackService = null;
|
||||||
let scheduleReminderWorker = null;
|
let scheduleReminderWorker = null;
|
||||||
@@ -592,9 +594,13 @@ async function bootstrapUserAuth() {
|
|||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
userAuth.setRechargeNotifier(async ({ userId, title, body }) => {
|
notificationDispatcher = createNotificationDispatcher({
|
||||||
if (!wechatMpService?.enabled) return;
|
sendWechatTextToUser: wechatMpService?.enabled
|
||||||
await wechatMpService.sendTextToUser(userId, `${title}\n${body}`.trim());
|
? (userId, text) => wechatMpService.sendTextToUser(userId, text)
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
userAuth.setRechargeNotifier(async ({ userId, title, body, dedupeKey }) => {
|
||||||
|
await notificationDispatcher.sendRechargeSuccess({ userId, title, body, dedupeKey });
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
process.env.H5_REMINDER_WORKER_ENABLED === '1' &&
|
process.env.H5_REMINDER_WORKER_ENABLED === '1' &&
|
||||||
@@ -603,7 +609,7 @@ async function bootstrapUserAuth() {
|
|||||||
) {
|
) {
|
||||||
scheduleReminderWorker = startScheduleReminderWorker({
|
scheduleReminderWorker = startScheduleReminderWorker({
|
||||||
scheduleService,
|
scheduleService,
|
||||||
sendWechatTextToUser: (userId, text) => wechatMpService.sendTextToUser(userId, text),
|
notificationDispatcher,
|
||||||
});
|
});
|
||||||
console.log('Schedule reminder worker enabled');
|
console.log('Schedule reminder worker enabled');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1215,6 +1215,7 @@ export function createUserAuth(pool, options = {}) {
|
|||||||
amountCents: amount,
|
amountCents: amount,
|
||||||
operatorId: operatorId ?? null,
|
operatorId: operatorId ?? null,
|
||||||
paymentOrderId,
|
paymentOrderId,
|
||||||
|
dedupeKey: paymentOrderId ? `recharge:${paymentOrderId}` : null,
|
||||||
notificationType: rechargeType,
|
notificationType: rechargeType,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
|
|||||||
Reference in New Issue
Block a user