diff --git a/es/entities/Notification.d.ts b/es/entities/Notification.d.ts index 3ee84e523..52756721a 100644 --- a/es/entities/Notification.d.ts +++ b/es/entities/Notification.d.ts @@ -1,11 +1,10 @@ import { String } from 'oak-domain/lib/types/DataType'; import { EntityShape } from 'oak-domain/lib/types/Entity'; -import { Channel } from '../types/Message'; import { Schema as Application } from './Application'; import { Schema as MessageSystem } from './MessageSystem'; import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; export interface Schema extends EntityShape { - channel: Channel; + channel: String<32>; application?: Application; data?: Object; messageSystem: MessageSystem; @@ -18,6 +17,5 @@ export type IState = 'sending' | 'success' | 'failure'; type Action = IAction; export declare const entityDesc: EntityDesc; export {}; diff --git a/es/entities/Notification.js b/es/entities/Notification.js index d5fba7440..12d821a67 100644 --- a/es/entities/Notification.js +++ b/es/entities/Notification.js @@ -30,14 +30,14 @@ export const entityDesc = { success: '发送成功', failure: '发送失败', }, - channel: { - wechatPublic: '公众号', - jPush: '极光推送', - jim: '极光消息', - wechatMp: '小程序', - sms: '短信', - email: '邮箱', - } + // channel: { + // wechatPublic: '公众号', + // jPush: '极光推送', + // jim: '极光消息', + // wechatMp: '小程序', + // sms: '短信', + // email: '邮箱', + // } } }, }, @@ -52,14 +52,14 @@ export const entityDesc = { success: '#008000', failure: '#9A9A9A', }, - channel: { - wechatMp: '#008000', - jPush: '#0000FF', - jim: '#0000FF', - wechatPublic: '#008000', - sms: '#000000', - email: '#000000', - }, + // channel: { + // wechatMp: '#008000', + // jPush: '#0000FF', + // jim: '#0000FF', + // wechatPublic: '#008000', + // sms: '#000000', + // email: '#000000', + // }, } } }; diff --git a/es/oak-app-domain/Notification/Storage.js b/es/oak-app-domain/Notification/Storage.js index 8417d9dd1..aa36b7ec4 100644 --- a/es/oak-app-domain/Notification/Storage.js +++ b/es/oak-app-domain/Notification/Storage.js @@ -3,8 +3,10 @@ export const desc = { attributes: { channel: { notNull: true, - type: "enum", - enumeration: ["wechatPublic", "jPush", "jim", "wechatMp", "sms", "email"] + type: "varchar", + params: { + length: 32 + } }, applicationId: { type: "ref", diff --git a/es/oak-app-domain/Notification/Style.js b/es/oak-app-domain/Notification/Style.js index 0fab04e8e..4011ec013 100644 --- a/es/oak-app-domain/Notification/Style.js +++ b/es/oak-app-domain/Notification/Style.js @@ -9,13 +9,13 @@ export const style = { success: '#008000', failure: '#9A9A9A', }, - channel: { - wechatMp: '#008000', - jPush: '#0000FF', - jim: '#0000FF', - wechatPublic: '#008000', - sms: '#000000', - email: '#000000', - }, + // channel: { + // wechatMp: '#008000', + // jPush: '#0000FF', + // jim: '#0000FF', + // wechatPublic: '#008000', + // sms: '#000000', + // email: '#000000', + // }, } }; diff --git a/es/oak-app-domain/Notification/_baseSchema.d.ts b/es/oak-app-domain/Notification/_baseSchema.d.ts index c20eeee3c..b67187071 100644 --- a/es/oak-app-domain/Notification/_baseSchema.d.ts +++ b/es/oak-app-domain/Notification/_baseSchema.d.ts @@ -2,10 +2,9 @@ import { ForeignKey } from "oak-domain/lib/types/DataType"; import { Q_DateValue, Q_NumberValue, Q_StringValue, Q_EnumValue, NodeId, ExprOp, ExpressionKey } from "oak-domain/lib/types/Demand"; import { MakeAction as OakMakeAction, EntityShape } from "oak-domain/lib/types/Entity"; import { Action, ParticularAction, IState } from "./Action"; -import { Channel } from "../../types/Message"; import { String } from "oak-domain/lib/types/DataType"; export type OpSchema = EntityShape & { - channel: Channel; + channel: String<32>; applicationId?: ForeignKey<"application"> | null; data?: Object | null; messageSystemId: ForeignKey<"messageSystem">; @@ -22,7 +21,7 @@ export type OpFilter = { $$createAt$$: Q_DateValue; $$seq$$: Q_NumberValue; $$updateAt$$: Q_DateValue; - channel: Q_EnumValue; + channel: Q_StringValue; applicationId: Q_StringValue; data: Object; messageSystemId: Q_StringValue; diff --git a/es/oak-app-domain/Notification/locales/zh_CN.json b/es/oak-app-domain/Notification/locales/zh_CN.json index 29695c575..19bbb24dc 100644 --- a/es/oak-app-domain/Notification/locales/zh_CN.json +++ b/es/oak-app-domain/Notification/locales/zh_CN.json @@ -19,14 +19,6 @@ "sending": "发送中", "success": "发送成功", "failure": "发送失败" - }, - "channel": { - "wechatPublic": "公众号", - "jPush": "极光推送", - "jim": "极光消息", - "wechatMp": "小程序", - "sms": "短信", - "email": "邮箱" } } } diff --git a/es/oak-app-domain/UserEntityGrant/Storage.js b/es/oak-app-domain/UserEntityGrant/Storage.js index 80a9a33c8..df524f632 100644 --- a/es/oak-app-domain/UserEntityGrant/Storage.js +++ b/es/oak-app-domain/UserEntityGrant/Storage.js @@ -59,7 +59,7 @@ export const desc = { qrCodeType: { notNull: true, type: "enum", - enumeration: ["wechatPublic", "wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublicForMp", "webForWechatPublic"] + enumeration: ["wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublic", "wechatPublicForMp", "webForWechatPublic"] }, expiresAt: { type: "datetime" diff --git a/es/oak-app-domain/WechatLogin/Storage.js b/es/oak-app-domain/WechatLogin/Storage.js index edbaa0c52..22576416a 100644 --- a/es/oak-app-domain/WechatLogin/Storage.js +++ b/es/oak-app-domain/WechatLogin/Storage.js @@ -20,7 +20,7 @@ export const desc = { qrCodeType: { notNull: true, type: "enum", - enumeration: ["wechatPublic", "wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublicForMp", "webForWechatPublic"] + enumeration: ["wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublic", "wechatPublicForMp", "webForWechatPublic"] }, expiresAt: { type: "datetime" diff --git a/es/oak-app-domain/WechatQrCode/Storage.js b/es/oak-app-domain/WechatQrCode/Storage.js index 81ec43f54..e64151319 100644 --- a/es/oak-app-domain/WechatQrCode/Storage.js +++ b/es/oak-app-domain/WechatQrCode/Storage.js @@ -19,7 +19,7 @@ export const desc = { type: { notNull: true, type: "enum", - enumeration: ["wechatPublic", "wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublicForMp", "webForWechatPublic"] + enumeration: ["wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublic", "wechatPublicForMp", "webForWechatPublic"] }, allowShare: { notNull: true, diff --git a/es/registry.backend.d.ts b/es/registry.backend.d.ts index 50b7aea24..77cb0612d 100644 --- a/es/registry.backend.d.ts +++ b/es/registry.backend.d.ts @@ -8,6 +8,32 @@ export { */ registerMessageType, } from './aspects/template'; export { registerWeChatPublicEventCallback, } from './endpoints/wechat'; -export { registerMessageNotificationConverters, registerMessageHandler, } from './utils/message'; +export { +/** + * 注册消息通知转换器 + * 用于将消息数据转换为特定渠道所需的格式 + * 例如: 将message转换为微信小程序、公众号、短信、邮件所需的数据格式 + */ +registerMessageNotificationConverters, +/** + * 注册消息渠道处理器 + * 用于处理特定渠道的消息创建逻辑 + * 例如: 处理微信小程序、公众号、短信、邮件等渠道的消息生成 + */ +registerMessageHandler, } from './utils/message'; +export { +/** + * 注册通知渠道处理器 + * 用于处理特定渠道的通知发送逻辑 + * 例如: 实际发送微信小程序、公众号、短信、邮件等渠道的通知 + */ +registerNotificationHandler, +/** + * 注册通知失败处理器 + * 用于在所有通知渠道都失败后执行自定义的补救逻辑 + * 可以注册多个处理器,它们会依次执行 + * 例如: 在其他渠道失败后自动发送短信通知 + */ +registerNotificationFailureHandler, } from './utils/notification'; export { registSms, } from './utils/sms'; export { registerCosBackend, } from './utils/cos/index.backend'; diff --git a/es/registry.backend.js b/es/registry.backend.js index 98cb2d3a5..79e4e68f7 100644 --- a/es/registry.backend.js +++ b/es/registry.backend.js @@ -11,8 +11,32 @@ export { // 注册微信事件回调处理器endpoint registerWeChatPublicEventCallback, } from './endpoints/wechat'; export { -// 注册消息通知转换器trigger -registerMessageNotificationConverters, registerMessageHandler, } from './utils/message'; +/** + * 注册消息通知转换器 + * 用于将消息数据转换为特定渠道所需的格式 + * 例如: 将message转换为微信小程序、公众号、短信、邮件所需的数据格式 + */ +registerMessageNotificationConverters, +/** + * 注册消息渠道处理器 + * 用于处理特定渠道的消息创建逻辑 + * 例如: 处理微信小程序、公众号、短信、邮件等渠道的消息生成 + */ +registerMessageHandler, } from './utils/message'; +export { +/** + * 注册通知渠道处理器 + * 用于处理特定渠道的通知发送逻辑 + * 例如: 实际发送微信小程序、公众号、短信、邮件等渠道的通知 + */ +registerNotificationHandler, +/** + * 注册通知失败处理器 + * 用于在所有通知渠道都失败后执行自定义的补救逻辑 + * 可以注册多个处理器,它们会依次执行 + * 例如: 在其他渠道失败后自动发送短信通知 + */ +registerNotificationFailureHandler, } from './utils/notification'; export { // 注册短信服务商实现 registSms, } from './utils/sms'; diff --git a/es/triggers/index.d.ts b/es/triggers/index.d.ts index fbd2c14a3..5e0cf60ac 100644 --- a/es/triggers/index.d.ts +++ b/es/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/es/triggers/message.d.ts b/es/triggers/message.d.ts index d7b744870..f228e2959 100644 --- a/es/triggers/message.d.ts +++ b/es/triggers/message.d.ts @@ -1,25 +1,5 @@ import { Trigger } from 'oak-domain/lib/types/Trigger'; import { EntityDict } from '../oak-app-domain/EntityDict'; import { BRC } from '../types/RuntimeCxt'; -import { BackendRuntimeContext } from '../context/BackendRuntimeContext'; -import { Router } from '../entities/Message'; -export declare function tryMakeSmsNotification(message: { - userId?: string; - type?: string; - entity?: string; - router?: Router | null; - entityId?: string; -}, context: BackendRuntimeContext): Promise; -export declare function tryMakeEmailNotification(message: { - userId?: string; - type?: string; - entity?: string; - router?: Router | null; - entityId?: string; -}, context: BackendRuntimeContext): Promise<{ - id: string; - data: import("../types/Email").EmailOptions; - channel: string; -} | undefined>; declare const triggers: Trigger>[]; export default triggers; diff --git a/es/triggers/message.js b/es/triggers/message.js index 671559f7d..a4dbb3370 100644 --- a/es/triggers/message.js +++ b/es/triggers/message.js @@ -1,55 +1,12 @@ import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import { assert } from 'oak-domain/lib/utils/assert'; import { uniqBy } from 'oak-domain/lib/utils/lodash'; -import { ConverterDict, getMessageHandler } from '../utils/message'; +import { getMessageHandler } from '../utils/message'; const InitialChannelByWeightMatrix = { high: ['wechatMp', 'wechatPublic', 'sms', 'email'], medium: ['wechatMp', 'wechatPublic', 'email'], low: ['wechatMp', 'wechatPublic', 'email'], }; -export async function tryMakeSmsNotification(message, context) { - const { userId, type, entity, entityId, router } = message; - assert(userId); - const [mobile] = await context.select('mobile', { - data: { - id: 1, - mobile: 1, - }, - filter: { - userId, - }, - indexFrom: 0, - count: 1, - }, { dontCollect: true }); - if (mobile) { - const converter = ConverterDict[type] && ConverterDict[type].toSms; - if (converter) { - const dispersedData = await converter(message, context); - if (dispersedData) { - return { - id: await generateNewIdAsync(), - data: dispersedData, - channel: 'sms', - data1: mobile, - }; - } - } - } -} -export async function tryMakeEmailNotification(message, context) { - const { userId, type, entity, entityId, router } = message; - const converter = ConverterDict[type] && ConverterDict[type].toEmail; - if (converter) { - const dispersedData = await converter(message, context); - if (dispersedData) { - return { - id: await generateNewIdAsync(), - data: dispersedData, - channel: 'email', - }; - } - } -} async function createNotification(message, context) { const { restriction, userId, weight, type, entity, entityId, platformId, channels } = message; assert(userId); diff --git a/es/triggers/notification.js b/es/triggers/notification.js index c5859ffeb..f67eafda0 100644 --- a/es/triggers/notification.js +++ b/es/triggers/notification.js @@ -1,264 +1,12 @@ import { assert } from 'oak-domain/lib/utils/assert'; -import WechatSDK from 'oak-external-sdk/lib/WechatSDK'; -import { composeUrl } from 'oak-domain/lib/utils/domain'; import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; -import { sendSms } from '../utils/sms'; -import { sendEmail } from '../utils/email'; -import { tryMakeSmsNotification } from './message'; -import { composeDomainUrl } from '../utils/domain'; +import { tryMakeSmsNotification } from '../utils/message/sms'; +import { getNotificationHandler, getNotificationFailureHandlers } from '../utils/notification'; async function sendNotification(notification, context) { - const { data, templateId, channel, messageSystemId, data1, id } = notification; - const [messageSystem] = await context.select('messageSystem', { - data: { - id: 1, - messageId: 1, - message: { - id: 1, - userId: 1, - router: 1, - type: 1, - }, - system: { - id: 1, - application$system: { - $entity: 'application', - data: { - id: 1, - type: 1, - config: 1, - }, - }, - } - }, - filter: { - id: messageSystemId, - } - }, { dontCollect: true }); - const { system, message } = messageSystem; - const { router, userId, type } = message; - const { application$system: applications, config } = system; - switch (channel) { - case 'wechatMp': { - const app = applications.find(ele => ele.type === 'wechatMp'); - const { config } = app; - const { appId, appSecret } = config; - const instance = WechatSDK.getInstance(appId, 'wechatMp', appSecret); - let page; - if (router) { - const pathname = router.pathname; - const url = pathname.startsWith('/') - ? `pages${pathname}/index` - : `pages/${pathname}/index`; - page = composeUrl(url, Object.assign({}, router.props, router.state)); - } - // 根据当前环境决定消息推哪个版本 - const StateDict = { - 'development': 'developer', - 'staging': 'trial', - 'production': 'former', - }; - try { - await instance.sendSubscribedMessage({ - templateId: templateId, - data: data, - openId: data1.openId, // 在notification创建时就赋值了 - page, - state: StateDict[process.env.NODE_ENV], - }); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'succeed', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - catch (err) { - console.warn('发微信小程序消息失败', err); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - } - case 'wechatPublic': { - const app = applications.find(ele => ele.type === 'wechatPublic'); - const { config, id: applicationId } = app; - const { appId, appSecret } = config; - const [domain] = await context.select('domain', { - data: { - id: 1, - url: 1, - apiPath: 1, - protocol: 1, - port: 1, - }, - filter: { - system: { - application$system: { - id: applicationId, - }, - }, - }, - }, { dontCollect: true }); - const instance = WechatSDK.getInstance(appId, 'wechatPublic', appSecret); - const { openId, wechatMpAppId } = data1; - let page; - // message 用户不需要跳转页面 - if (router) { - const pathname = router.pathname; - if (wechatMpAppId) { - const url = pathname.startsWith('/') - ? `pages${pathname}/index` - : `pages/${pathname}/index`; - page = composeUrl(url, Object.assign({}, router.props, router.state)); - } - else { - const url = composeDomainUrl(domain, pathname); - page = composeUrl(url, Object.assign({}, router.props, router.state)); - } - } - try { - await instance.sendTemplateMessage({ - openId, - templateId: templateId, - url: !wechatMpAppId ? page : undefined, - data: data, - miniProgram: wechatMpAppId - ? { - appid: wechatMpAppId, - pagepath: page, - } - : undefined, - clientMsgId: id, - }); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'succeed', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - catch (err) { - console.warn('发微信公众号消息失败', err); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - } - case 'email': { - try { - const result = await sendEmail(data, context); - if (result?.success) { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'succeed', - data: {}, - filter: { - id, - }, - }, { dontCollect: true }); - } - else { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: { - data2: { - res: result?.error, - }, - }, - filter: { - id, - }, - }, { dontCollect: true }); - } - return 1; - } - catch (err) { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: { - data2: { - res: err?.message, - }, - }, - filter: { - id, - }, - }, { dontCollect: true }); - console.warn('发邮件消息失败', err); - return 1; - } - } - default: { - assert(channel === 'sms'); - try { - const result = await sendSms({ - messageType: type, - templateParam: data.params, - mobile: data1.mobile, - }, context); - if (result?.success === true) { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'succeed', - data: { - data2: { - res: result?.res || {} - } - }, - filter: { - id, - } - }, { dontCollect: true }); - } - else { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: { - data2: { - res: result?.res || {} - } - }, - filter: { - id, - } - }, { dontCollect: true }); - } - } - catch (err) { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - console.warn('发短信消息失败', err); - return 1; - } - } - } + const { channel } = notification; + const handler = getNotificationHandler(channel); + await handler(notification, context); + return 1; } async function tryCreateSmsNotification(message, context) { const smsNotification = await tryMakeSmsNotification(message, context); @@ -415,8 +163,27 @@ const triggers = [ closeRootMode(); return 1; } - if (message.weight === 'medium' && !smsTried && allFailed) { - // 中级的消息,在其它途径都失败的情况下再发短信 + // 获取所有注册的失败处理器 + const failureHandlers = getNotificationFailureHandlers(); + if (failureHandlers.length > 0) { + // 如果有注册的失败处理器,执行所有处理器 + let totalResult = 0; + for (const handler of failureHandlers) { + try { + const result = await handler(message, context); + totalResult += result; + } + catch (err) { + console.error('执行notification失败处理器时出错:', err); + } + } + if (totalResult > 0) { + closeRootMode(); + return totalResult; + } + } + else if (message.weight === 'medium' && !smsTried && allFailed) { + // 如果没有注册处理器,走原有逻辑:中级的消息,在其它途径都失败的情况下再发短信 const result = await tryCreateSmsNotification(message, context); closeRootMode(); return result; diff --git a/es/utils/message/email.d.ts b/es/utils/message/email.d.ts index 608ce29bc..35e34928e 100644 --- a/es/utils/message/email.d.ts +++ b/es/utils/message/email.d.ts @@ -1,2 +1,16 @@ +import BackendRuntimeContext from '../../context/BackendRuntimeContext'; +import { Router } from '../../entities/Message'; +import { EntityDict } from '../../oak-app-domain'; import { MessageHandler } from './index'; export declare const emailHandler: MessageHandler; +export declare function tryMakeEmailNotification(message: { + userId?: string; + type?: string; + entity?: string; + router?: Router | null; + entityId?: string; +}, context: BackendRuntimeContext): Promise<{ + id: string; + data: import("../../types/Email").EmailOptions; + channel: string; +} | undefined>; diff --git a/es/utils/message/email.js b/es/utils/message/email.js index 659cc4ef6..8da59c417 100644 --- a/es/utils/message/email.js +++ b/es/utils/message/email.js @@ -1,4 +1,5 @@ -import { tryMakeEmailNotification } from '../../triggers/message'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { ConverterDict } from './index'; export const emailHandler = async ({ message, applications, system, messageTypeTemplates, context }) => { const notificationDatas = []; const emailNotification = await tryMakeEmailNotification(message, context); @@ -7,3 +8,17 @@ export const emailHandler = async ({ message, applications, system, messageTypeT } return notificationDatas; }; +export async function tryMakeEmailNotification(message, context) { + const { userId, type, entity, entityId, router } = message; + const converter = ConverterDict[type] && ConverterDict[type].toEmail; + if (converter) { + const dispersedData = await converter(message, context); + if (dispersedData) { + return { + id: await generateNewIdAsync(), + data: dispersedData, + channel: 'email', + }; + } + } +} diff --git a/es/utils/message/index.js b/es/utils/message/index.js index 63667f11c..78ea53d66 100644 --- a/es/utils/message/index.js +++ b/es/utils/message/index.js @@ -1,4 +1,8 @@ import { assert } from "oak-domain/lib/utils/assert"; +import { wechatMpHandler } from './wechatMp'; +import { wechatPublicHandler } from './wechatPublic'; +import { smsHandler } from './sms'; +import { emailHandler } from './email'; export const ConverterDict = {}; export function registerMessageNotificationConverters(converter) { Object.keys(converter).forEach(key => { @@ -14,3 +18,8 @@ export function getMessageHandler(channel) { assert(handler, `消息渠道 ${channel} 的处理器未注册`); return handler; } +// 默认注册所有处理器 +registerMessageHandler('wechatMp', wechatMpHandler); +registerMessageHandler('wechatPublic', wechatPublicHandler); +registerMessageHandler('sms', smsHandler); +registerMessageHandler('email', emailHandler); diff --git a/es/utils/message/sms.d.ts b/es/utils/message/sms.d.ts index 2892fd53e..96c1efc43 100644 --- a/es/utils/message/sms.d.ts +++ b/es/utils/message/sms.d.ts @@ -1,2 +1,12 @@ +import BackendRuntimeContext from '../../context/BackendRuntimeContext'; +import { Router } from '../../entities/Message'; +import { EntityDict } from '../../oak-app-domain'; import { MessageHandler } from './index'; export declare const smsHandler: MessageHandler; +export declare function tryMakeSmsNotification(message: { + userId?: string; + type?: string; + entity?: string; + router?: Router | null; + entityId?: string; +}, context: BackendRuntimeContext): Promise; diff --git a/es/utils/message/sms.js b/es/utils/message/sms.js index 8be5b467a..23e976e4e 100644 --- a/es/utils/message/sms.js +++ b/es/utils/message/sms.js @@ -1,4 +1,6 @@ -import { tryMakeSmsNotification } from '../../triggers/message'; +import assert from 'assert'; +import { ConverterDict } from './index'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; export const smsHandler = async ({ message, applications, system, messageTypeTemplates, context }) => { const notificationDatas = []; const smsNotification = await tryMakeSmsNotification(message, context); @@ -7,3 +9,32 @@ export const smsHandler = async ({ message, applications, system, messageTypeTem } return notificationDatas; }; +export async function tryMakeSmsNotification(message, context) { + const { userId, type, entity, entityId, router } = message; + assert(userId); + const [mobile] = await context.select('mobile', { + data: { + id: 1, + mobile: 1, + }, + filter: { + userId, + }, + indexFrom: 0, + count: 1, + }, { dontCollect: true }); + if (mobile) { + const converter = ConverterDict[type] && ConverterDict[type].toSms; + if (converter) { + const dispersedData = await converter(message, context); + if (dispersedData) { + return { + id: await generateNewIdAsync(), + data: dispersedData, + channel: 'sms', + data1: mobile, + }; + } + } + } +} diff --git a/es/utils/notification/email.d.ts b/es/utils/notification/email.d.ts new file mode 100644 index 000000000..5771c887c --- /dev/null +++ b/es/utils/notification/email.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const emailHandler: NotificationHandler; diff --git a/es/utils/notification/email.js b/es/utils/notification/email.js new file mode 100644 index 000000000..ece0f4b0e --- /dev/null +++ b/es/utils/notification/email.js @@ -0,0 +1,47 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { sendEmail } from '../email'; +export const emailHandler = async (notification, context) => { + const { data, id } = notification; + try { + const result = await sendEmail(data, context); + if (result?.success) { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'succeed', + data: {}, + filter: { + id, + }, + }, { dontCollect: true }); + } + else { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: { + data2: { + res: result?.error, + }, + }, + filter: { + id, + }, + }, { dontCollect: true }); + } + } + catch (err) { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: { + data2: { + res: err?.message, + }, + }, + filter: { + id, + }, + }, { dontCollect: true }); + console.warn('发邮件消息失败', err); + } +}; diff --git a/es/utils/notification/index.d.ts b/es/utils/notification/index.d.ts new file mode 100644 index 000000000..4cce90798 --- /dev/null +++ b/es/utils/notification/index.d.ts @@ -0,0 +1,9 @@ +import { EntityDict } from "../../oak-app-domain"; +import { BRC } from "../../types/RuntimeCxt"; +import { Channel } from "../../types/Message"; +export type NotificationHandler = (notification: EntityDict['notification']['OpSchema'], context: BRC) => Promise; +export type NotificationFailureHandler = (message: EntityDict['message']['Schema'], context: BRC) => Promise; +export declare function registerNotificationHandler(channel: Channel, handler: NotificationHandler): void; +export declare function getNotificationHandler(channel: Channel): NotificationHandler; +export declare function registerNotificationFailureHandler(handler: NotificationFailureHandler): void; +export declare function getNotificationFailureHandlers(): NotificationFailureHandler[]; diff --git a/es/utils/notification/index.js b/es/utils/notification/index.js new file mode 100644 index 000000000..9072b748e --- /dev/null +++ b/es/utils/notification/index.js @@ -0,0 +1,26 @@ +import { assert } from "oak-domain/lib/utils/assert"; +import { wechatMpHandler } from './wechatMp'; +import { wechatPublicHandler } from './wechatPublic'; +import { smsHandler } from './sms'; +import { emailHandler } from './email'; +const notificationHandlers = {}; +const notificationFailureHandlers = []; +export function registerNotificationHandler(channel, handler) { + notificationHandlers[channel] = handler; +} +export function getNotificationHandler(channel) { + const handler = notificationHandlers[channel]; + assert(handler, `通知渠道 ${channel} 的处理器未注册`); + return handler; +} +export function registerNotificationFailureHandler(handler) { + notificationFailureHandlers.push(handler); +} +export function getNotificationFailureHandlers() { + return notificationFailureHandlers; +} +// 默认注册所有处理器 +registerNotificationHandler('wechatMp', wechatMpHandler); +registerNotificationHandler('wechatPublic', wechatPublicHandler); +registerNotificationHandler('sms', smsHandler); +registerNotificationHandler('email', emailHandler); diff --git a/es/utils/notification/sms.d.ts b/es/utils/notification/sms.d.ts new file mode 100644 index 000000000..0b928df24 --- /dev/null +++ b/es/utils/notification/sms.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const smsHandler: NotificationHandler; diff --git a/es/utils/notification/sms.js b/es/utils/notification/sms.js new file mode 100644 index 000000000..04b88cfe8 --- /dev/null +++ b/es/utils/notification/sms.js @@ -0,0 +1,65 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { sendSms } from '../sms'; +export const smsHandler = async (notification, context) => { + const { data, data1, id } = notification; + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + message: { + id: 1, + type: 1, + }, + }, + filter: { + id: notification.messageSystemId, + } + }, { dontCollect: true }); + const { message } = messageSystem; + const { type } = message; + try { + const result = await sendSms({ + messageType: type, + templateParam: data.params, + mobile: data1.mobile, + }, context); + if (result?.success === true) { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'succeed', + data: { + data2: { + res: result?.res || {} + } + }, + filter: { + id, + } + }, { dontCollect: true }); + } + else { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: { + data2: { + res: result?.res || {} + } + }, + filter: { + id, + } + }, { dontCollect: true }); + } + } + catch (err) { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + console.warn('发短信消息失败', err); + } +}; diff --git a/es/utils/notification/wechatMp.d.ts b/es/utils/notification/wechatMp.d.ts new file mode 100644 index 000000000..b5fc91f45 --- /dev/null +++ b/es/utils/notification/wechatMp.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const wechatMpHandler: NotificationHandler; diff --git a/es/utils/notification/wechatMp.js b/es/utils/notification/wechatMp.js new file mode 100644 index 000000000..5bf48feb1 --- /dev/null +++ b/es/utils/notification/wechatMp.js @@ -0,0 +1,81 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import WechatSDK from 'oak-external-sdk/lib/WechatSDK'; +import { composeUrl } from 'oak-domain/lib/utils/domain'; +export const wechatMpHandler = async (notification, context) => { + const { data, templateId, messageSystemId, data1, id } = notification; + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + messageId: 1, + message: { + id: 1, + userId: 1, + router: 1, + type: 1, + }, + system: { + id: 1, + application$system: { + $entity: 'application', + data: { + id: 1, + type: 1, + config: 1, + }, + }, + } + }, + filter: { + id: messageSystemId, + } + }, { dontCollect: true }); + const { system, message } = messageSystem; + const { router } = message; + const { application$system: applications } = system; + const app = applications.find(ele => ele.type === 'wechatMp'); + const { config } = app; + const { appId, appSecret } = config; + const instance = WechatSDK.getInstance(appId, 'wechatMp', appSecret); + let page; + if (router) { + const pathname = router.pathname; + const url = pathname.startsWith('/') + ? `pages${pathname}/index` + : `pages/${pathname}/index`; + page = composeUrl(url, Object.assign({}, router.props, router.state)); + } + // 根据当前环境决定消息推哪个版本 + const StateDict = { + 'development': 'developer', + 'staging': 'trial', + 'production': 'former', + }; + try { + await instance.sendSubscribedMessage({ + templateId: templateId, + data: data, + openId: data1.openId, + page, + state: StateDict[process.env.NODE_ENV], + }); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'succeed', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } + catch (err) { + console.warn('发微信小程序消息失败', err); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } +}; diff --git a/es/utils/notification/wechatPublic.d.ts b/es/utils/notification/wechatPublic.d.ts new file mode 100644 index 000000000..6cf7a254d --- /dev/null +++ b/es/utils/notification/wechatPublic.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const wechatPublicHandler: NotificationHandler; diff --git a/es/utils/notification/wechatPublic.js b/es/utils/notification/wechatPublic.js new file mode 100644 index 000000000..c490111ec --- /dev/null +++ b/es/utils/notification/wechatPublic.js @@ -0,0 +1,106 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import WechatSDK from 'oak-external-sdk/lib/WechatSDK'; +import { composeUrl } from 'oak-domain/lib/utils/domain'; +import { composeDomainUrl } from '../domain'; +export const wechatPublicHandler = async (notification, context) => { + const { data, templateId, messageSystemId, data1, id } = notification; + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + messageId: 1, + message: { + id: 1, + userId: 1, + router: 1, + type: 1, + }, + system: { + id: 1, + application$system: { + $entity: 'application', + data: { + id: 1, + type: 1, + config: 1, + }, + }, + } + }, + filter: { + id: messageSystemId, + } + }, { dontCollect: true }); + const { system, message } = messageSystem; + const { router } = message; + const { application$system: applications } = system; + const app = applications.find(ele => ele.type === 'wechatPublic'); + const { config, id: applicationId } = app; + const { appId, appSecret } = config; + const [domain] = await context.select('domain', { + data: { + id: 1, + url: 1, + apiPath: 1, + protocol: 1, + port: 1, + }, + filter: { + system: { + application$system: { + id: applicationId, + }, + }, + }, + }, { dontCollect: true }); + const instance = WechatSDK.getInstance(appId, 'wechatPublic', appSecret); + const { openId, wechatMpAppId } = data1; + let page; + // message 用户不需要跳转页面 + if (router) { + const pathname = router.pathname; + if (wechatMpAppId) { + const url = pathname.startsWith('/') + ? `pages${pathname}/index` + : `pages/${pathname}/index`; + page = composeUrl(url, Object.assign({}, router.props, router.state)); + } + else { + const url = composeDomainUrl(domain, pathname); + page = composeUrl(url, Object.assign({}, router.props, router.state)); + } + } + try { + await instance.sendTemplateMessage({ + openId, + templateId: templateId, + url: !wechatMpAppId ? page : undefined, + data: data, + miniProgram: wechatMpAppId + ? { + appid: wechatMpAppId, + pagepath: page, + } + : undefined, + clientMsgId: id, + }); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'succeed', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } + catch (err) { + console.warn('发微信公众号消息失败', err); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } +}; diff --git a/lib/entities/Notification.d.ts b/lib/entities/Notification.d.ts index 3ee84e523..52756721a 100644 --- a/lib/entities/Notification.d.ts +++ b/lib/entities/Notification.d.ts @@ -1,11 +1,10 @@ import { String } from 'oak-domain/lib/types/DataType'; import { EntityShape } from 'oak-domain/lib/types/Entity'; -import { Channel } from '../types/Message'; import { Schema as Application } from './Application'; import { Schema as MessageSystem } from './MessageSystem'; import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; export interface Schema extends EntityShape { - channel: Channel; + channel: String<32>; application?: Application; data?: Object; messageSystem: MessageSystem; @@ -18,6 +17,5 @@ export type IState = 'sending' | 'success' | 'failure'; type Action = IAction; export declare const entityDesc: EntityDesc; export {}; diff --git a/lib/entities/Notification.js b/lib/entities/Notification.js index 064eb9b24..3d8027790 100644 --- a/lib/entities/Notification.js +++ b/lib/entities/Notification.js @@ -33,14 +33,14 @@ exports.entityDesc = { success: '发送成功', failure: '发送失败', }, - channel: { - wechatPublic: '公众号', - jPush: '极光推送', - jim: '极光消息', - wechatMp: '小程序', - sms: '短信', - email: '邮箱', - } + // channel: { + // wechatPublic: '公众号', + // jPush: '极光推送', + // jim: '极光消息', + // wechatMp: '小程序', + // sms: '短信', + // email: '邮箱', + // } } }, }, @@ -55,14 +55,14 @@ exports.entityDesc = { success: '#008000', failure: '#9A9A9A', }, - channel: { - wechatMp: '#008000', - jPush: '#0000FF', - jim: '#0000FF', - wechatPublic: '#008000', - sms: '#000000', - email: '#000000', - }, + // channel: { + // wechatMp: '#008000', + // jPush: '#0000FF', + // jim: '#0000FF', + // wechatPublic: '#008000', + // sms: '#000000', + // email: '#000000', + // }, } } }; diff --git a/lib/oak-app-domain/Notification/Storage.js b/lib/oak-app-domain/Notification/Storage.js index 13c550f09..938d7f31e 100644 --- a/lib/oak-app-domain/Notification/Storage.js +++ b/lib/oak-app-domain/Notification/Storage.js @@ -6,8 +6,10 @@ exports.desc = { attributes: { channel: { notNull: true, - type: "enum", - enumeration: ["wechatPublic", "jPush", "jim", "wechatMp", "sms", "email"] + type: "varchar", + params: { + length: 32 + } }, applicationId: { type: "ref", diff --git a/lib/oak-app-domain/Notification/Style.js b/lib/oak-app-domain/Notification/Style.js index b09425b29..14c0002fe 100644 --- a/lib/oak-app-domain/Notification/Style.js +++ b/lib/oak-app-domain/Notification/Style.js @@ -12,13 +12,13 @@ exports.style = { success: '#008000', failure: '#9A9A9A', }, - channel: { - wechatMp: '#008000', - jPush: '#0000FF', - jim: '#0000FF', - wechatPublic: '#008000', - sms: '#000000', - email: '#000000', - }, + // channel: { + // wechatMp: '#008000', + // jPush: '#0000FF', + // jim: '#0000FF', + // wechatPublic: '#008000', + // sms: '#000000', + // email: '#000000', + // }, } }; diff --git a/lib/oak-app-domain/Notification/_baseSchema.d.ts b/lib/oak-app-domain/Notification/_baseSchema.d.ts index c20eeee3c..b67187071 100644 --- a/lib/oak-app-domain/Notification/_baseSchema.d.ts +++ b/lib/oak-app-domain/Notification/_baseSchema.d.ts @@ -2,10 +2,9 @@ import { ForeignKey } from "oak-domain/lib/types/DataType"; import { Q_DateValue, Q_NumberValue, Q_StringValue, Q_EnumValue, NodeId, ExprOp, ExpressionKey } from "oak-domain/lib/types/Demand"; import { MakeAction as OakMakeAction, EntityShape } from "oak-domain/lib/types/Entity"; import { Action, ParticularAction, IState } from "./Action"; -import { Channel } from "../../types/Message"; import { String } from "oak-domain/lib/types/DataType"; export type OpSchema = EntityShape & { - channel: Channel; + channel: String<32>; applicationId?: ForeignKey<"application"> | null; data?: Object | null; messageSystemId: ForeignKey<"messageSystem">; @@ -22,7 +21,7 @@ export type OpFilter = { $$createAt$$: Q_DateValue; $$seq$$: Q_NumberValue; $$updateAt$$: Q_DateValue; - channel: Q_EnumValue; + channel: Q_StringValue; applicationId: Q_StringValue; data: Object; messageSystemId: Q_StringValue; diff --git a/lib/oak-app-domain/Notification/locales/zh_CN.json b/lib/oak-app-domain/Notification/locales/zh_CN.json index 29695c575..19bbb24dc 100644 --- a/lib/oak-app-domain/Notification/locales/zh_CN.json +++ b/lib/oak-app-domain/Notification/locales/zh_CN.json @@ -19,14 +19,6 @@ "sending": "发送中", "success": "发送成功", "failure": "发送失败" - }, - "channel": { - "wechatPublic": "公众号", - "jPush": "极光推送", - "jim": "极光消息", - "wechatMp": "小程序", - "sms": "短信", - "email": "邮箱" } } } diff --git a/lib/oak-app-domain/UserEntityGrant/Storage.js b/lib/oak-app-domain/UserEntityGrant/Storage.js index 2022de361..93b22f650 100644 --- a/lib/oak-app-domain/UserEntityGrant/Storage.js +++ b/lib/oak-app-domain/UserEntityGrant/Storage.js @@ -62,7 +62,7 @@ exports.desc = { qrCodeType: { notNull: true, type: "enum", - enumeration: ["wechatPublic", "wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublicForMp", "webForWechatPublic"] + enumeration: ["wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublic", "wechatPublicForMp", "webForWechatPublic"] }, expiresAt: { type: "datetime" diff --git a/lib/oak-app-domain/WechatLogin/Storage.js b/lib/oak-app-domain/WechatLogin/Storage.js index d27b8ea6e..678e6fa32 100644 --- a/lib/oak-app-domain/WechatLogin/Storage.js +++ b/lib/oak-app-domain/WechatLogin/Storage.js @@ -23,7 +23,7 @@ exports.desc = { qrCodeType: { notNull: true, type: "enum", - enumeration: ["wechatPublic", "wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublicForMp", "webForWechatPublic"] + enumeration: ["wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublic", "wechatPublicForMp", "webForWechatPublic"] }, expiresAt: { type: "datetime" diff --git a/lib/oak-app-domain/WechatQrCode/Storage.js b/lib/oak-app-domain/WechatQrCode/Storage.js index 0762671f9..342dd24f4 100644 --- a/lib/oak-app-domain/WechatQrCode/Storage.js +++ b/lib/oak-app-domain/WechatQrCode/Storage.js @@ -22,7 +22,7 @@ exports.desc = { type: { notNull: true, type: "enum", - enumeration: ["wechatPublic", "wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublicForMp", "webForWechatPublic"] + enumeration: ["wechatMpDomainUrl", "wechatMpWxaCode", "wechatPublic", "wechatPublicForMp", "webForWechatPublic"] }, allowShare: { notNull: true, diff --git a/lib/registry.backend.d.ts b/lib/registry.backend.d.ts index 50b7aea24..77cb0612d 100644 --- a/lib/registry.backend.d.ts +++ b/lib/registry.backend.d.ts @@ -8,6 +8,32 @@ export { */ registerMessageType, } from './aspects/template'; export { registerWeChatPublicEventCallback, } from './endpoints/wechat'; -export { registerMessageNotificationConverters, registerMessageHandler, } from './utils/message'; +export { +/** + * 注册消息通知转换器 + * 用于将消息数据转换为特定渠道所需的格式 + * 例如: 将message转换为微信小程序、公众号、短信、邮件所需的数据格式 + */ +registerMessageNotificationConverters, +/** + * 注册消息渠道处理器 + * 用于处理特定渠道的消息创建逻辑 + * 例如: 处理微信小程序、公众号、短信、邮件等渠道的消息生成 + */ +registerMessageHandler, } from './utils/message'; +export { +/** + * 注册通知渠道处理器 + * 用于处理特定渠道的通知发送逻辑 + * 例如: 实际发送微信小程序、公众号、短信、邮件等渠道的通知 + */ +registerNotificationHandler, +/** + * 注册通知失败处理器 + * 用于在所有通知渠道都失败后执行自定义的补救逻辑 + * 可以注册多个处理器,它们会依次执行 + * 例如: 在其他渠道失败后自动发送短信通知 + */ +registerNotificationFailureHandler, } from './utils/notification'; export { registSms, } from './utils/sms'; export { registerCosBackend, } from './utils/cos/index.backend'; diff --git a/lib/registry.backend.js b/lib/registry.backend.js index 21da82e68..08f2e162c 100644 --- a/lib/registry.backend.js +++ b/lib/registry.backend.js @@ -4,7 +4,7 @@ * 如需要注入,请在routine中编写注册逻辑,使用此处提供的注册方法进行注册 */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.registerCosBackend = exports.registSms = exports.registerMessageHandler = exports.registerMessageNotificationConverters = exports.registerWeChatPublicEventCallback = exports.registerMessageType = void 0; +exports.registerCosBackend = exports.registSms = exports.registerNotificationFailureHandler = exports.registerNotificationHandler = exports.registerMessageHandler = exports.registerMessageNotificationConverters = exports.registerWeChatPublicEventCallback = exports.registerMessageType = void 0; var template_1 = require("./aspects/template"); /** * 注册消息类型 @@ -14,9 +14,32 @@ var wechat_1 = require("./endpoints/wechat"); // 注册微信事件回调处理器endpoint Object.defineProperty(exports, "registerWeChatPublicEventCallback", { enumerable: true, get: function () { return wechat_1.registerWeChatPublicEventCallback; } }); var message_1 = require("./utils/message"); -// 注册消息通知转换器trigger +/** + * 注册消息通知转换器 + * 用于将消息数据转换为特定渠道所需的格式 + * 例如: 将message转换为微信小程序、公众号、短信、邮件所需的数据格式 + */ Object.defineProperty(exports, "registerMessageNotificationConverters", { enumerable: true, get: function () { return message_1.registerMessageNotificationConverters; } }); +/** + * 注册消息渠道处理器 + * 用于处理特定渠道的消息创建逻辑 + * 例如: 处理微信小程序、公众号、短信、邮件等渠道的消息生成 + */ Object.defineProperty(exports, "registerMessageHandler", { enumerable: true, get: function () { return message_1.registerMessageHandler; } }); +var notification_1 = require("./utils/notification"); +/** + * 注册通知渠道处理器 + * 用于处理特定渠道的通知发送逻辑 + * 例如: 实际发送微信小程序、公众号、短信、邮件等渠道的通知 + */ +Object.defineProperty(exports, "registerNotificationHandler", { enumerable: true, get: function () { return notification_1.registerNotificationHandler; } }); +/** + * 注册通知失败处理器 + * 用于在所有通知渠道都失败后执行自定义的补救逻辑 + * 可以注册多个处理器,它们会依次执行 + * 例如: 在其他渠道失败后自动发送短信通知 + */ +Object.defineProperty(exports, "registerNotificationFailureHandler", { enumerable: true, get: function () { return notification_1.registerNotificationFailureHandler; } }); var sms_1 = require("./utils/sms"); // 注册短信服务商实现 Object.defineProperty(exports, "registSms", { enumerable: true, get: function () { return sms_1.registSms; } }); diff --git a/lib/triggers/index.d.ts b/lib/triggers/index.d.ts index fbd2c14a3..5e0cf60ac 100644 --- a/lib/triggers/index.d.ts +++ b/lib/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/lib/triggers/message.d.ts b/lib/triggers/message.d.ts index d7b744870..f228e2959 100644 --- a/lib/triggers/message.d.ts +++ b/lib/triggers/message.d.ts @@ -1,25 +1,5 @@ import { Trigger } from 'oak-domain/lib/types/Trigger'; import { EntityDict } from '../oak-app-domain/EntityDict'; import { BRC } from '../types/RuntimeCxt'; -import { BackendRuntimeContext } from '../context/BackendRuntimeContext'; -import { Router } from '../entities/Message'; -export declare function tryMakeSmsNotification(message: { - userId?: string; - type?: string; - entity?: string; - router?: Router | null; - entityId?: string; -}, context: BackendRuntimeContext): Promise; -export declare function tryMakeEmailNotification(message: { - userId?: string; - type?: string; - entity?: string; - router?: Router | null; - entityId?: string; -}, context: BackendRuntimeContext): Promise<{ - id: string; - data: import("../types/Email").EmailOptions; - channel: string; -} | undefined>; declare const triggers: Trigger>[]; export default triggers; diff --git a/lib/triggers/message.js b/lib/triggers/message.js index 8c58b8ca3..b60eefd13 100644 --- a/lib/triggers/message.js +++ b/lib/triggers/message.js @@ -1,7 +1,5 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.tryMakeSmsNotification = tryMakeSmsNotification; -exports.tryMakeEmailNotification = tryMakeEmailNotification; const uuid_1 = require("oak-domain/lib/utils/uuid"); const assert_1 = require("oak-domain/lib/utils/assert"); const lodash_1 = require("oak-domain/lib/utils/lodash"); @@ -11,49 +9,6 @@ const InitialChannelByWeightMatrix = { medium: ['wechatMp', 'wechatPublic', 'email'], low: ['wechatMp', 'wechatPublic', 'email'], }; -async function tryMakeSmsNotification(message, context) { - const { userId, type, entity, entityId, router } = message; - (0, assert_1.assert)(userId); - const [mobile] = await context.select('mobile', { - data: { - id: 1, - mobile: 1, - }, - filter: { - userId, - }, - indexFrom: 0, - count: 1, - }, { dontCollect: true }); - if (mobile) { - const converter = message_1.ConverterDict[type] && message_1.ConverterDict[type].toSms; - if (converter) { - const dispersedData = await converter(message, context); - if (dispersedData) { - return { - id: await (0, uuid_1.generateNewIdAsync)(), - data: dispersedData, - channel: 'sms', - data1: mobile, - }; - } - } - } -} -async function tryMakeEmailNotification(message, context) { - const { userId, type, entity, entityId, router } = message; - const converter = message_1.ConverterDict[type] && message_1.ConverterDict[type].toEmail; - if (converter) { - const dispersedData = await converter(message, context); - if (dispersedData) { - return { - id: await (0, uuid_1.generateNewIdAsync)(), - data: dispersedData, - channel: 'email', - }; - } - } -} async function createNotification(message, context) { const { restriction, userId, weight, type, entity, entityId, platformId, channels } = message; (0, assert_1.assert)(userId); diff --git a/lib/triggers/notification.js b/lib/triggers/notification.js index ca7df6315..fb221384c 100644 --- a/lib/triggers/notification.js +++ b/lib/triggers/notification.js @@ -1,270 +1,17 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const tslib_1 = require("tslib"); const assert_1 = require("oak-domain/lib/utils/assert"); -const WechatSDK_1 = tslib_1.__importDefault(require("oak-external-sdk/lib/WechatSDK")); -const domain_1 = require("oak-domain/lib/utils/domain"); const uuid_1 = require("oak-domain/lib/utils/uuid"); -const sms_1 = require("../utils/sms"); -const email_1 = require("../utils/email"); -const message_1 = require("./message"); -const domain_2 = require("../utils/domain"); +const sms_1 = require("../utils/message/sms"); +const notification_1 = require("../utils/notification"); async function sendNotification(notification, context) { - const { data, templateId, channel, messageSystemId, data1, id } = notification; - const [messageSystem] = await context.select('messageSystem', { - data: { - id: 1, - messageId: 1, - message: { - id: 1, - userId: 1, - router: 1, - type: 1, - }, - system: { - id: 1, - application$system: { - $entity: 'application', - data: { - id: 1, - type: 1, - config: 1, - }, - }, - } - }, - filter: { - id: messageSystemId, - } - }, { dontCollect: true }); - const { system, message } = messageSystem; - const { router, userId, type } = message; - const { application$system: applications, config } = system; - switch (channel) { - case 'wechatMp': { - const app = applications.find(ele => ele.type === 'wechatMp'); - const { config } = app; - const { appId, appSecret } = config; - const instance = WechatSDK_1.default.getInstance(appId, 'wechatMp', appSecret); - let page; - if (router) { - const pathname = router.pathname; - const url = pathname.startsWith('/') - ? `pages${pathname}/index` - : `pages/${pathname}/index`; - page = (0, domain_1.composeUrl)(url, Object.assign({}, router.props, router.state)); - } - // 根据当前环境决定消息推哪个版本 - const StateDict = { - 'development': 'developer', - 'staging': 'trial', - 'production': 'former', - }; - try { - await instance.sendSubscribedMessage({ - templateId: templateId, - data: data, - openId: data1.openId, // 在notification创建时就赋值了 - page, - state: StateDict[process.env.NODE_ENV], - }); - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'succeed', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - catch (err) { - console.warn('发微信小程序消息失败', err); - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'fail', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - } - case 'wechatPublic': { - const app = applications.find(ele => ele.type === 'wechatPublic'); - const { config, id: applicationId } = app; - const { appId, appSecret } = config; - const [domain] = await context.select('domain', { - data: { - id: 1, - url: 1, - apiPath: 1, - protocol: 1, - port: 1, - }, - filter: { - system: { - application$system: { - id: applicationId, - }, - }, - }, - }, { dontCollect: true }); - const instance = WechatSDK_1.default.getInstance(appId, 'wechatPublic', appSecret); - const { openId, wechatMpAppId } = data1; - let page; - // message 用户不需要跳转页面 - if (router) { - const pathname = router.pathname; - if (wechatMpAppId) { - const url = pathname.startsWith('/') - ? `pages${pathname}/index` - : `pages/${pathname}/index`; - page = (0, domain_1.composeUrl)(url, Object.assign({}, router.props, router.state)); - } - else { - const url = (0, domain_2.composeDomainUrl)(domain, pathname); - page = (0, domain_1.composeUrl)(url, Object.assign({}, router.props, router.state)); - } - } - try { - await instance.sendTemplateMessage({ - openId, - templateId: templateId, - url: !wechatMpAppId ? page : undefined, - data: data, - miniProgram: wechatMpAppId - ? { - appid: wechatMpAppId, - pagepath: page, - } - : undefined, - clientMsgId: id, - }); - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'succeed', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - catch (err) { - console.warn('发微信公众号消息失败', err); - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'fail', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - } - case 'email': { - try { - const result = await (0, email_1.sendEmail)(data, context); - if (result?.success) { - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'succeed', - data: {}, - filter: { - id, - }, - }, { dontCollect: true }); - } - else { - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'fail', - data: { - data2: { - res: result?.error, - }, - }, - filter: { - id, - }, - }, { dontCollect: true }); - } - return 1; - } - catch (err) { - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'fail', - data: { - data2: { - res: err?.message, - }, - }, - filter: { - id, - }, - }, { dontCollect: true }); - console.warn('发邮件消息失败', err); - return 1; - } - } - default: { - (0, assert_1.assert)(channel === 'sms'); - try { - const result = await (0, sms_1.sendSms)({ - messageType: type, - templateParam: data.params, - mobile: data1.mobile, - }, context); - if (result?.success === true) { - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'succeed', - data: { - data2: { - res: result?.res || {} - } - }, - filter: { - id, - } - }, { dontCollect: true }); - } - else { - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'fail', - data: { - data2: { - res: result?.res || {} - } - }, - filter: { - id, - } - }, { dontCollect: true }); - } - } - catch (err) { - await context.operate('notification', { - id: await (0, uuid_1.generateNewIdAsync)(), - action: 'fail', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - console.warn('发短信消息失败', err); - return 1; - } - } - } + const { channel } = notification; + const handler = (0, notification_1.getNotificationHandler)(channel); + await handler(notification, context); + return 1; } async function tryCreateSmsNotification(message, context) { - const smsNotification = await (0, message_1.tryMakeSmsNotification)(message, context); + const smsNotification = await (0, sms_1.tryMakeSmsNotification)(message, context); if (smsNotification) { const { messageSystem$message } = message; for (const ms of messageSystem$message) { @@ -418,8 +165,27 @@ const triggers = [ closeRootMode(); return 1; } - if (message.weight === 'medium' && !smsTried && allFailed) { - // 中级的消息,在其它途径都失败的情况下再发短信 + // 获取所有注册的失败处理器 + const failureHandlers = (0, notification_1.getNotificationFailureHandlers)(); + if (failureHandlers.length > 0) { + // 如果有注册的失败处理器,执行所有处理器 + let totalResult = 0; + for (const handler of failureHandlers) { + try { + const result = await handler(message, context); + totalResult += result; + } + catch (err) { + console.error('执行notification失败处理器时出错:', err); + } + } + if (totalResult > 0) { + closeRootMode(); + return totalResult; + } + } + else if (message.weight === 'medium' && !smsTried && allFailed) { + // 如果没有注册处理器,走原有逻辑:中级的消息,在其它途径都失败的情况下再发短信 const result = await tryCreateSmsNotification(message, context); closeRootMode(); return result; diff --git a/lib/triggers/toDo.d.ts b/lib/triggers/toDo.d.ts index e5be399e1..1d9bc2ce2 100644 --- a/lib/triggers/toDo.d.ts +++ b/lib/triggers/toDo.d.ts @@ -14,7 +14,7 @@ export declare function createToDo; +}, userIds?: string[]): Promise<0 | 1>; /** * 完成todo例程,当在entity对象上进行action操作时(操作条件是filter),将对应的todo完成 * 必须在entity的action的后trigger中调用 diff --git a/lib/utils/message/email.d.ts b/lib/utils/message/email.d.ts index 608ce29bc..35e34928e 100644 --- a/lib/utils/message/email.d.ts +++ b/lib/utils/message/email.d.ts @@ -1,2 +1,16 @@ +import BackendRuntimeContext from '../../context/BackendRuntimeContext'; +import { Router } from '../../entities/Message'; +import { EntityDict } from '../../oak-app-domain'; import { MessageHandler } from './index'; export declare const emailHandler: MessageHandler; +export declare function tryMakeEmailNotification(message: { + userId?: string; + type?: string; + entity?: string; + router?: Router | null; + entityId?: string; +}, context: BackendRuntimeContext): Promise<{ + id: string; + data: import("../../types/Email").EmailOptions; + channel: string; +} | undefined>; diff --git a/lib/utils/message/email.js b/lib/utils/message/email.js index 3734f9fd0..0f1aac20d 100644 --- a/lib/utils/message/email.js +++ b/lib/utils/message/email.js @@ -1,13 +1,29 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.emailHandler = void 0; -const message_1 = require("../../triggers/message"); +exports.tryMakeEmailNotification = tryMakeEmailNotification; +const uuid_1 = require("oak-domain/lib/utils/uuid"); +const index_1 = require("./index"); const emailHandler = async ({ message, applications, system, messageTypeTemplates, context }) => { const notificationDatas = []; - const emailNotification = await (0, message_1.tryMakeEmailNotification)(message, context); + const emailNotification = await tryMakeEmailNotification(message, context); if (emailNotification) { notificationDatas.push(emailNotification); } return notificationDatas; }; exports.emailHandler = emailHandler; +async function tryMakeEmailNotification(message, context) { + const { userId, type, entity, entityId, router } = message; + const converter = index_1.ConverterDict[type] && index_1.ConverterDict[type].toEmail; + if (converter) { + const dispersedData = await converter(message, context); + if (dispersedData) { + return { + id: await (0, uuid_1.generateNewIdAsync)(), + data: dispersedData, + channel: 'email', + }; + } + } +} diff --git a/lib/utils/message/index.js b/lib/utils/message/index.js index 10a31281e..15a7a93a0 100644 --- a/lib/utils/message/index.js +++ b/lib/utils/message/index.js @@ -5,6 +5,10 @@ exports.registerMessageNotificationConverters = registerMessageNotificationConve exports.registerMessageHandler = registerMessageHandler; exports.getMessageHandler = getMessageHandler; const assert_1 = require("oak-domain/lib/utils/assert"); +const wechatMp_1 = require("./wechatMp"); +const wechatPublic_1 = require("./wechatPublic"); +const sms_1 = require("./sms"); +const email_1 = require("./email"); exports.ConverterDict = {}; function registerMessageNotificationConverters(converter) { Object.keys(converter).forEach(key => { @@ -20,3 +24,8 @@ function getMessageHandler(channel) { (0, assert_1.assert)(handler, `消息渠道 ${channel} 的处理器未注册`); return handler; } +// 默认注册所有处理器 +registerMessageHandler('wechatMp', wechatMp_1.wechatMpHandler); +registerMessageHandler('wechatPublic', wechatPublic_1.wechatPublicHandler); +registerMessageHandler('sms', sms_1.smsHandler); +registerMessageHandler('email', email_1.emailHandler); diff --git a/lib/utils/message/sms.d.ts b/lib/utils/message/sms.d.ts index 2892fd53e..96c1efc43 100644 --- a/lib/utils/message/sms.d.ts +++ b/lib/utils/message/sms.d.ts @@ -1,2 +1,12 @@ +import BackendRuntimeContext from '../../context/BackendRuntimeContext'; +import { Router } from '../../entities/Message'; +import { EntityDict } from '../../oak-app-domain'; import { MessageHandler } from './index'; export declare const smsHandler: MessageHandler; +export declare function tryMakeSmsNotification(message: { + userId?: string; + type?: string; + entity?: string; + router?: Router | null; + entityId?: string; +}, context: BackendRuntimeContext): Promise; diff --git a/lib/utils/message/sms.js b/lib/utils/message/sms.js index a48fe595a..3ec8dc129 100644 --- a/lib/utils/message/sms.js +++ b/lib/utils/message/sms.js @@ -1,13 +1,46 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.smsHandler = void 0; -const message_1 = require("../../triggers/message"); +exports.tryMakeSmsNotification = tryMakeSmsNotification; +const tslib_1 = require("tslib"); +const assert_1 = tslib_1.__importDefault(require("assert")); +const index_1 = require("./index"); +const uuid_1 = require("oak-domain/lib/utils/uuid"); const smsHandler = async ({ message, applications, system, messageTypeTemplates, context }) => { const notificationDatas = []; - const smsNotification = await (0, message_1.tryMakeSmsNotification)(message, context); + const smsNotification = await tryMakeSmsNotification(message, context); if (smsNotification) { notificationDatas.push(smsNotification); } return notificationDatas; }; exports.smsHandler = smsHandler; +async function tryMakeSmsNotification(message, context) { + const { userId, type, entity, entityId, router } = message; + (0, assert_1.default)(userId); + const [mobile] = await context.select('mobile', { + data: { + id: 1, + mobile: 1, + }, + filter: { + userId, + }, + indexFrom: 0, + count: 1, + }, { dontCollect: true }); + if (mobile) { + const converter = index_1.ConverterDict[type] && index_1.ConverterDict[type].toSms; + if (converter) { + const dispersedData = await converter(message, context); + if (dispersedData) { + return { + id: await (0, uuid_1.generateNewIdAsync)(), + data: dispersedData, + channel: 'sms', + data1: mobile, + }; + } + } + } +} diff --git a/lib/utils/notification/email.d.ts b/lib/utils/notification/email.d.ts new file mode 100644 index 000000000..5771c887c --- /dev/null +++ b/lib/utils/notification/email.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const emailHandler: NotificationHandler; diff --git a/lib/utils/notification/email.js b/lib/utils/notification/email.js new file mode 100644 index 000000000..cfaea9c98 --- /dev/null +++ b/lib/utils/notification/email.js @@ -0,0 +1,51 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.emailHandler = void 0; +const uuid_1 = require("oak-domain/lib/utils/uuid"); +const email_1 = require("../email"); +const emailHandler = async (notification, context) => { + const { data, id } = notification; + try { + const result = await (0, email_1.sendEmail)(data, context); + if (result?.success) { + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'succeed', + data: {}, + filter: { + id, + }, + }, { dontCollect: true }); + } + else { + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'fail', + data: { + data2: { + res: result?.error, + }, + }, + filter: { + id, + }, + }, { dontCollect: true }); + } + } + catch (err) { + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'fail', + data: { + data2: { + res: err?.message, + }, + }, + filter: { + id, + }, + }, { dontCollect: true }); + console.warn('发邮件消息失败', err); + } +}; +exports.emailHandler = emailHandler; diff --git a/lib/utils/notification/index.d.ts b/lib/utils/notification/index.d.ts new file mode 100644 index 000000000..4cce90798 --- /dev/null +++ b/lib/utils/notification/index.d.ts @@ -0,0 +1,9 @@ +import { EntityDict } from "../../oak-app-domain"; +import { BRC } from "../../types/RuntimeCxt"; +import { Channel } from "../../types/Message"; +export type NotificationHandler = (notification: EntityDict['notification']['OpSchema'], context: BRC) => Promise; +export type NotificationFailureHandler = (message: EntityDict['message']['Schema'], context: BRC) => Promise; +export declare function registerNotificationHandler(channel: Channel, handler: NotificationHandler): void; +export declare function getNotificationHandler(channel: Channel): NotificationHandler; +export declare function registerNotificationFailureHandler(handler: NotificationFailureHandler): void; +export declare function getNotificationFailureHandlers(): NotificationFailureHandler[]; diff --git a/lib/utils/notification/index.js b/lib/utils/notification/index.js new file mode 100644 index 000000000..7ef5a6263 --- /dev/null +++ b/lib/utils/notification/index.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerNotificationHandler = registerNotificationHandler; +exports.getNotificationHandler = getNotificationHandler; +exports.registerNotificationFailureHandler = registerNotificationFailureHandler; +exports.getNotificationFailureHandlers = getNotificationFailureHandlers; +const assert_1 = require("oak-domain/lib/utils/assert"); +const wechatMp_1 = require("./wechatMp"); +const wechatPublic_1 = require("./wechatPublic"); +const sms_1 = require("./sms"); +const email_1 = require("./email"); +const notificationHandlers = {}; +const notificationFailureHandlers = []; +function registerNotificationHandler(channel, handler) { + notificationHandlers[channel] = handler; +} +function getNotificationHandler(channel) { + const handler = notificationHandlers[channel]; + (0, assert_1.assert)(handler, `通知渠道 ${channel} 的处理器未注册`); + return handler; +} +function registerNotificationFailureHandler(handler) { + notificationFailureHandlers.push(handler); +} +function getNotificationFailureHandlers() { + return notificationFailureHandlers; +} +// 默认注册所有处理器 +registerNotificationHandler('wechatMp', wechatMp_1.wechatMpHandler); +registerNotificationHandler('wechatPublic', wechatPublic_1.wechatPublicHandler); +registerNotificationHandler('sms', sms_1.smsHandler); +registerNotificationHandler('email', email_1.emailHandler); diff --git a/lib/utils/notification/sms.d.ts b/lib/utils/notification/sms.d.ts new file mode 100644 index 000000000..0b928df24 --- /dev/null +++ b/lib/utils/notification/sms.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const smsHandler: NotificationHandler; diff --git a/lib/utils/notification/sms.js b/lib/utils/notification/sms.js new file mode 100644 index 000000000..d8c3f3245 --- /dev/null +++ b/lib/utils/notification/sms.js @@ -0,0 +1,69 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.smsHandler = void 0; +const uuid_1 = require("oak-domain/lib/utils/uuid"); +const sms_1 = require("../sms"); +const smsHandler = async (notification, context) => { + const { data, data1, id } = notification; + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + message: { + id: 1, + type: 1, + }, + }, + filter: { + id: notification.messageSystemId, + } + }, { dontCollect: true }); + const { message } = messageSystem; + const { type } = message; + try { + const result = await (0, sms_1.sendSms)({ + messageType: type, + templateParam: data.params, + mobile: data1.mobile, + }, context); + if (result?.success === true) { + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'succeed', + data: { + data2: { + res: result?.res || {} + } + }, + filter: { + id, + } + }, { dontCollect: true }); + } + else { + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'fail', + data: { + data2: { + res: result?.res || {} + } + }, + filter: { + id, + } + }, { dontCollect: true }); + } + } + catch (err) { + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + console.warn('发短信消息失败', err); + } +}; +exports.smsHandler = smsHandler; diff --git a/lib/utils/notification/wechatMp.d.ts b/lib/utils/notification/wechatMp.d.ts new file mode 100644 index 000000000..b5fc91f45 --- /dev/null +++ b/lib/utils/notification/wechatMp.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const wechatMpHandler: NotificationHandler; diff --git a/lib/utils/notification/wechatMp.js b/lib/utils/notification/wechatMp.js new file mode 100644 index 000000000..40b3c9f14 --- /dev/null +++ b/lib/utils/notification/wechatMp.js @@ -0,0 +1,86 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.wechatMpHandler = void 0; +const tslib_1 = require("tslib"); +const uuid_1 = require("oak-domain/lib/utils/uuid"); +const WechatSDK_1 = tslib_1.__importDefault(require("oak-external-sdk/lib/WechatSDK")); +const domain_1 = require("oak-domain/lib/utils/domain"); +const wechatMpHandler = async (notification, context) => { + const { data, templateId, messageSystemId, data1, id } = notification; + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + messageId: 1, + message: { + id: 1, + userId: 1, + router: 1, + type: 1, + }, + system: { + id: 1, + application$system: { + $entity: 'application', + data: { + id: 1, + type: 1, + config: 1, + }, + }, + } + }, + filter: { + id: messageSystemId, + } + }, { dontCollect: true }); + const { system, message } = messageSystem; + const { router } = message; + const { application$system: applications } = system; + const app = applications.find(ele => ele.type === 'wechatMp'); + const { config } = app; + const { appId, appSecret } = config; + const instance = WechatSDK_1.default.getInstance(appId, 'wechatMp', appSecret); + let page; + if (router) { + const pathname = router.pathname; + const url = pathname.startsWith('/') + ? `pages${pathname}/index` + : `pages/${pathname}/index`; + page = (0, domain_1.composeUrl)(url, Object.assign({}, router.props, router.state)); + } + // 根据当前环境决定消息推哪个版本 + const StateDict = { + 'development': 'developer', + 'staging': 'trial', + 'production': 'former', + }; + try { + await instance.sendSubscribedMessage({ + templateId: templateId, + data: data, + openId: data1.openId, + page, + state: StateDict[process.env.NODE_ENV], + }); + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'succeed', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } + catch (err) { + console.warn('发微信小程序消息失败', err); + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } +}; +exports.wechatMpHandler = wechatMpHandler; diff --git a/lib/utils/notification/wechatPublic.d.ts b/lib/utils/notification/wechatPublic.d.ts new file mode 100644 index 000000000..6cf7a254d --- /dev/null +++ b/lib/utils/notification/wechatPublic.d.ts @@ -0,0 +1,2 @@ +import { NotificationHandler } from './index'; +export declare const wechatPublicHandler: NotificationHandler; diff --git a/lib/utils/notification/wechatPublic.js b/lib/utils/notification/wechatPublic.js new file mode 100644 index 000000000..0dccfdd0b --- /dev/null +++ b/lib/utils/notification/wechatPublic.js @@ -0,0 +1,111 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.wechatPublicHandler = void 0; +const tslib_1 = require("tslib"); +const uuid_1 = require("oak-domain/lib/utils/uuid"); +const WechatSDK_1 = tslib_1.__importDefault(require("oak-external-sdk/lib/WechatSDK")); +const domain_1 = require("oak-domain/lib/utils/domain"); +const domain_2 = require("../domain"); +const wechatPublicHandler = async (notification, context) => { + const { data, templateId, messageSystemId, data1, id } = notification; + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + messageId: 1, + message: { + id: 1, + userId: 1, + router: 1, + type: 1, + }, + system: { + id: 1, + application$system: { + $entity: 'application', + data: { + id: 1, + type: 1, + config: 1, + }, + }, + } + }, + filter: { + id: messageSystemId, + } + }, { dontCollect: true }); + const { system, message } = messageSystem; + const { router } = message; + const { application$system: applications } = system; + const app = applications.find(ele => ele.type === 'wechatPublic'); + const { config, id: applicationId } = app; + const { appId, appSecret } = config; + const [domain] = await context.select('domain', { + data: { + id: 1, + url: 1, + apiPath: 1, + protocol: 1, + port: 1, + }, + filter: { + system: { + application$system: { + id: applicationId, + }, + }, + }, + }, { dontCollect: true }); + const instance = WechatSDK_1.default.getInstance(appId, 'wechatPublic', appSecret); + const { openId, wechatMpAppId } = data1; + let page; + // message 用户不需要跳转页面 + if (router) { + const pathname = router.pathname; + if (wechatMpAppId) { + const url = pathname.startsWith('/') + ? `pages${pathname}/index` + : `pages/${pathname}/index`; + page = (0, domain_1.composeUrl)(url, Object.assign({}, router.props, router.state)); + } + else { + const url = (0, domain_2.composeDomainUrl)(domain, pathname); + page = (0, domain_1.composeUrl)(url, Object.assign({}, router.props, router.state)); + } + } + try { + await instance.sendTemplateMessage({ + openId, + templateId: templateId, + url: !wechatMpAppId ? page : undefined, + data: data, + miniProgram: wechatMpAppId + ? { + appid: wechatMpAppId, + pagepath: page, + } + : undefined, + clientMsgId: id, + }); + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'succeed', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } + catch (err) { + console.warn('发微信公众号消息失败', err); + await context.operate('notification', { + id: await (0, uuid_1.generateNewIdAsync)(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } +}; +exports.wechatPublicHandler = wechatPublicHandler; diff --git a/src/entities/Notification.ts b/src/entities/Notification.ts index 8edc160de..3859ca9da 100644 --- a/src/entities/Notification.ts +++ b/src/entities/Notification.ts @@ -9,7 +9,7 @@ import { Schema as MessageSystem } from './MessageSystem'; import { EntityDesc } from 'oak-domain/lib/types/EntityDesc'; export interface Schema extends EntityShape { - channel: Channel, + channel: String<32>, application?: Application, data?: Object, messageSystem: MessageSystem, @@ -33,7 +33,7 @@ const IActionDef: ActionDef = { export const entityDesc: EntityDesc = { locales: { zh_CN: { @@ -58,14 +58,14 @@ export const entityDesc: EntityDesc = { high: ['wechatMp', 'wechatPublic', 'sms', 'email'], @@ -17,69 +14,6 @@ const InitialChannelByWeightMatrix: Record = { low: ['wechatMp', 'wechatPublic', 'email'], }; -export async function tryMakeSmsNotification(message: { - userId?: string; - type?: string; - entity?: string; - router?: Router | null; - entityId?: string; -}, context: BackendRuntimeContext) { - const { userId, type, entity, entityId, router } = message; - assert(userId); - const [mobile] = await context.select('mobile', { - data: { - id: 1, - mobile: 1, - }, - filter: { - userId, - }, - indexFrom: 0, - count: 1, - }, { dontCollect: true }); - if (mobile) { - const converter = ConverterDict[type!] && ConverterDict[type!].toSms; - if (converter) { - const dispersedData = await converter(message as EntityDict['message']['OpSchema'], context); - if (dispersedData) { - return { - id: await generateNewIdAsync(), - data: dispersedData, - channel: 'sms', - data1: mobile, - } as any; - } - } - } -} - -export async function tryMakeEmailNotification( - message: { - userId?: string; - type?: string; - entity?: string; - router?: Router | null; - entityId?: string; - }, - context: BackendRuntimeContext -) { - const { userId, type, entity, entityId, router } = message; - const converter = ConverterDict[type!] && ConverterDict[type!].toEmail; - if (converter) { - const dispersedData = await converter( - message as EntityDict['message']['OpSchema'], - context - ); - if (dispersedData) { - return { - id: await generateNewIdAsync(), - data: dispersedData, - channel: 'email', - }; - } - } -} - async function createNotification(message: CreateMessageData, context: BRC) { const { restriction, userId, weight, type, entity, entityId, platformId, channels } = message; assert(userId); diff --git a/src/triggers/notification.ts b/src/triggers/notification.ts index 9262cbe80..4d3a23697 100644 --- a/src/triggers/notification.ts +++ b/src/triggers/notification.ts @@ -3,307 +3,15 @@ import { EntityDict } from '../oak-app-domain/EntityDict'; import { CreateOperationData as CreateNotificationData } from '../oak-app-domain/Notification/Schema'; import { assert } from 'oak-domain/lib/utils/assert'; import { BRC } from '../types/RuntimeCxt'; -import { WechatMpConfig, WechatPublicConfig, WebConfig } from '../oak-app-domain/Application/Schema'; -import WechatSDK, { WechatMpInstance, WechatPublicInstance } from 'oak-external-sdk/lib/WechatSDK'; -import { composeUrl } from 'oak-domain/lib/utils/domain'; import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; -import { sendSms } from '../utils/sms'; -import { sendEmail } from '../utils/email'; -import { tryMakeSmsNotification } from './message'; -import { composeDomainUrl } from '../utils/domain'; +import { tryMakeSmsNotification } from '../utils/message/sms'; +import { getNotificationHandler, getNotificationFailureHandlers } from '../utils/notification'; async function sendNotification(notification: EntityDict['notification']['OpSchema'], context: BRC) { - const { data, templateId, channel, messageSystemId, data1, id } = notification; - const [messageSystem] = await context.select('messageSystem', { - data: { - id: 1, - messageId: 1, - message: { - id: 1, - userId: 1, - router: 1, - type: 1, - }, - system: { - id: 1, - application$system: { - $entity: 'application', - data: { - id: 1, - type: 1, - config: 1, - }, - }, - } - }, - filter: { - id: messageSystemId, - } - }, { dontCollect: true }); - const { system, message } = messageSystem!; - const { router, userId, type } = message!; - const { application$system: applications, config } = system!; - switch (channel) { - case 'wechatMp': { - const app = applications!.find( - ele => ele.type === 'wechatMp' - ); - const { config } = app!; - const { appId, appSecret } = config as WechatMpConfig; - const instance = WechatSDK.getInstance(appId!, 'wechatMp', appSecret) as WechatMpInstance; - let page; - if (router) { - const pathname = router.pathname; - const url = pathname.startsWith('/') - ? `pages${pathname}/index` - : `pages/${pathname}/index`; - page = composeUrl( - url, - Object.assign({}, router!.props!, router!.state!) - ); - } - - // 根据当前环境决定消息推哪个版本 - const StateDict = { - 'development': 'developer', - 'staging': 'trial', - 'production': 'former', - } - try { - await instance.sendSubscribedMessage({ - templateId: templateId!, - data: data!, - openId: (data1 as { openId: string }).openId, // 在notification创建时就赋值了 - page, - state: StateDict[process.env.NODE_ENV as 'development'] as 'developer', - }); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'succeed', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - catch (err) { - console.warn('发微信小程序消息失败', err); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: { - }, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - } - case 'wechatPublic': { - const app = applications!.find( - ele => ele.type === 'wechatPublic' - ); - const { config, id: applicationId } = app!; - const { appId, appSecret } = config as WechatPublicConfig; - const [domain] = await context.select( - 'domain', - { - data: { - id: 1, - url: 1, - apiPath: 1, - protocol: 1, - port: 1, - }, - filter: { - system: { - application$system: { - id: applicationId, - }, - }, - }, - }, - { dontCollect: true } - ); - const instance = WechatSDK.getInstance(appId!, 'wechatPublic', appSecret) as WechatPublicInstance; - const { openId, wechatMpAppId } = data1 as { - openId: string, - wechatMpAppId?: string, - }; - - let page; - // message 用户不需要跳转页面 - if (router) { - const pathname = router.pathname; - - if (wechatMpAppId) { - const url = pathname.startsWith('/') - ? `pages${pathname}/index` - : `pages/${pathname}/index`; - - page = composeUrl( - url, - Object.assign({}, router!.props!, router!.state!) - ); - } else { - const url = composeDomainUrl( - domain as EntityDict['domain']['Schema'], - pathname - ); - page = composeUrl( - url, - Object.assign({}, router!.props!, router!.state!) - ); - } - } - - - try { - await instance.sendTemplateMessage({ - openId, - templateId: templateId!, - url: !wechatMpAppId ? page : undefined, - data: data!, - miniProgram: wechatMpAppId - ? { - appid: wechatMpAppId, - pagepath: page as string, - } - : undefined, - clientMsgId: id, - }); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'succeed', - data: {}, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - catch (err) { - console.warn('发微信公众号消息失败', err); - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: { - }, - filter: { - id, - } - }, { dontCollect: true }); - return 1; - } - } - case 'email': { - try { - const result = await sendEmail(data as any, context); - if (result?.success) { - await context.operate( - 'notification', - { - id: await generateNewIdAsync(), - action: 'succeed', - data: {}, - filter: { - id, - }, - }, - { dontCollect: true } - ); - } else { - await context.operate( - 'notification', - { - id: await generateNewIdAsync(), - action: 'fail', - data: { - data2: { - res: result?.error, - }, - }, - filter: { - id, - }, - }, - { dontCollect: true } - ); - } - return 1; - } catch (err: any) { - await context.operate( - 'notification', - { - id: await generateNewIdAsync(), - action: 'fail', - data: { - data2: { - res: err?.message, - }, - }, - filter: { - id, - }, - }, - { dontCollect: true } - ); - console.warn('发邮件消息失败', err); - return 1; - } - } - default: { - assert(channel === 'sms'); - try { - const result = await sendSms({ - messageType: type!, - templateParam: (data as { params: any }).params!, - mobile: (data1 as { mobile: string }).mobile, - }, context) - if (result?.success === true) { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'succeed', - data: { - data2: { - res: result?.res || {} - } - }, - filter: { - id, - } - }, { dontCollect: true }); - } else { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: { - data2: { - res: result?.res || {} - } - }, - filter: { - id, - } - }, { dontCollect: true }); - } - } catch (err) { - await context.operate('notification', { - id: await generateNewIdAsync(), - action: 'fail', - data: { - }, - filter: { - id, - } - }, { dontCollect: true }); - console.warn('发短信消息失败', err); - return 1; - } - } - } + const { channel } = notification; + const handler = getNotificationHandler(channel!); + await handler(notification, context); + return 1; } async function tryCreateSmsNotification(message: EntityDict['message']['Schema'], context: BRC) { @@ -473,8 +181,26 @@ const triggers: Trigger>[] = [ return 1; } - if (message.weight === 'medium' && !smsTried && allFailed) { - // 中级的消息,在其它途径都失败的情况下再发短信 + // 获取所有注册的失败处理器 + const failureHandlers = getNotificationFailureHandlers(); + + if (failureHandlers.length > 0) { + // 如果有注册的失败处理器,执行所有处理器 + let totalResult = 0; + for (const handler of failureHandlers) { + try { + const result = await handler(message as EntityDict['message']['Schema'], context); + totalResult += result; + } catch (err) { + console.error('执行notification失败处理器时出错:', err); + } + } + if (totalResult > 0) { + closeRootMode(); + return totalResult; + } + } else if (message.weight === 'medium' && !smsTried && allFailed) { + // 如果没有注册处理器,走原有逻辑:中级的消息,在其它途径都失败的情况下再发短信 const result = await tryCreateSmsNotification( message as EntityDict['message']['Schema'], context @@ -482,6 +208,7 @@ const triggers: Trigger>[] = [ closeRootMode(); return result; } + // 标识消息发送失败 if (allFailed) { await context.operate( diff --git a/src/utils/message/email.ts b/src/utils/message/email.ts index ca08c0daf..a69874030 100644 --- a/src/utils/message/email.ts +++ b/src/utils/message/email.ts @@ -1,6 +1,8 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import BackendRuntimeContext from '../../context/BackendRuntimeContext'; +import { Router } from '../../entities/Message'; import { EntityDict } from '../../oak-app-domain'; -import { MessageHandler } from './index'; -import { tryMakeEmailNotification } from '../../triggers/message'; +import { ConverterDict, MessageHandler } from './index'; export const emailHandler: MessageHandler = async ({ message, applications, system, messageTypeTemplates, context }) => { const notificationDatas: Omit[] = []; @@ -14,3 +16,31 @@ export const emailHandler: MessageHandler = async ({ message, applications, syst return notificationDatas; }; + + +export async function tryMakeEmailNotification( + message: { + userId?: string; + type?: string; + entity?: string; + router?: Router | null; + entityId?: string; + }, + context: BackendRuntimeContext +) { + const { userId, type, entity, entityId, router } = message; + const converter = ConverterDict[type!] && ConverterDict[type!].toEmail; + if (converter) { + const dispersedData = await converter( + message as EntityDict['message']['OpSchema'], + context + ); + if (dispersedData) { + return { + id: await generateNewIdAsync(), + data: dispersedData, + channel: 'email', + }; + } + } +} \ No newline at end of file diff --git a/src/utils/message/index.ts b/src/utils/message/index.ts index 29b854807..f79e65e13 100644 --- a/src/utils/message/index.ts +++ b/src/utils/message/index.ts @@ -5,6 +5,10 @@ import { CreateOperationData as CreateMessageData } from '../../oak-app-domain/M import { Channel, MessageNotificationConverter } from "../../types/Message"; import { assert } from "oak-domain/lib/utils/assert"; import BackendRuntimeContext from "../../context/BackendRuntimeContext"; +import { wechatMpHandler } from './wechatMp'; +import { wechatPublicHandler } from './wechatPublic'; +import { smsHandler } from './sms'; +import { emailHandler } from './email'; export type MessageHandler = (options: { message: CreateMessageData, @@ -33,3 +37,9 @@ export function getMessageHandler(channel: Channel): MessageHandler { assert(handler, `消息渠道 ${channel} 的处理器未注册`); return handler; } + +// 默认注册所有处理器 +registerMessageHandler('wechatMp', wechatMpHandler); +registerMessageHandler('wechatPublic', wechatPublicHandler); +registerMessageHandler('sms', smsHandler); +registerMessageHandler('email', emailHandler); diff --git a/src/utils/message/sms.ts b/src/utils/message/sms.ts index b8a7e85b1..9728b801e 100644 --- a/src/utils/message/sms.ts +++ b/src/utils/message/sms.ts @@ -1,6 +1,9 @@ +import assert from 'assert'; +import BackendRuntimeContext from '../../context/BackendRuntimeContext'; +import { Router } from '../../entities/Message'; import { EntityDict } from '../../oak-app-domain'; -import { MessageHandler } from './index'; -import { tryMakeSmsNotification } from '../../triggers/message'; +import { ConverterDict, MessageHandler } from './index'; +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; export const smsHandler: MessageHandler = async ({ message, applications, system, messageTypeTemplates, context }) => { const notificationDatas: Omit[] = []; @@ -12,3 +15,39 @@ export const smsHandler: MessageHandler = async ({ message, applications, system return notificationDatas; }; + +export async function tryMakeSmsNotification(message: { + userId?: string; + type?: string; + entity?: string; + router?: Router | null; + entityId?: string; +}, context: BackendRuntimeContext) { + const { userId, type, entity, entityId, router } = message; + assert(userId); + const [mobile] = await context.select('mobile', { + data: { + id: 1, + mobile: 1, + }, + filter: { + userId, + }, + indexFrom: 0, + count: 1, + }, { dontCollect: true }); + if (mobile) { + const converter = ConverterDict[type!] && ConverterDict[type!].toSms; + if (converter) { + const dispersedData = await converter(message as EntityDict['message']['OpSchema'], context); + if (dispersedData) { + return { + id: await generateNewIdAsync(), + data: dispersedData, + channel: 'sms', + data1: mobile, + } as any; + } + } + } +} diff --git a/src/utils/notification/email.ts b/src/utils/notification/email.ts new file mode 100644 index 000000000..b57d4924a --- /dev/null +++ b/src/utils/notification/email.ts @@ -0,0 +1,62 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { EntityDict } from '../../oak-app-domain'; +import { BRC } from '../../types/RuntimeCxt'; +import { sendEmail } from '../email'; +import { NotificationHandler } from './index'; + +export const emailHandler: NotificationHandler = async (notification, context) => { + const { data, id } = notification; + + try { + const result = await sendEmail(data as any, context); + if (result?.success) { + await context.operate( + 'notification', + { + id: await generateNewIdAsync(), + action: 'succeed', + data: {}, + filter: { + id, + }, + }, + { dontCollect: true } + ); + } else { + await context.operate( + 'notification', + { + id: await generateNewIdAsync(), + action: 'fail', + data: { + data2: { + res: result?.error, + }, + }, + filter: { + id, + }, + }, + { dontCollect: true } + ); + } + } catch (err: any) { + await context.operate( + 'notification', + { + id: await generateNewIdAsync(), + action: 'fail', + data: { + data2: { + res: err?.message, + }, + }, + filter: { + id, + }, + }, + { dontCollect: true } + ); + console.warn('发邮件消息失败', err); + } +}; diff --git a/src/utils/notification/index.ts b/src/utils/notification/index.ts new file mode 100644 index 000000000..719a85896 --- /dev/null +++ b/src/utils/notification/index.ts @@ -0,0 +1,45 @@ +import { EntityDict } from "../../oak-app-domain"; +import { BRC } from "../../types/RuntimeCxt"; +import { Channel } from "../../types/Message"; +import { assert } from "oak-domain/lib/utils/assert"; +import { wechatMpHandler } from './wechatMp'; +import { wechatPublicHandler } from './wechatPublic'; +import { smsHandler } from './sms'; +import { emailHandler } from './email'; + +export type NotificationHandler = ( + notification: EntityDict['notification']['OpSchema'], + context: BRC +) => Promise; + +export type NotificationFailureHandler = ( + message: EntityDict['message']['Schema'], + context: BRC +) => Promise; + +const notificationHandlers: Record = {} as Record; +const notificationFailureHandlers: NotificationFailureHandler[] = []; + +export function registerNotificationHandler(channel: Channel, handler: NotificationHandler) { + notificationHandlers[channel] = handler; +} + +export function getNotificationHandler(channel: Channel): NotificationHandler { + const handler = notificationHandlers[channel]; + assert(handler, `通知渠道 ${channel} 的处理器未注册`); + return handler; +} + +export function registerNotificationFailureHandler(handler: NotificationFailureHandler) { + notificationFailureHandlers.push(handler); +} + +export function getNotificationFailureHandlers(): NotificationFailureHandler[] { + return notificationFailureHandlers; +} + +// 默认注册所有处理器 +registerNotificationHandler('wechatMp', wechatMpHandler); +registerNotificationHandler('wechatPublic', wechatPublicHandler); +registerNotificationHandler('sms', smsHandler); +registerNotificationHandler('email', emailHandler); diff --git a/src/utils/notification/sms.ts b/src/utils/notification/sms.ts new file mode 100644 index 000000000..5ae8bd5d0 --- /dev/null +++ b/src/utils/notification/sms.ts @@ -0,0 +1,71 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { EntityDict } from '../../oak-app-domain'; +import { BRC } from '../../types/RuntimeCxt'; +import { sendSms } from '../sms'; +import { NotificationHandler } from './index'; + +export const smsHandler: NotificationHandler = async (notification, context) => { + const { data, data1, id } = notification; + + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + message: { + id: 1, + type: 1, + }, + }, + filter: { + id: notification.messageSystemId, + } + }, { dontCollect: true }); + + const { message } = messageSystem!; + const { type } = message!; + + try { + const result = await sendSms({ + messageType: type!, + templateParam: (data as { params: any }).params!, + mobile: (data1 as { mobile: string }).mobile, + }, context); + + if (result?.success === true) { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'succeed', + data: { + data2: { + res: result?.res || {} + } + }, + filter: { + id, + } + }, { dontCollect: true }); + } else { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: { + data2: { + res: result?.res || {} + } + }, + filter: { + id, + } + }, { dontCollect: true }); + } + } catch (err) { + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + console.warn('发短信消息失败', err); + } +}; diff --git a/src/utils/notification/wechatMp.ts b/src/utils/notification/wechatMp.ts new file mode 100644 index 000000000..fd48dc817 --- /dev/null +++ b/src/utils/notification/wechatMp.ts @@ -0,0 +1,96 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { EntityDict } from '../../oak-app-domain'; +import { BRC } from '../../types/RuntimeCxt'; +import { WechatMpConfig } from '../../oak-app-domain/Application/Schema'; +import WechatSDK, { WechatMpInstance } from 'oak-external-sdk/lib/WechatSDK'; +import { composeUrl } from 'oak-domain/lib/utils/domain'; +import { NotificationHandler } from './index'; + +export const wechatMpHandler: NotificationHandler = async (notification, context) => { + const { data, templateId, messageSystemId, data1, id } = notification; + + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + messageId: 1, + message: { + id: 1, + userId: 1, + router: 1, + type: 1, + }, + system: { + id: 1, + application$system: { + $entity: 'application', + data: { + id: 1, + type: 1, + config: 1, + }, + }, + } + }, + filter: { + id: messageSystemId, + } + }, { dontCollect: true }); + + const { system, message } = messageSystem!; + const { router } = message!; + const { application$system: applications } = system!; + + const app = applications!.find( + ele => ele.type === 'wechatMp' + ); + const { config } = app!; + const { appId, appSecret } = config as WechatMpConfig; + const instance = WechatSDK.getInstance(appId!, 'wechatMp', appSecret) as WechatMpInstance; + + let page; + if (router) { + const pathname = router.pathname; + const url = pathname.startsWith('/') + ? `pages${pathname}/index` + : `pages/${pathname}/index`; + page = composeUrl( + url, + Object.assign({}, router!.props!, router!.state!) + ); + } + + // 根据当前环境决定消息推哪个版本 + const StateDict = { + 'development': 'developer', + 'staging': 'trial', + 'production': 'former', + } + + try { + await instance.sendSubscribedMessage({ + templateId: templateId!, + data: data!, + openId: (data1 as { openId: string }).openId, + page, + state: StateDict[process.env.NODE_ENV as 'development'] as 'developer', + }); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'succeed', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } catch (err) { + console.warn('发微信小程序消息失败', err); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } +}; diff --git a/src/utils/notification/wechatPublic.ts b/src/utils/notification/wechatPublic.ts new file mode 100644 index 000000000..d57f34f72 --- /dev/null +++ b/src/utils/notification/wechatPublic.ts @@ -0,0 +1,136 @@ +import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; +import { EntityDict } from '../../oak-app-domain'; +import { BRC } from '../../types/RuntimeCxt'; +import { WechatPublicConfig } from '../../oak-app-domain/Application/Schema'; +import WechatSDK, { WechatPublicInstance } from 'oak-external-sdk/lib/WechatSDK'; +import { composeUrl } from 'oak-domain/lib/utils/domain'; +import { composeDomainUrl } from '../domain'; +import { NotificationHandler } from './index'; + +export const wechatPublicHandler: NotificationHandler = async (notification, context) => { + const { data, templateId, messageSystemId, data1, id } = notification; + + const [messageSystem] = await context.select('messageSystem', { + data: { + id: 1, + messageId: 1, + message: { + id: 1, + userId: 1, + router: 1, + type: 1, + }, + system: { + id: 1, + application$system: { + $entity: 'application', + data: { + id: 1, + type: 1, + config: 1, + }, + }, + } + }, + filter: { + id: messageSystemId, + } + }, { dontCollect: true }); + + const { system, message } = messageSystem!; + const { router } = message!; + const { application$system: applications } = system!; + + const app = applications!.find( + ele => ele.type === 'wechatPublic' + ); + const { config, id: applicationId } = app!; + const { appId, appSecret } = config as WechatPublicConfig; + + const [domain] = await context.select( + 'domain', + { + data: { + id: 1, + url: 1, + apiPath: 1, + protocol: 1, + port: 1, + }, + filter: { + system: { + application$system: { + id: applicationId, + }, + }, + }, + }, + { dontCollect: true } + ); + + const instance = WechatSDK.getInstance(appId!, 'wechatPublic', appSecret) as WechatPublicInstance; + const { openId, wechatMpAppId } = data1 as { + openId: string, + wechatMpAppId?: string, + }; + + let page; + // message 用户不需要跳转页面 + if (router) { + const pathname = router.pathname; + + if (wechatMpAppId) { + const url = pathname.startsWith('/') + ? `pages${pathname}/index` + : `pages/${pathname}/index`; + + page = composeUrl( + url, + Object.assign({}, router!.props!, router!.state!) + ); + } else { + const url = composeDomainUrl( + domain as EntityDict['domain']['Schema'], + pathname + ); + page = composeUrl( + url, + Object.assign({}, router!.props!, router!.state!) + ); + } + } + + try { + await instance.sendTemplateMessage({ + openId, + templateId: templateId!, + url: !wechatMpAppId ? page : undefined, + data: data!, + miniProgram: wechatMpAppId + ? { + appid: wechatMpAppId, + pagepath: page as string, + } + : undefined, + clientMsgId: id, + }); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'succeed', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } catch (err) { + console.warn('发微信公众号消息失败', err); + await context.operate('notification', { + id: await generateNewIdAsync(), + action: 'fail', + data: {}, + filter: { + id, + } + }, { dontCollect: true }); + } +};