notification.service.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import { Injectable, inject } from '@angular/core';
  2. import { HttpClient } from '@angular/common/http';
  3. import { Observable, of, BehaviorSubject } from 'rxjs';
  4. import { delay, map, catchError } from 'rxjs/operators';
  5. // 通知类型枚举
  6. export enum NotificationType {
  7. PAYMENT_COMPLETED = 'payment_completed',
  8. IMAGE_UNLOCKED = 'image_unlocked',
  9. SETTLEMENT_CONFIRMED = 'settlement_confirmed',
  10. REMINDER = 'reminder',
  11. SYSTEM = 'system'
  12. }
  13. // 通知渠道枚举
  14. export enum NotificationChannel {
  15. SMS = 'sms',
  16. EMAIL = 'email',
  17. WECHAT = 'wechat',
  18. PUSH = 'push',
  19. IN_APP = 'in_app'
  20. }
  21. // 通知接口
  22. export interface Notification {
  23. id: string;
  24. type: NotificationType;
  25. title: string;
  26. content: string;
  27. recipient: string;
  28. channels: NotificationChannel[];
  29. templateData?: any;
  30. scheduledAt?: Date;
  31. sentAt?: Date;
  32. status: 'pending' | 'sent' | 'failed' | 'cancelled';
  33. retryCount?: number;
  34. metadata?: any;
  35. }
  36. // 通知模板接口
  37. export interface NotificationTemplate {
  38. id: string;
  39. type: NotificationType;
  40. name: string;
  41. title: string;
  42. content: string;
  43. channels: NotificationChannel[];
  44. variables: string[];
  45. }
  46. // 通知发送结果接口
  47. export interface NotificationResult {
  48. success: boolean;
  49. notificationId: string;
  50. sentChannels: NotificationChannel[];
  51. failedChannels: NotificationChannel[];
  52. error?: string;
  53. }
  54. @Injectable({
  55. providedIn: 'root'
  56. })
  57. export class NotificationService {
  58. private http = inject(HttpClient);
  59. private notifications$ = new BehaviorSubject<Notification[]>([]);
  60. // 预定义通知模板
  61. private templates: NotificationTemplate[] = [
  62. {
  63. id: 'payment_completed',
  64. type: NotificationType.PAYMENT_COMPLETED,
  65. name: '支付完成通知',
  66. title: '🎉 尾款已到账,大图已解锁!',
  67. content: `
  68. 亲爱的客户,您好!
  69. 您的尾款支付已确认到账:
  70. • 支付方式:{{paymentMethod}}
  71. • 支付金额:¥{{amount}}
  72. • 处理时间:{{processedAt}}
  73. 🎨 高清渲染图已为您解锁,您现在可以:
  74. • 下载所有高清渲染图
  75. • 查看全景漫游链接
  76. • 获取设计方案文件
  77. 感谢您选择银三色摄影工作室!
  78. 如有任何问题,请随时联系我们。
  79. `,
  80. channels: [NotificationChannel.SMS, NotificationChannel.WECHAT, NotificationChannel.IN_APP],
  81. variables: ['paymentMethod', 'amount', 'processedAt', 'customerName', 'projectName']
  82. },
  83. {
  84. id: 'image_unlocked',
  85. type: NotificationType.IMAGE_UNLOCKED,
  86. name: '大图解锁通知',
  87. title: '📸 您的高清渲染图已解锁',
  88. content: `
  89. {{customerName}},您好!
  90. 项目"{{projectName}}"的高清渲染图已为您解锁:
  91. • 解锁图片数量:{{imageCount}}张
  92. • 图片分辨率:{{resolution}}
  93. • 下载有效期:{{validUntil}}
  94. 立即下载:{{downloadLink}}
  95. 银三色摄影工作室
  96. `,
  97. channels: [NotificationChannel.SMS, NotificationChannel.EMAIL, NotificationChannel.IN_APP],
  98. variables: ['customerName', 'projectName', 'imageCount', 'resolution', 'validUntil', 'downloadLink']
  99. },
  100. {
  101. id: 'settlement_reminder',
  102. type: NotificationType.REMINDER,
  103. name: '结算提醒',
  104. title: '💰 尾款结算提醒',
  105. content: `
  106. {{customerName}},您好!
  107. 您的项目"{{projectName}}"已完成,请及时结算尾款:
  108. • 应付金额:¥{{amount}}
  109. • 截止日期:{{dueDate}}
  110. 支付完成后,我们将立即为您解锁高清渲染图。
  111. 银三色摄影工作室
  112. `,
  113. channels: [NotificationChannel.SMS, NotificationChannel.WECHAT],
  114. variables: ['customerName', 'projectName', 'amount', 'dueDate']
  115. }
  116. ];
  117. /**
  118. * 发送支付完成通知
  119. */
  120. sendPaymentCompletedNotification(data: {
  121. recipient: string;
  122. paymentMethod: string;
  123. amount: number;
  124. customerName: string;
  125. projectName: string;
  126. channels?: NotificationChannel[];
  127. }): Observable<NotificationResult> {
  128. console.log('发送支付完成通知:', data);
  129. const template = this.getTemplate(NotificationType.PAYMENT_COMPLETED);
  130. if (!template) {
  131. return of({
  132. success: false,
  133. notificationId: '',
  134. sentChannels: [],
  135. failedChannels: [],
  136. error: '未找到通知模板'
  137. });
  138. }
  139. const notification: Notification = {
  140. id: `notification_${Date.now()}`,
  141. type: NotificationType.PAYMENT_COMPLETED,
  142. title: template.title,
  143. content: this.renderTemplate(template.content, {
  144. paymentMethod: data.paymentMethod,
  145. amount: data.amount.toString(),
  146. processedAt: new Date().toLocaleString(),
  147. customerName: data.customerName,
  148. projectName: data.projectName
  149. }),
  150. recipient: data.recipient,
  151. channels: data.channels || template.channels,
  152. templateData: data,
  153. status: 'pending'
  154. };
  155. return this.sendNotification(notification);
  156. }
  157. /**
  158. * 发送大图解锁通知
  159. */
  160. sendImageUnlockedNotification(data: {
  161. recipient: string;
  162. customerName: string;
  163. projectName: string;
  164. imageCount: number;
  165. resolution: string;
  166. validUntil: string;
  167. downloadLink: string;
  168. channels?: NotificationChannel[];
  169. }): Observable<NotificationResult> {
  170. console.log('发送大图解锁通知:', data);
  171. const template = this.getTemplate(NotificationType.IMAGE_UNLOCKED);
  172. if (!template) {
  173. return of({
  174. success: false,
  175. notificationId: '',
  176. sentChannels: [],
  177. failedChannels: [],
  178. error: '未找到通知模板'
  179. });
  180. }
  181. const notification: Notification = {
  182. id: `notification_${Date.now()}`,
  183. type: NotificationType.IMAGE_UNLOCKED,
  184. title: template.title,
  185. content: this.renderTemplate(template.content, data),
  186. recipient: data.recipient,
  187. channels: data.channels || template.channels,
  188. templateData: data,
  189. status: 'pending'
  190. };
  191. return this.sendNotification(notification);
  192. }
  193. /**
  194. * 发送结算提醒通知
  195. */
  196. sendSettlementReminderNotification(data: {
  197. recipient: string;
  198. customerName: string;
  199. projectName: string;
  200. amount: number;
  201. dueDate: string;
  202. channels?: NotificationChannel[];
  203. }): Observable<NotificationResult> {
  204. console.log('发送结算提醒通知:', data);
  205. const template = this.getTemplate(NotificationType.REMINDER);
  206. if (!template) {
  207. return of({
  208. success: false,
  209. notificationId: '',
  210. sentChannels: [],
  211. failedChannels: [],
  212. error: '未找到通知模板'
  213. });
  214. }
  215. const notification: Notification = {
  216. id: `notification_${Date.now()}`,
  217. type: NotificationType.REMINDER,
  218. title: template.title,
  219. content: this.renderTemplate(template.content, {
  220. customerName: data.customerName,
  221. projectName: data.projectName,
  222. amount: data.amount.toString(),
  223. dueDate: data.dueDate
  224. }),
  225. recipient: data.recipient,
  226. channels: data.channels || template.channels,
  227. templateData: data,
  228. status: 'pending'
  229. };
  230. return this.sendNotification(notification);
  231. }
  232. /**
  233. * 发送通知
  234. */
  235. private sendNotification(notification: Notification): Observable<NotificationResult> {
  236. console.log('发送通知:', notification);
  237. // 模拟发送过程
  238. return of(null).pipe(
  239. delay(1500), // 模拟网络延迟
  240. map(() => {
  241. // 模拟发送结果
  242. const successRate = 0.95; // 95%成功率
  243. const success = Math.random() < successRate;
  244. if (success) {
  245. notification.status = 'sent';
  246. notification.sentAt = new Date();
  247. // 添加到通知历史
  248. this.addToNotificationHistory(notification);
  249. return {
  250. success: true,
  251. notificationId: notification.id,
  252. sentChannels: notification.channels,
  253. failedChannels: [],
  254. };
  255. } else {
  256. notification.status = 'failed';
  257. notification.retryCount = (notification.retryCount || 0) + 1;
  258. return {
  259. success: false,
  260. notificationId: notification.id,
  261. sentChannels: [],
  262. failedChannels: notification.channels,
  263. error: '网络错误或服务暂时不可用'
  264. };
  265. }
  266. }),
  267. catchError(error => {
  268. console.error('通知发送失败:', error);
  269. notification.status = 'failed';
  270. return of({
  271. success: false,
  272. notificationId: notification.id,
  273. sentChannels: [],
  274. failedChannels: notification.channels,
  275. error: error.message || '发送失败'
  276. });
  277. })
  278. );
  279. }
  280. /**
  281. * 获取通知模板
  282. */
  283. private getTemplate(type: NotificationType): NotificationTemplate | undefined {
  284. return this.templates.find(t => t.type === type);
  285. }
  286. /**
  287. * 渲染模板内容
  288. */
  289. private renderTemplate(template: string, data: any): string {
  290. let rendered = template;
  291. Object.keys(data).forEach(key => {
  292. const placeholder = `{{${key}}}`;
  293. rendered = rendered.replace(new RegExp(placeholder, 'g'), data[key]);
  294. });
  295. return rendered;
  296. }
  297. /**
  298. * 添加到通知历史
  299. */
  300. private addToNotificationHistory(notification: Notification): void {
  301. const current = this.notifications$.value;
  302. this.notifications$.next([notification, ...current]);
  303. }
  304. /**
  305. * 获取通知历史
  306. */
  307. getNotificationHistory(): Observable<Notification[]> {
  308. return this.notifications$.asObservable();
  309. }
  310. /**
  311. * 获取通知统计
  312. */
  313. getNotificationStatistics(): Observable<{
  314. total: number;
  315. sent: number;
  316. failed: number;
  317. pending: number;
  318. byType: { [key: string]: number };
  319. byChannel: { [key: string]: number };
  320. }> {
  321. return this.notifications$.pipe(
  322. map(notifications => {
  323. const total = notifications.length;
  324. const sent = notifications.filter(n => n.status === 'sent').length;
  325. const failed = notifications.filter(n => n.status === 'failed').length;
  326. const pending = notifications.filter(n => n.status === 'pending').length;
  327. const byType: { [key: string]: number } = {};
  328. const byChannel: { [key: string]: number } = {};
  329. notifications.forEach(n => {
  330. byType[n.type] = (byType[n.type] || 0) + 1;
  331. n.channels.forEach(channel => {
  332. byChannel[channel] = (byChannel[channel] || 0) + 1;
  333. });
  334. });
  335. return {
  336. total,
  337. sent,
  338. failed,
  339. pending,
  340. byType,
  341. byChannel
  342. };
  343. })
  344. );
  345. }
  346. /**
  347. * 重试失败的通知
  348. */
  349. retryFailedNotification(notificationId: string): Observable<NotificationResult> {
  350. const notifications = this.notifications$.value;
  351. const notification = notifications.find(n => n.id === notificationId);
  352. if (!notification || notification.status !== 'failed') {
  353. return of({
  354. success: false,
  355. notificationId,
  356. sentChannels: [],
  357. failedChannels: [],
  358. error: '通知不存在或状态不正确'
  359. });
  360. }
  361. notification.status = 'pending';
  362. return this.sendNotification(notification);
  363. }
  364. /**
  365. * 获取支持的通知渠道
  366. */
  367. getSupportedChannels(): NotificationChannel[] {
  368. return Object.values(NotificationChannel);
  369. }
  370. /**
  371. * 获取通知模板列表
  372. */
  373. getTemplates(): NotificationTemplate[] {
  374. return [...this.templates];
  375. }
  376. }