message.service.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. import { Injectable } from '@angular/core';
  2. import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
  3. import { catchError, map, Observable, of } from 'rxjs';
  4. // 定义模板类型枚举
  5. export enum TemplateType {
  6. OTHER = 0,
  7. NEW_USER_WELCOM = 1, // 欢迎与引导
  8. ENCOURAGE_AND_REWARD = 2, // 鼓励与奖励
  9. DAILY_REWARD_REMINDER = 3, // 每日奖励提醒
  10. PERSONALIZED_RECOMMENDATION = 4, // 个性化推荐
  11. NEW_CONTENT_REMINDER = 5, // 新画作提醒
  12. HOT_CONTENT_RECOMMENDATION = 6, // 热门画作推荐
  13. LIMITED_TIME_EVENT_REMINDER = 7, // 限时活动提醒
  14. HOLIDAY_CELEBRATION = 8, // 节日/特殊日庆贺
  15. COMPLETION_ENCOURAGEMENT = 9, // 完成度鼓励
  16. SOCIAL_SHARING_ENCOURAGEMENT = 10, // 社交分享鼓励
  17. UNACTIVE_USER_RECALL = 11, // 长时间不活跃用户召回
  18. NEW_FEATURE_INTRODUCTION = 12, // 新功能介绍
  19. CHALLENGE_TASK_REMINDER = 13, // 挑战任务提醒
  20. }
  21. // 定义模板类型映射
  22. export const TEMPLATE_TYPE_MAP: Record<TemplateType, string> = {
  23. [TemplateType.OTHER]: '其他',
  24. [TemplateType.NEW_USER_WELCOM]: '欢迎与引导',
  25. [TemplateType.ENCOURAGE_AND_REWARD]: '鼓励与奖励',
  26. [TemplateType.DAILY_REWARD_REMINDER]: '每日奖励提醒',
  27. [TemplateType.PERSONALIZED_RECOMMENDATION]: '个性化推荐',
  28. [TemplateType.NEW_CONTENT_REMINDER]: '新画作提醒',
  29. [TemplateType.HOT_CONTENT_RECOMMENDATION]: '热门画作推荐',
  30. [TemplateType.LIMITED_TIME_EVENT_REMINDER]: '限时活动提醒',
  31. [TemplateType.HOLIDAY_CELEBRATION]: '假日/节日主题',
  32. [TemplateType.COMPLETION_ENCOURAGEMENT]: '完成度鼓励',
  33. [TemplateType.SOCIAL_SHARING_ENCOURAGEMENT]: '社交分享鼓励',
  34. [TemplateType.UNACTIVE_USER_RECALL]: '长时间不活跃用户召回',
  35. [TemplateType.NEW_FEATURE_INTRODUCTION]: '新功能介绍',
  36. [TemplateType.CHALLENGE_TASK_REMINDER]: '挑战任务提醒',
  37. };
  38. // 新增 ActionType 枚举和映射
  39. export enum ActionType {
  40. GO_APP = 'go/app',
  41. GO_ART = 'go/art',
  42. GO_ALBUM = 'go/album',
  43. GAIN_HINT = 'gain/hint',
  44. KEEP_GOING = 'keepgoing',
  45. }
  46. export const ACTION_TYPE_MAP: Record<ActionType, string> = {
  47. [ActionType.GO_APP]: '打开应用',
  48. [ActionType.GO_ART]: '打开填色作品',
  49. [ActionType.GO_ALBUM]: '打开专辑',
  50. [ActionType.GAIN_HINT]: '获取提示奖励',
  51. [ActionType.KEEP_GOING]: '继续完成作品',
  52. };
  53. export const ACTION_PARAM_PLACEHOLDERS: Record<ActionType, string> = {
  54. [ActionType.GO_APP]: '',
  55. [ActionType.GO_ART]: '输入作品ID',
  56. [ActionType.GO_ALBUM]: '输入专辑ID',
  57. [ActionType.GAIN_HINT]: '输入提示数量',
  58. [ActionType.KEEP_GOING]: '输入作品ID',
  59. };
  60. // 定义消息模板接口
  61. export interface IMessageTemplate {
  62. _id: string;
  63. templateName: string;
  64. templateType: TemplateType;
  65. description: string;
  66. messageTitle: { [key: string]: string };
  67. messageContent: { [key: string]: string };
  68. image?: string;
  69. bigger?: boolean;
  70. action?: ActionType;
  71. param?: string;
  72. extend?: string;
  73. createdAt: Date;
  74. updatedAt: Date;
  75. }
  76. // 定义创建/更新模板的数据结构
  77. export interface MessageTemplateData {
  78. templateName: string;
  79. templateType: TemplateType;
  80. descritpion: string;
  81. messageTitle: { [key: string]: string };
  82. messageContent: { [key: string]: string };
  83. image?: string;
  84. bigger?: boolean;
  85. action?: string;
  86. param?: string;
  87. extend?: string;
  88. }
  89. // 定义目标用户筛选项相关接口
  90. export interface FilterCondition {
  91. field: string;
  92. operator: string;
  93. value: any;
  94. }
  95. export interface FieldConfig {
  96. value: string;
  97. label: string;
  98. type: 'country' | 'date' | 'number' | 'number_days' | 'text';
  99. }
  100. export interface OperatorOption {
  101. value: string;
  102. label: string;
  103. valueType: 'single' | 'multiple' | 'range';
  104. }
  105. // 更新模板信息接口
  106. export interface IMessageTemplateInfo {
  107. _id: string;
  108. templateName: string;
  109. templateType?: TemplateType;
  110. description?: string;
  111. createdAt?: Date;
  112. updatedAt?: Date;
  113. }
  114. // 策略接口保持不变
  115. export interface IMessageStrategy {
  116. _id: string;
  117. name: string;
  118. description?: string;
  119. templates: IMessageTemplateInfo[]; // 使用 IMessageTemplateInfo 类型
  120. createdAt: Date;
  121. updatedAt: Date;
  122. }
  123. // 创建策略数据接口保持不变
  124. export interface MessageStrategyData {
  125. name: string;
  126. description?: string;
  127. templates: string[]; // 创建时只需要模板ID数组
  128. }
  129. // 定义消息活动接口
  130. export interface IMessageActivity {
  131. _id: string;
  132. name: string;
  133. templateId: string;
  134. image?: string;
  135. bigger: boolean;
  136. action?: ActionType;
  137. description: string;
  138. param?: string;
  139. extend?: string;
  140. strategy: number;
  141. filter?: FilterCondition[];
  142. scheduleAt?: string;
  143. everyday: boolean;
  144. status: number;
  145. createdAt: Date;
  146. updatedAt: Date;
  147. }
  148. // 定义创建/更新消息活动的数据结构
  149. export interface MessageActivityData {
  150. name: string;
  151. templateId: string;
  152. image?: string;
  153. bigger: boolean;
  154. action?: string;
  155. param?: string;
  156. extend?: string;
  157. strategy: number;
  158. filter?: FilterCondition[];
  159. scheduleAt?: string;
  160. everyday: boolean;
  161. status: number;
  162. description: string;
  163. }
  164. // 定义消息记录接口
  165. export interface IMessageRecord {
  166. _id: string;
  167. uid: string;
  168. cc?: string;
  169. activityId?: string;
  170. activityName?: string;
  171. templateId?: string;
  172. templateName?: string;
  173. strategyId?: string;
  174. strategyName?: string;
  175. title: string;
  176. content: string;
  177. image?: string;
  178. bigger: boolean;
  179. action?: string;
  180. param?: string;
  181. extend?: string;
  182. status: number;
  183. inforeground?: boolean;
  184. errno?: string;
  185. fcmReceipt?: string;
  186. plannedSendAt?: Date;
  187. actualSendAt?: Date;
  188. deliveredAt?: Date;
  189. openedAt?: Date;
  190. createdAt: Date;
  191. updatedAt: Date;
  192. }
  193. // 定义分页响应接口
  194. export interface IMessageRecordPaginationResponse {
  195. success: boolean;
  196. data: IMessageRecord[];
  197. pagination: {
  198. total: number;
  199. page: number;
  200. limit: number;
  201. totalPages: number;
  202. };
  203. }
  204. // 定义删除操作的响应结构
  205. interface DeleteResponse {
  206. deletedCount?: number;
  207. }
  208. @Injectable({
  209. providedIn: 'root',
  210. })
  211. export class MessageService {
  212. private templatesApiUrl = '/api/message-template';
  213. private activitiesApiUrl = '/api/message-activity';
  214. private activityListApiUri = '/api/message-activities';
  215. private recordsApiUrl = '/api/message-record';
  216. private recordsListApiUrl = '/api/message-records';
  217. private strategiesApiUrl = '/api/message-strategy';
  218. private httpOptions = {
  219. headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
  220. };
  221. constructor(private http: HttpClient) {}
  222. /**
  223. * 获取所有客户端行为选项
  224. */
  225. getActionOptions(): { value: ActionType; label: string }[] {
  226. return Object.entries(ACTION_TYPE_MAP).map(([value, label]) => ({
  227. value: value as ActionType,
  228. label,
  229. }));
  230. }
  231. /**
  232. * 获取行为参数占位符
  233. */
  234. getActionParamPlaceholder(action: ActionType): string {
  235. return ACTION_PARAM_PLACEHOLDERS[action] || '输入参数';
  236. }
  237. /**
  238. * 获取所有模板类型选项
  239. */
  240. getTemplateTypeOptions(): { value: TemplateType; label: string }[] {
  241. return Object.entries(TEMPLATE_TYPE_MAP).map(([value, label]) => ({
  242. value: parseInt(value, 10) as TemplateType,
  243. label,
  244. }));
  245. }
  246. /**
  247. * 获取模板类型映射
  248. */
  249. getTemplateTypeMap(): Record<TemplateType, string> {
  250. return TEMPLATE_TYPE_MAP;
  251. }
  252. /**
  253. * 模板管理 API
  254. */
  255. createTemplate(
  256. templateData: MessageTemplateData
  257. ): Observable<IMessageTemplate> {
  258. return this.http.post<IMessageTemplate>(
  259. this.templatesApiUrl,
  260. templateData,
  261. this.httpOptions
  262. );
  263. }
  264. getTemplateById(templateId: string): Observable<IMessageTemplate | null> {
  265. return this.http.get<IMessageTemplate | null>(
  266. `${this.templatesApiUrl}/${templateId}`,
  267. this.httpOptions
  268. );
  269. }
  270. getTemplateByName(templateName: string): Observable<IMessageTemplate | null> {
  271. return this.http.get<IMessageTemplate | null>(
  272. `${this.templatesApiUrl}/name/${templateName}`,
  273. this.httpOptions
  274. );
  275. }
  276. getAllTemplates(): Observable<IMessageTemplate[]> {
  277. return this.http.get<IMessageTemplate[]>(
  278. this.templatesApiUrl,
  279. this.httpOptions
  280. );
  281. }
  282. updateTemplate(
  283. templateId: string,
  284. updateData: Partial<MessageTemplateData>
  285. ): Observable<IMessageTemplate | null> {
  286. return this.http.put<IMessageTemplate | null>(
  287. `${this.templatesApiUrl}/${templateId}`,
  288. updateData,
  289. this.httpOptions
  290. );
  291. }
  292. deleteTemplate(templateId: string): Observable<DeleteResponse> {
  293. return this.http.delete<DeleteResponse>(
  294. `${this.templatesApiUrl}/${templateId}`,
  295. this.httpOptions
  296. );
  297. }
  298. getAllStrategies(): Observable<IMessageStrategy[]> {
  299. return this.http
  300. .get<IMessageStrategy[]>('/api/message-strategies', this.httpOptions)
  301. .pipe(
  302. map((response) => (Array.isArray(response) ? response : [])),
  303. catchError((err) => {
  304. console.error('获取策略失败:', err);
  305. return of([]);
  306. })
  307. );
  308. }
  309. getStrategyById(id: string): Observable<IMessageStrategy | null> {
  310. return this.http.get<IMessageStrategy | null>(
  311. `${this.strategiesApiUrl}/${id}`,
  312. this.httpOptions
  313. );
  314. }
  315. createStrategy(
  316. strategyData: MessageStrategyData
  317. ): Observable<IMessageStrategy> {
  318. return this.http.post<IMessageStrategy>(
  319. this.strategiesApiUrl,
  320. strategyData,
  321. this.httpOptions
  322. );
  323. }
  324. updateStrategy(
  325. id: string,
  326. updateData: Partial<MessageStrategyData>
  327. ): Observable<IMessageStrategy | null> {
  328. return this.http.put<IMessageStrategy | null>(
  329. `${this.strategiesApiUrl}/${id}`,
  330. updateData,
  331. this.httpOptions
  332. );
  333. }
  334. deleteStrategy(id: string): Observable<{ deletedCount?: number }> {
  335. return this.http.delete<{ deletedCount?: number }>(
  336. `${this.strategiesApiUrl}/${id}`,
  337. this.httpOptions
  338. );
  339. }
  340. /**
  341. * 活动管理 API
  342. */
  343. getAllActivities(): Observable<IMessageActivity[]> {
  344. return this.http.get<IMessageActivity[]>(
  345. this.activityListApiUri,
  346. this.httpOptions
  347. );
  348. }
  349. getActivityById(activityId: string): Observable<IMessageActivity | null> {
  350. return this.http.get<IMessageActivity | null>(
  351. `${this.activitiesApiUrl}/${activityId}`,
  352. this.httpOptions
  353. );
  354. }
  355. createActivity(
  356. activityData: MessageActivityData
  357. ): Observable<IMessageActivity> {
  358. return this.http.post<IMessageActivity>(
  359. this.activitiesApiUrl,
  360. activityData,
  361. this.httpOptions
  362. );
  363. }
  364. updateActivity(
  365. id: string,
  366. updateData: Partial<MessageActivityData>
  367. ): Observable<IMessageActivity | null> {
  368. return this.http.put<IMessageActivity | null>(
  369. `${this.activitiesApiUrl}/${id}`,
  370. updateData,
  371. this.httpOptions
  372. );
  373. }
  374. deleteActivity(id: string): Observable<DeleteResponse> {
  375. return this.http.delete<DeleteResponse>(
  376. `${this.activitiesApiUrl}/${id}`,
  377. this.httpOptions
  378. );
  379. }
  380. updateActivityStatus(
  381. id: string,
  382. status: number
  383. ): Observable<IMessageActivity | null> {
  384. return this.http.put<IMessageActivity | null>(
  385. `${this.activitiesApiUrl}/${id}/status`,
  386. { status },
  387. this.httpOptions
  388. );
  389. }
  390. /**
  391. * 语言支持相关
  392. */
  393. getSupportedLanguages(): { code: string; name: string }[] {
  394. return [
  395. { code: 'en', name: 'English' },
  396. { code: 'zh-cn', name: '简体中文' },
  397. { code: 'zh-tw', name: '繁體中文' },
  398. { code: 'es', name: 'Español' },
  399. { code: 'fr', name: 'Français' },
  400. { code: 'de', name: 'Deutsch' },
  401. { code: 'ja', name: '日本語' },
  402. { code: 'ko', name: '한국어' },
  403. { code: 'ru', name: 'Русский' },
  404. { code: 'pt', name: 'Português' },
  405. ];
  406. }
  407. getLanguageName(code: string): string {
  408. const lang = this.getSupportedLanguages().find((l) => l.code === code);
  409. return lang ? lang.name : code;
  410. }
  411. /**
  412. * 目标用户筛选相关
  413. */
  414. countTargetUsers(filters: FilterCondition[]): Observable<number> {
  415. return this.http.post<number>(
  416. `/api/users/count`,
  417. { filters },
  418. this.httpOptions
  419. );
  420. }
  421. /**
  422. * 模板内容验证
  423. */
  424. validateTemplateContent(
  425. content: string
  426. ): Observable<{ valid: boolean; message?: string }> {
  427. return this.http.post<{ valid: boolean; message?: string }>(
  428. `${this.templatesApiUrl}/validate`,
  429. { content },
  430. this.httpOptions
  431. );
  432. }
  433. /**
  434. * 模板名称可用性检查
  435. */
  436. checkTemplateNameAvailable(name: string): Observable<{ available: boolean }> {
  437. return this.http.post<{ available: boolean }>(
  438. `${this.templatesApiUrl}/check-name`,
  439. { name },
  440. this.httpOptions
  441. );
  442. }
  443. /**
  444. * 获取模板默认内容
  445. */
  446. getDefaultTemplateContent(
  447. language: string
  448. ): Observable<{ title: string; content: string }> {
  449. return this.http.get<{ title: string; content: string }>(
  450. `${this.templatesApiUrl}/default-content/${language}`,
  451. this.httpOptions
  452. );
  453. }
  454. /**
  455. * 消息记录管理 API
  456. */
  457. createRecord(
  458. recordData: Partial<IMessageRecord>
  459. ): Observable<IMessageRecord> {
  460. return this.http.post<IMessageRecord>(
  461. this.recordsApiUrl,
  462. recordData,
  463. this.httpOptions
  464. );
  465. }
  466. getPaginatedRecords(params: {
  467. page?: number;
  468. limit?: number;
  469. uid?: string;
  470. activityId?: string;
  471. status?: number;
  472. startDate?: string;
  473. endDate?: string;
  474. }): Observable<IMessageRecordPaginationResponse> {
  475. let httpParams = new HttpParams();
  476. for (const key in params) {
  477. if (params.hasOwnProperty(key) && params[key as keyof typeof params]) {
  478. httpParams = httpParams.append(
  479. key,
  480. String(params[key as keyof typeof params])
  481. );
  482. }
  483. }
  484. return this.http.get<IMessageRecordPaginationResponse>(
  485. this.recordsListApiUrl,
  486. {
  487. ...this.httpOptions,
  488. params: httpParams,
  489. }
  490. );
  491. }
  492. getRecordById(recordId: string): Observable<IMessageRecord> {
  493. return this.http.get<IMessageRecord>(
  494. `${this.recordsApiUrl}/${recordId}`,
  495. this.httpOptions
  496. );
  497. }
  498. updateRecord(
  499. recordId: string,
  500. updateData: Partial<IMessageRecord>
  501. ): Observable<IMessageRecord> {
  502. return this.http.put<IMessageRecord>(
  503. `${this.recordsApiUrl}/${recordId}`,
  504. updateData,
  505. this.httpOptions
  506. );
  507. }
  508. }