activity-log.service.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import { Injectable, inject } from '@angular/core';
  2. import { FmodeParse } from 'fmode-ng/core';
  3. /**
  4. * 活动日志服务
  5. * 用于记录和查询系统中所有成员的操作活动
  6. */
  7. @Injectable({
  8. providedIn: 'root'
  9. })
  10. export class ActivityLogService {
  11. private Parse = FmodeParse.with('nova');
  12. /**
  13. * 记录活动日志
  14. */
  15. async logActivity(data: {
  16. actorId: string;
  17. actorName: string;
  18. actorRole: string;
  19. actionType: string;
  20. module: string;
  21. entityType: string;
  22. entityId: string;
  23. entityName: string;
  24. description: string;
  25. metadata?: any;
  26. }): Promise<any> {
  27. try {
  28. const currentUser = await this.Parse.User.current();
  29. if (!currentUser) {
  30. console.warn('No current user, skipping activity log');
  31. return null;
  32. }
  33. const company = currentUser.get('company');
  34. if (!company) {
  35. console.warn('No company found, skipping activity log');
  36. return null;
  37. }
  38. const ActivityLog = this.Parse.Object.extend('ActivityLog');
  39. const log = new ActivityLog();
  40. log.set('company', company);
  41. log.set('actor', { __type: 'Pointer', className: 'Profile', objectId: data.actorId });
  42. log.set('actorName', data.actorName);
  43. log.set('actorRole', data.actorRole);
  44. log.set('actionType', data.actionType);
  45. log.set('module', data.module);
  46. log.set('entityType', data.entityType);
  47. log.set('entityId', data.entityId);
  48. log.set('entityName', data.entityName);
  49. log.set('description', data.description);
  50. log.set('metadata', data.metadata || {});
  51. log.set('userAgent', navigator.userAgent);
  52. log.set('isDeleted', false);
  53. const saved = await log.save();
  54. return saved;
  55. } catch (error) {
  56. console.error('Error logging activity:', error);
  57. return null;
  58. }
  59. }
  60. /**
  61. * 获取最近的活动日志
  62. */
  63. async getRecentActivities(limit: number = 10): Promise<any[]> {
  64. try {
  65. const currentUser = await this.Parse.User.current();
  66. if (!currentUser) {
  67. return [];
  68. }
  69. const company = currentUser.get('company');
  70. if (!company) {
  71. return [];
  72. }
  73. const query = new this.Parse.Query('ActivityLog');
  74. query.equalTo('company', company);
  75. query.notEqualTo('isDeleted', true);
  76. query.include('actor');
  77. query.descending('createdAt');
  78. query.limit(limit);
  79. const results = await query.find();
  80. return results.map(log => this.formatActivityLog(log));
  81. } catch (error) {
  82. console.error('Error fetching recent activities:', error);
  83. return [];
  84. }
  85. }
  86. /**
  87. * 获取所有活动日志(支持分页)
  88. */
  89. async getAllActivities(page: number = 1, limit: number = 50): Promise<{ activities: any[], total: number }> {
  90. try {
  91. const currentUser = await this.Parse.User.current();
  92. if (!currentUser) {
  93. return { activities: [], total: 0 };
  94. }
  95. const company = currentUser.get('company');
  96. if (!company) {
  97. return { activities: [], total: 0 };
  98. }
  99. const query = new this.Parse.Query('ActivityLog');
  100. query.equalTo('company', company);
  101. query.notEqualTo('isDeleted', true);
  102. query.include('actor');
  103. query.descending('createdAt');
  104. query.skip((page - 1) * limit);
  105. query.limit(limit);
  106. const [results, total] = await Promise.all([
  107. query.find(),
  108. query.count()
  109. ]);
  110. return {
  111. activities: results.map(log => this.formatActivityLog(log)),
  112. total
  113. };
  114. } catch (error) {
  115. console.error('Error fetching all activities:', error);
  116. return { activities: [], total: 0 };
  117. }
  118. }
  119. /**
  120. * 按模块查询活动日志
  121. */
  122. async getActivitiesByModule(module: string, limit: number = 20): Promise<any[]> {
  123. try {
  124. const currentUser = await this.Parse.User.current();
  125. if (!currentUser) {
  126. return [];
  127. }
  128. const company = currentUser.get('company');
  129. if (!company) {
  130. return [];
  131. }
  132. const query = new this.Parse.Query('ActivityLog');
  133. query.equalTo('company', company);
  134. query.equalTo('module', module);
  135. query.notEqualTo('isDeleted', true);
  136. query.include('actor');
  137. query.descending('createdAt');
  138. query.limit(limit);
  139. const results = await query.find();
  140. return results.map(log => this.formatActivityLog(log));
  141. } catch (error) {
  142. console.error('Error fetching activities by module:', error);
  143. return [];
  144. }
  145. }
  146. /**
  147. * 按用户查询活动日志
  148. */
  149. async getActivitiesByActor(actorId: string, limit: number = 20): Promise<any[]> {
  150. try {
  151. const currentUser = await this.Parse.User.current();
  152. if (!currentUser) {
  153. return [];
  154. }
  155. const company = currentUser.get('company');
  156. if (!company) {
  157. return [];
  158. }
  159. const query = new this.Parse.Query('ActivityLog');
  160. query.equalTo('company', company);
  161. query.equalTo('actor', { __type: 'Pointer', className: 'Profile', objectId: actorId });
  162. query.notEqualTo('isDeleted', true);
  163. query.descending('createdAt');
  164. query.limit(limit);
  165. const results = await query.find();
  166. return results.map(log => this.formatActivityLog(log));
  167. } catch (error) {
  168. console.error('Error fetching activities by actor:', error);
  169. return [];
  170. }
  171. }
  172. /**
  173. * 按时间范围查询活动日志
  174. */
  175. async getActivitiesByDateRange(startDate: Date, endDate: Date): Promise<any[]> {
  176. try {
  177. const currentUser = await this.Parse.User.current();
  178. if (!currentUser) {
  179. return [];
  180. }
  181. const company = currentUser.get('company');
  182. if (!company) {
  183. return [];
  184. }
  185. const query = new this.Parse.Query('ActivityLog');
  186. query.equalTo('company', company);
  187. query.greaterThanOrEqualTo('createdAt', startDate);
  188. query.lessThanOrEqualTo('createdAt', endDate);
  189. query.notEqualTo('isDeleted', true);
  190. query.include('actor');
  191. query.descending('createdAt');
  192. const results = await query.find();
  193. return results.map(log => this.formatActivityLog(log));
  194. } catch (error) {
  195. console.error('Error fetching activities by date range:', error);
  196. return [];
  197. }
  198. }
  199. /**
  200. * 获取活动统计
  201. */
  202. async getActivityStats(startDate?: Date, endDate?: Date): Promise<{
  203. total: number;
  204. byModule: Record<string, number>;
  205. byAction: Record<string, number>;
  206. byActor: Record<string, number>;
  207. }> {
  208. try {
  209. const currentUser = await this.Parse.User.current();
  210. if (!currentUser) {
  211. return { total: 0, byModule: {}, byAction: {}, byActor: {} };
  212. }
  213. const company = currentUser.get('company');
  214. if (!company) {
  215. return { total: 0, byModule: {}, byAction: {}, byActor: {} };
  216. }
  217. const query = new this.Parse.Query('ActivityLog');
  218. query.equalTo('company', company);
  219. query.notEqualTo('isDeleted', true);
  220. if (startDate) {
  221. query.greaterThanOrEqualTo('createdAt', startDate);
  222. }
  223. if (endDate) {
  224. query.lessThanOrEqualTo('createdAt', endDate);
  225. }
  226. const results = await query.find();
  227. const stats = {
  228. total: results.length,
  229. byModule: {} as Record<string, number>,
  230. byAction: {} as Record<string, number>,
  231. byActor: {} as Record<string, number>
  232. };
  233. results.forEach(log => {
  234. const module = log.get('module');
  235. const action = log.get('actionType');
  236. const actor = log.get('actorName');
  237. stats.byModule[module] = (stats.byModule[module] || 0) + 1;
  238. stats.byAction[action] = (stats.byAction[action] || 0) + 1;
  239. stats.byActor[actor] = (stats.byActor[actor] || 0) + 1;
  240. });
  241. return stats;
  242. } catch (error) {
  243. console.error('Error fetching activity stats:', error);
  244. return { total: 0, byModule: {}, byAction: {}, byActor: {} };
  245. }
  246. }
  247. /**
  248. * 格式化活动日志对象
  249. */
  250. private formatActivityLog(log: any): any {
  251. const actor = log.get('actor');
  252. return {
  253. id: log.id,
  254. actorName: log.get('actorName'),
  255. actorRole: log.get('actorRole'),
  256. actorAvatar: actor?.get('data')?.avatar || '/assets/avatars/default.png',
  257. actionType: log.get('actionType'),
  258. module: log.get('module'),
  259. entityType: log.get('entityType'),
  260. entityId: log.get('entityId'),
  261. entityName: log.get('entityName'),
  262. description: log.get('description'),
  263. metadata: log.get('metadata') || {},
  264. createdAt: log.get('createdAt'),
  265. formattedTime: this.formatTime(log.get('createdAt')),
  266. icon: this.getActivityIcon(log.get('module'), log.get('actionType')),
  267. color: this.getActivityColor(log.get('module'), log.get('actionType'))
  268. };
  269. }
  270. /**
  271. * 获取活动图标
  272. */
  273. private getActivityIcon(module: string, actionType: string): string {
  274. const iconMap: Record<string, string> = {
  275. 'project-create': '📋',
  276. 'project-update': '📝',
  277. 'project-complete': '✅',
  278. 'project-assign': '👤',
  279. 'customer-create': '🆕',
  280. 'customer-update': '✏️',
  281. 'task-create': '📌',
  282. 'task-complete': '✔️',
  283. 'design-create': '🎨',
  284. 'design-upload': '📤',
  285. 'design-complete': '🎨',
  286. 'finance-create': '💰',
  287. 'finance-approve': '✓',
  288. 'urgent_task-create': '🚨',
  289. 'urgent_task-complete': '✅',
  290. 'case_library-create': '📚',
  291. 'case_library-share': '🔗',
  292. 'groupchat-create': '💬',
  293. 'employee-create': '👥',
  294. 'system-login': '🔐'
  295. };
  296. const key = `${module}-${actionType}`;
  297. return iconMap[key] || '📝';
  298. }
  299. /**
  300. * 获取活动颜色
  301. */
  302. private getActivityColor(module: string, actionType: string): string {
  303. const colorMap: Record<string, string> = {
  304. 'create': '#4CAF50',
  305. 'update': '#2196F3',
  306. 'delete': '#F44336',
  307. 'complete': '#8BC34A',
  308. 'assign': '#9C27B0',
  309. 'upload': '#FF9800',
  310. 'approve': '#00BCD4'
  311. };
  312. return colorMap[actionType] || '#607D8B';
  313. }
  314. /**
  315. * 格式化时间
  316. */
  317. private formatTime(date: Date): string {
  318. const now = new Date();
  319. const diff = now.getTime() - date.getTime();
  320. const seconds = Math.floor(diff / 1000);
  321. const minutes = Math.floor(seconds / 60);
  322. const hours = Math.floor(minutes / 60);
  323. const days = Math.floor(hours / 24);
  324. if (seconds < 60) {
  325. return '刚刚';
  326. } else if (minutes < 60) {
  327. return `${minutes}分钟前`;
  328. } else if (hours < 24) {
  329. return `${hours}小时前`;
  330. } else if (days < 7) {
  331. return `${days}天前`;
  332. } else {
  333. const year = date.getFullYear();
  334. const month = String(date.getMonth() + 1).padStart(2, '0');
  335. const day = String(date.getDate()).padStart(2, '0');
  336. const hour = String(date.getHours()).padStart(2, '0');
  337. const minute = String(date.getMinutes()).padStart(2, '0');
  338. return `${year}-${month}-${day} ${hour}:${minute}`;
  339. }
  340. }
  341. /**
  342. * 获取活动描述的友好文本
  343. */
  344. getActivityDescription(activity: any): string {
  345. const actionMap: Record<string, string> = {
  346. create: '创建了',
  347. update: '更新了',
  348. delete: '删除了',
  349. complete: '完成了',
  350. assign: '分配了',
  351. upload: '上传了',
  352. share: '分享了',
  353. approve: '审批了',
  354. reject: '拒绝了',
  355. cancel: '取消了',
  356. comment: '评论了'
  357. };
  358. const moduleMap: Record<string, string> = {
  359. project: '项目',
  360. customer: '客户',
  361. task: '任务',
  362. design: '设计',
  363. finance: '报价单',
  364. urgent_task: '紧急事项',
  365. case_library: '案例',
  366. groupchat: '群聊',
  367. employee: '员工',
  368. department: '部门'
  369. };
  370. return activity.description || `${actionMap[activity.actionType] || activity.actionType}${moduleMap[activity.module] || activity.module}`;
  371. }
  372. }