634 lines
21 KiB
TypeScript
634 lines
21 KiB
TypeScript
import { BadRequestException, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
import {
|
|
WECOM_BIZ_TODO_PUSH_NAME,
|
|
WECOM_DEAL_BROADCAST_PUSH_NAME,
|
|
WECOM_PUSH_CONDITIONS,
|
|
WECOM_PUSH_DEFAULT_ALERT_CONDITIONS,
|
|
WECOM_PUSH_DEFAULT_BIZ_TODO_CONDITIONS,
|
|
WECOM_PUSH_DEFAULT_DEAL_BROADCAST_CONDITIONS,
|
|
WECOM_PUSH_DEFAULT_DEV_DISPATCH_CONDITIONS,
|
|
WECOM_PUSH_DEFAULT_SETTLEMENT_CONDITIONS,
|
|
WECOM_PUSH_DEFAULT_STORE_AUDIT_CONDITIONS,
|
|
WECOM_SETTLEMENT_PUSH_NAME,
|
|
WECOM_STORE_AUDIT_PLACEHOLDER_WEBHOOK,
|
|
WECOM_STORE_AUDIT_PUSH_NAME,
|
|
WECOM_TEMPLATE_EVENT_KEYS,
|
|
maskWecomWebhookUrl,
|
|
parseWecomPushConditions,
|
|
type WecomMessagePushDto,
|
|
type WecomPushCondition,
|
|
type WecomPushTemplateDto,
|
|
type WecomTemplateEventKey,
|
|
} from '@dukang/shared-types';
|
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
|
import { applyWecomAtMentionInContent } from '../../modules/dev-plan/dev-plan-wecom-mention.util';
|
|
import {
|
|
WECOM_PUSH_TEMPLATE_DEFAULTS,
|
|
WECOM_PUSH_TEMPLATE_LEGACY_BODIES,
|
|
buildHqHandleUrl,
|
|
getDefaultTemplate,
|
|
renderWecomTemplate,
|
|
} from './wecom-push-template.defaults';
|
|
|
|
type PushRow = {
|
|
id: bigint;
|
|
name: string;
|
|
avatarUrl: string | null;
|
|
webhookUrl: string;
|
|
enabled: boolean;
|
|
mentionWecomUserId: string | null;
|
|
pushConditions: string;
|
|
sortOrder: number;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
};
|
|
|
|
type TemplateRow = {
|
|
id: bigint;
|
|
eventKey: string;
|
|
title: string;
|
|
body: string;
|
|
handleLabel: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
};
|
|
|
|
@Injectable()
|
|
export class WecomMessagePushService implements OnModuleInit {
|
|
private readonly logger = new Logger(WecomMessagePushService.name);
|
|
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async onModuleInit(): Promise<void> {
|
|
try {
|
|
await this.ensureDefaults();
|
|
} catch (e) {
|
|
this.logger.warn(
|
|
`wecom message push ensureDefaults failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** 表空时从 .env / 旧设置迁移;并确保门店审核 / 业务待办 / 成交播报 / 结算通知 / 模板缺行 */
|
|
async ensureDefaults(): Promise<void> {
|
|
const count = await this.prisma.wecomMessagePush.count();
|
|
if (count === 0) {
|
|
const alertUrl = (process.env.WECOM_ALERT_WEBHOOK_URL || '').trim();
|
|
if (alertUrl) {
|
|
const alertEnabled = process.env.WECOM_ALERT_ENABLED !== 'false';
|
|
await this.prisma.wecomMessagePush.create({
|
|
data: {
|
|
name: '运营告警',
|
|
webhookUrl: alertUrl,
|
|
enabled: alertEnabled,
|
|
pushConditions: JSON.stringify(WECOM_PUSH_DEFAULT_ALERT_CONDITIONS),
|
|
sortOrder: 0,
|
|
},
|
|
});
|
|
this.logger.log('seeded wecom message push: 运营告警');
|
|
}
|
|
|
|
let devWebhook: string | null = null;
|
|
let devUserId: string | null = null;
|
|
let devEnabled = false;
|
|
try {
|
|
const rows = await this.prisma.$queryRawUnsafe<
|
|
Array<{
|
|
task_dispatch_webhook_url: string | null;
|
|
task_dispatch_wecom_user_id: string | null;
|
|
task_dispatch_enabled: number | boolean | null;
|
|
}>
|
|
>(
|
|
'SELECT task_dispatch_webhook_url, task_dispatch_wecom_user_id, task_dispatch_enabled FROM dev_plan_settings LIMIT 1',
|
|
);
|
|
const row = rows[0];
|
|
if (row) {
|
|
devWebhook = row.task_dispatch_webhook_url;
|
|
devUserId = row.task_dispatch_wecom_user_id;
|
|
devEnabled = !!row.task_dispatch_enabled;
|
|
}
|
|
} catch {
|
|
// 列已迁移删除,跳过
|
|
}
|
|
|
|
if (devWebhook?.trim()) {
|
|
await this.prisma.wecomMessagePush.create({
|
|
data: {
|
|
name: '开发任务派发',
|
|
webhookUrl: devWebhook.trim(),
|
|
enabled: devEnabled,
|
|
mentionWecomUserId: devUserId?.trim() || null,
|
|
pushConditions: JSON.stringify(WECOM_PUSH_DEFAULT_DEV_DISPATCH_CONDITIONS),
|
|
sortOrder: 10,
|
|
},
|
|
});
|
|
this.logger.log('seeded wecom message push: 开发任务派发');
|
|
}
|
|
}
|
|
|
|
await this.ensureNamedPush(
|
|
WECOM_STORE_AUDIT_PUSH_NAME,
|
|
WECOM_PUSH_DEFAULT_STORE_AUDIT_CONDITIONS,
|
|
20,
|
|
process.env.WECOM_STORE_AUDIT_WEBHOOK_URL,
|
|
);
|
|
await this.ensureNamedPush(
|
|
WECOM_BIZ_TODO_PUSH_NAME,
|
|
WECOM_PUSH_DEFAULT_BIZ_TODO_CONDITIONS,
|
|
25,
|
|
process.env.WECOM_BIZ_TODO_WEBHOOK_URL,
|
|
);
|
|
await this.ensureNamedPush(
|
|
WECOM_DEAL_BROADCAST_PUSH_NAME,
|
|
WECOM_PUSH_DEFAULT_DEAL_BROADCAST_CONDITIONS,
|
|
30,
|
|
process.env.WECOM_DEAL_BROADCAST_WEBHOOK_URL,
|
|
);
|
|
await this.ensureNamedPush(
|
|
WECOM_SETTLEMENT_PUSH_NAME,
|
|
WECOM_PUSH_DEFAULT_SETTLEMENT_CONDITIONS,
|
|
35,
|
|
process.env.WECOM_SETTLEMENT_WEBHOOK_URL,
|
|
);
|
|
await this.ensureTemplates();
|
|
}
|
|
|
|
private async ensureNamedPush(
|
|
name: string,
|
|
conditions: WecomPushCondition[],
|
|
sortOrder: number,
|
|
envUrl?: string,
|
|
): Promise<void> {
|
|
const existing = await this.prisma.wecomMessagePush.findFirst({ where: { name } });
|
|
if (existing) return;
|
|
|
|
const conditionsJson = JSON.stringify(conditions);
|
|
const url = (envUrl || '').trim();
|
|
if (url) {
|
|
await this.prisma.wecomMessagePush.create({
|
|
data: {
|
|
name,
|
|
webhookUrl: url,
|
|
enabled: true,
|
|
pushConditions: conditionsJson,
|
|
sortOrder,
|
|
},
|
|
});
|
|
this.logger.log(`seeded wecom message push: ${name} (from env)`);
|
|
return;
|
|
}
|
|
|
|
await this.prisma.wecomMessagePush.create({
|
|
data: {
|
|
name,
|
|
webhookUrl: WECOM_STORE_AUDIT_PLACEHOLDER_WEBHOOK,
|
|
enabled: false,
|
|
pushConditions: conditionsJson,
|
|
sortOrder,
|
|
},
|
|
});
|
|
this.logger.log(`seeded wecom message push: ${name} (placeholder, disabled)`);
|
|
}
|
|
|
|
/** 插入缺失 eventKey;仅当 body 仍为上一版默认文案时升级,不覆盖 HQ 手改 */
|
|
async ensureTemplates(): Promise<void> {
|
|
for (const def of WECOM_PUSH_TEMPLATE_DEFAULTS) {
|
|
const existing = await this.prisma.wecomPushTemplate.findUnique({
|
|
where: { eventKey: def.eventKey },
|
|
});
|
|
if (!existing) {
|
|
await this.prisma.wecomPushTemplate.create({
|
|
data: {
|
|
eventKey: def.eventKey,
|
|
title: def.title,
|
|
body: def.body,
|
|
handleLabel: def.handleLabel,
|
|
},
|
|
});
|
|
this.logger.log(`seeded wecom push template: ${def.eventKey}`);
|
|
continue;
|
|
}
|
|
const legacy = WECOM_PUSH_TEMPLATE_LEGACY_BODIES[def.eventKey];
|
|
if (legacy && existing.body === legacy) {
|
|
await this.prisma.wecomPushTemplate.update({
|
|
where: { eventKey: def.eventKey },
|
|
data: {
|
|
title: def.title,
|
|
body: def.body,
|
|
handleLabel: def.handleLabel,
|
|
},
|
|
});
|
|
this.logger.log(`upgraded wecom push template from legacy default: ${def.eventKey}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async listMatchingPushes(eventKey: WecomPushCondition): Promise<PushRow[]> {
|
|
const rows = await this.prisma.wecomMessagePush.findMany({
|
|
where: { enabled: true },
|
|
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
|
});
|
|
return rows.filter((r) => parseWecomPushConditions(r.pushConditions).includes(eventKey));
|
|
}
|
|
|
|
async hasEnabledPushes(eventKey: WecomPushCondition): Promise<boolean> {
|
|
const pushes = await this.listMatchingPushes(eventKey);
|
|
return pushes.length > 0;
|
|
}
|
|
|
|
/**
|
|
* 读 HQ 模板 → 插值 → 补快链 → 按条件路由推送。
|
|
* 失败只打日志,不抛给业务。
|
|
*/
|
|
async dispatchEvent(
|
|
eventKey: WecomTemplateEventKey,
|
|
vars: Record<string, string | number | null | undefined>,
|
|
options?: { applyMention?: boolean; handlePath?: string },
|
|
): Promise<number> {
|
|
try {
|
|
const content = await this.renderEventContent(eventKey, vars, options?.handlePath);
|
|
return await this.dispatchMarkdown(eventKey, content, {
|
|
applyMention: options?.applyMention ?? false,
|
|
});
|
|
} catch (e) {
|
|
this.logger.warn(
|
|
`dispatchEvent(${eventKey}) failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
async renderEventContent(
|
|
eventKey: WecomTemplateEventKey,
|
|
vars: Record<string, string | number | null | undefined>,
|
|
handlePath?: string,
|
|
): Promise<string> {
|
|
const row = await this.prisma.wecomPushTemplate.findUnique({ where: { eventKey } });
|
|
const def = getDefaultTemplate(eventKey);
|
|
const body = row?.body || def?.body || `**${eventKey}**`;
|
|
const handleLabel = row?.handleLabel || def?.handleLabel || '去处理';
|
|
|
|
const merged: Record<string, string | number | null | undefined> = {
|
|
...vars,
|
|
handleLabel: vars.handleLabel ?? handleLabel,
|
|
time:
|
|
vars.time ??
|
|
new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
|
};
|
|
|
|
if (handlePath && !merged.handleUrl) {
|
|
merged.handleUrl = buildHqHandleUrl(handlePath);
|
|
}
|
|
|
|
let content = renderWecomTemplate(body, merged).trim();
|
|
const url = String(merged.handleUrl || '').trim();
|
|
if (url && !content.includes(url) && !/\{\{handleUrl\}\}/.test(body)) {
|
|
content = `${content}\n[${handleLabel}](${url})`;
|
|
}
|
|
return content;
|
|
}
|
|
|
|
/** 向所有匹配 eventKey 的启用推送发送 markdown;返回成功发送数 */
|
|
async dispatchMarkdown(
|
|
eventKey: WecomPushCondition,
|
|
content: string,
|
|
options?: { applyMention?: boolean },
|
|
): Promise<number> {
|
|
const pushes = await this.listMatchingPushes(eventKey);
|
|
if (!pushes.length) return 0;
|
|
|
|
const applyMention = options?.applyMention !== false;
|
|
let sent = 0;
|
|
for (const push of pushes) {
|
|
let text = content.trim();
|
|
if (applyMention && push.mentionWecomUserId) {
|
|
text = applyWecomAtMentionInContent(text, push.mentionWecomUserId);
|
|
}
|
|
const ok = await this.sendMarkdownToWebhook(push.webhookUrl, text);
|
|
if (ok) sent += 1;
|
|
}
|
|
return sent;
|
|
}
|
|
|
|
async dispatchMarkdownOrThrow(
|
|
eventKey: WecomPushCondition,
|
|
content: string,
|
|
options?: { applyMention?: boolean },
|
|
): Promise<number> {
|
|
const sent = await this.dispatchMarkdown(eventKey, content, options);
|
|
if (sent === 0) {
|
|
throw new BadRequestException(
|
|
`没有已启用且勾选「${eventKey}」条件的消息推送,请在 HQ「企微机器人 → 消息推送」中配置`,
|
|
);
|
|
}
|
|
return sent;
|
|
}
|
|
|
|
async sendMarkdownToWebhook(webhookUrl: string, content: string): Promise<boolean> {
|
|
const url = (webhookUrl || '').trim();
|
|
if (!url) return false;
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
msgtype: 'markdown',
|
|
markdown: { content: content.slice(0, 4000) },
|
|
}),
|
|
});
|
|
const data = (await res.json().catch(() => ({}))) as {
|
|
errcode?: number;
|
|
errmsg?: string;
|
|
};
|
|
if (!res.ok || (data.errcode != null && data.errcode !== 0)) {
|
|
this.logger.warn(
|
|
`wecom message push failed: HTTP ${res.status} errcode=${data.errcode} ${data.errmsg ?? ''}`,
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch (e) {
|
|
this.logger.warn(
|
|
`wecom message push network error: ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async sendTest(id: bigint): Promise<{ ok: boolean; message: string }> {
|
|
const row = await this.prisma.wecomMessagePush.findUnique({ where: { id } });
|
|
if (!row) throw new BadRequestException('消息推送不存在');
|
|
if (!row.webhookUrl.trim()) {
|
|
return { ok: false, message: 'Webhook URL 未配置' };
|
|
}
|
|
|
|
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
|
let content = `**消息推送测试 · ${row.name}**\n时间:${now}`;
|
|
if (row.mentionWecomUserId) {
|
|
content = applyWecomAtMentionInContent(content, row.mentionWecomUserId);
|
|
}
|
|
const ok = await this.sendMarkdownToWebhook(row.webhookUrl, content);
|
|
return ok
|
|
? { ok: true, message: '已发送测试消息,请查看企微群' }
|
|
: { ok: false, message: 'Webhook 调用失败,请检查 URL 或 API 日志' };
|
|
}
|
|
|
|
// ── templates CRUD ──
|
|
|
|
async listTemplates(): Promise<WecomPushTemplateDto[]> {
|
|
await this.ensureTemplates();
|
|
const rows = await this.prisma.wecomPushTemplate.findMany({
|
|
orderBy: { eventKey: 'asc' },
|
|
});
|
|
return rows.map((r) => this.templateToDto(r));
|
|
}
|
|
|
|
async getTemplate(eventKey: string): Promise<WecomPushTemplateDto> {
|
|
this.assertTemplateKey(eventKey);
|
|
await this.ensureTemplates();
|
|
const row = await this.prisma.wecomPushTemplate.findUnique({ where: { eventKey } });
|
|
if (!row) throw new BadRequestException('模板不存在');
|
|
return this.templateToDto(row);
|
|
}
|
|
|
|
async updateTemplate(
|
|
eventKey: string,
|
|
data: { title?: string; body?: string; handleLabel?: string },
|
|
): Promise<WecomPushTemplateDto> {
|
|
this.assertTemplateKey(eventKey);
|
|
await this.ensureTemplates();
|
|
const existing = await this.prisma.wecomPushTemplate.findUnique({ where: { eventKey } });
|
|
if (!existing) throw new BadRequestException('模板不存在');
|
|
|
|
const title = data.title != null ? String(data.title).trim() : undefined;
|
|
const body = data.body != null ? String(data.body).trim() : undefined;
|
|
const handleLabel =
|
|
data.handleLabel != null ? String(data.handleLabel).trim() || '去处理' : undefined;
|
|
if (title !== undefined && !title) throw new BadRequestException('标题不能为空');
|
|
if (body !== undefined && !body) throw new BadRequestException('正文不能为空');
|
|
|
|
const row = await this.prisma.wecomPushTemplate.update({
|
|
where: { eventKey },
|
|
data: {
|
|
...(title !== undefined ? { title } : {}),
|
|
...(body !== undefined ? { body } : {}),
|
|
...(handleLabel !== undefined ? { handleLabel } : {}),
|
|
},
|
|
});
|
|
return this.templateToDto(row);
|
|
}
|
|
|
|
async resetTemplate(eventKey: string): Promise<WecomPushTemplateDto> {
|
|
this.assertTemplateKey(eventKey);
|
|
const def = getDefaultTemplate(eventKey);
|
|
if (!def) throw new BadRequestException('无默认模板');
|
|
await this.ensureTemplates();
|
|
const row = await this.prisma.wecomPushTemplate.upsert({
|
|
where: { eventKey },
|
|
create: {
|
|
eventKey: def.eventKey,
|
|
title: def.title,
|
|
body: def.body,
|
|
handleLabel: def.handleLabel,
|
|
},
|
|
update: {
|
|
title: def.title,
|
|
body: def.body,
|
|
handleLabel: def.handleLabel,
|
|
},
|
|
});
|
|
return this.templateToDto(row);
|
|
}
|
|
|
|
/** 用示例变量渲染并推到勾选了该事件的启用群 */
|
|
async testTemplate(eventKey: string): Promise<{ ok: boolean; message: string; preview: string }> {
|
|
this.assertTemplateKey(eventKey);
|
|
const sample = this.sampleVars(eventKey);
|
|
const preview = await this.renderEventContent(
|
|
eventKey,
|
|
sample.vars,
|
|
sample.handlePath,
|
|
);
|
|
const sent = await this.dispatchMarkdown(eventKey, preview, { applyMention: false });
|
|
if (sent === 0) {
|
|
return {
|
|
ok: false,
|
|
message: '没有已启用且勾选该事件的消息推送,请先配置 webhook',
|
|
preview,
|
|
};
|
|
}
|
|
return { ok: true, message: `已发送至 ${sent} 个推送`, preview };
|
|
}
|
|
|
|
private sampleVars(eventKey: WecomTemplateEventKey): {
|
|
vars: Record<string, string>;
|
|
handlePath: string;
|
|
} {
|
|
const samples: Record<WecomTemplateEventKey, { vars: Record<string, string>; handlePath: string }> = {
|
|
'order.paid': {
|
|
vars: {
|
|
orderNo: 'DK202608200001',
|
|
payAmount: '199.00',
|
|
cityName: '郑州',
|
|
skuSummary: '杜康原浆 52度 瓶×2',
|
|
deliveryLabel: '同城',
|
|
receiverInfo: '张三 13800008000 河南省郑州市金水区示例路1号',
|
|
phoneMasked: '138****8000',
|
|
},
|
|
handlePath: '/orders?orderNo=DK202608200001',
|
|
},
|
|
'redeem.success': {
|
|
vars: {
|
|
redeemNo: 'RD202608200001',
|
|
amount: '88.00',
|
|
storeName: '示例门店',
|
|
channel: '扫码',
|
|
userName: '好客用户',
|
|
userPhone: '138****8000',
|
|
},
|
|
handlePath: '/redeem-records?redeemNo=RD202608200001',
|
|
},
|
|
'store.audit_pending': {
|
|
vars: {
|
|
storeName: '示例门店',
|
|
cityName: '郑州',
|
|
submitter: '郑州某某商贸 · 张三',
|
|
partnerLabel: '郑州某某商贸 · 张三',
|
|
action: '新建',
|
|
storeId: '1',
|
|
},
|
|
handlePath: '/stores?auditStatus=PENDING&storeId=1',
|
|
},
|
|
'store.package_audit_pending': {
|
|
vars: {
|
|
storeName: '示例门店',
|
|
cityName: '郑州',
|
|
submitter: '郑州某某商贸 · 张三',
|
|
packageCount: '3',
|
|
requestId: '1',
|
|
},
|
|
handlePath: '/store-package-audits?requestId=1',
|
|
},
|
|
'store.info_change_pending': {
|
|
vars: {
|
|
storeName: '示例门店',
|
|
cityName: '郑州',
|
|
submitter: '门店 · 李四',
|
|
changedFields: '门店名称、详细地址',
|
|
requestId: '1',
|
|
},
|
|
handlePath: '/store-package-audits?tab=info&infoRequestId=1',
|
|
},
|
|
'store.withdraw_pending': {
|
|
vars: {
|
|
storeName: '示例门店',
|
|
withdrawNo: 'SW202608200001',
|
|
amount: '500.00',
|
|
payoutCount: '5',
|
|
storeId: '1',
|
|
},
|
|
handlePath: '/finance/store-bills?kind=WITHDRAW&status=PENDING_REVIEW&storeId=1',
|
|
},
|
|
'store.withdraw_approved': {
|
|
vars: {
|
|
storeName: '示例门店',
|
|
withdrawNo: 'SW202608200001',
|
|
amount: '500.00',
|
|
bankAccount: '张三 / 郑州支行 / 6222000011112222',
|
|
storeId: '1',
|
|
},
|
|
handlePath: '/finance/store-bills?kind=WITHDRAW&status=PAID&storeId=1',
|
|
},
|
|
'invoice.pending': {
|
|
vars: {
|
|
invoiceNo: 'IV202608200001',
|
|
orderNo: 'DK202608200001',
|
|
payAmount: '199.00',
|
|
titleName: '示例公司',
|
|
phoneMasked: '138****8000',
|
|
},
|
|
handlePath: '/invoices?status=PENDING&invoiceNo=IV202608200001',
|
|
},
|
|
'finance.store_bill': {
|
|
vars: {
|
|
period: '2026-08-25',
|
|
billCount: '2',
|
|
billSummary: '示例门店 ¥300.00 张三 / 郑州支行 / 6222000011112222',
|
|
},
|
|
handlePath: '/finance/store-bills?kind=T1_BILL',
|
|
},
|
|
'finance.partner_bill': {
|
|
vars: {
|
|
period: '2026-07',
|
|
billCount: '1',
|
|
billSummary: '郑州某某商贸 ¥120.00 张三 / 郑州支行 / 6222000011112222',
|
|
},
|
|
handlePath: '/finance/partner-bills',
|
|
},
|
|
'finance.winery_bill': {
|
|
vars: {
|
|
period: '2026-08-23',
|
|
amount: '1990.00',
|
|
orderCount: '8',
|
|
bankAccount: '杜康酒厂 / 洛阳支行 / 6222000099998888',
|
|
},
|
|
handlePath: '/finance/winery-bills',
|
|
},
|
|
'finance.logistics_bill': {
|
|
vars: {
|
|
period: '2026-07',
|
|
billCount: '1',
|
|
billSummary: '小飞侠 ¥86.00 承运商 / 郑州支行 / 6222000033334444',
|
|
},
|
|
handlePath: '/finance/logistics-bills',
|
|
},
|
|
};
|
|
return samples[eventKey];
|
|
}
|
|
|
|
private assertTemplateKey(eventKey: string): asserts eventKey is WecomTemplateEventKey {
|
|
if (!(WECOM_TEMPLATE_EVENT_KEYS as readonly string[]).includes(eventKey)) {
|
|
throw new BadRequestException(`无效模板事件:${eventKey}`);
|
|
}
|
|
}
|
|
|
|
templateToDto(row: TemplateRow): WecomPushTemplateDto {
|
|
return {
|
|
id: row.id.toString(),
|
|
eventKey: row.eventKey as WecomTemplateEventKey,
|
|
title: row.title,
|
|
body: row.body,
|
|
handleLabel: row.handleLabel,
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
toDto(row: PushRow): WecomMessagePushDto {
|
|
return {
|
|
id: row.id.toString(),
|
|
name: row.name,
|
|
avatarUrl: row.avatarUrl,
|
|
webhookUrl: row.webhookUrl,
|
|
webhookUrlMasked: maskWecomWebhookUrl(row.webhookUrl),
|
|
enabled: row.enabled,
|
|
mentionWecomUserId: row.mentionWecomUserId,
|
|
pushConditions: parseWecomPushConditions(row.pushConditions),
|
|
sortOrder: row.sortOrder,
|
|
createdAt: row.createdAt.toISOString(),
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
validatePushConditions(conditions: string[]): WecomPushCondition[] {
|
|
const parsed = parseWecomPushConditions(conditions);
|
|
if (!parsed.length) {
|
|
throw new BadRequestException('请至少勾选一项推送条件');
|
|
}
|
|
const valid = new Set<string>(WECOM_PUSH_CONDITIONS);
|
|
for (const c of conditions) {
|
|
if (!valid.has(c)) throw new BadRequestException(`无效推送条件:${c}`);
|
|
}
|
|
return parsed;
|
|
}
|
|
}
|