@hecom/codearts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BusinessService = void 0;
4
+ const api_service_1 = require("./api.service");
5
+ /**
6
+ * 业务服务类
7
+ * 提供面向业务场景的高级操作,封装ApiService的底层调用
8
+ */
9
+ class BusinessService {
10
+ constructor(config) {
11
+ this.apiService = new api_service_1.ApiService(config);
12
+ }
13
+ /**
14
+ * 获取底层ApiService实例
15
+ * 用于需要直接访问API服务的场景
16
+ */
17
+ getApiService() {
18
+ return this.apiService;
19
+ }
20
+ // 业务操作方法将在后续添加
21
+ /**
22
+ * 通过角色ID获取项目成员
23
+ * @param projectId 项目ID
24
+ * @param roleId 角色ID
25
+ * @returns 指定角色的成员列表
26
+ */
27
+ async getMembersByRoleId(projectId, roleId) {
28
+ const membersResponse = await this.apiService.getMembers(projectId);
29
+ if (!membersResponse.success) {
30
+ throw new Error(`获取成员列表失败: ${membersResponse.error || '未知错误'}`);
31
+ }
32
+ const allMembers = membersResponse.data?.members || [];
33
+ return allMembers.filter((member) => member.role_id === roleId);
34
+ }
35
+ /**
36
+ * 获取指定日期之后的迭代列表
37
+ * @param projectId 项目ID
38
+ * @param targetDate 目标日期,格式:YYYY-MM-DD
39
+ * @returns 正在进行中的和未来的迭代列表
40
+ */
41
+ async getActiveIterationsOnDate(projectId, targetDate) {
42
+ const iterationsResponse = await this.apiService.getIterations(projectId, {
43
+ include_deleted: false,
44
+ });
45
+ if (!iterationsResponse.success) {
46
+ throw new Error(`获取迭代列表失败: ${iterationsResponse.error || '未知错误'}`);
47
+ }
48
+ const iterations = iterationsResponse.data?.iterations || [];
49
+ const targetDateTime = new Date(targetDate).getTime();
50
+ // 过滤出在目标日期正在进行中的迭代
51
+ return iterations.filter((iteration) => {
52
+ // 检查迭代状态是否为进行中 (1)
53
+ if (iteration.status !== '1') {
54
+ return false;
55
+ }
56
+ // 检查目标日期是否在迭代时间范围内
57
+ // const beginTime = new Date(iteration.begin_time).getTime();
58
+ const endTime = new Date(iteration.end_time).getTime();
59
+ return targetDateTime <= endTime;
60
+ });
61
+ }
62
+ /**
63
+ * 根据多个迭代ID和用户ID列表查询工作量列表(仅Task)
64
+ * @param projectId 项目ID
65
+ * @param iterationIds 迭代ID列表
66
+ * @param userIds 用户ID列表
67
+ * @returns Task类型的工作项列表
68
+ */
69
+ async getWorkloadByIterationsAndUsers(projectId, iterationIds, userIds) {
70
+ if (iterationIds.length === 0) {
71
+ return [];
72
+ }
73
+ const issuesResponse = await this.apiService.getIssues(projectId, {
74
+ iteration_ids: iterationIds,
75
+ tracker_ids: [2], // 2=Task(任务), 7=Story
76
+ assigned_ids: userIds,
77
+ limit: 100,
78
+ offset: 0,
79
+ });
80
+ if (!issuesResponse.success) {
81
+ throw new Error(`获取工作项失败: ${issuesResponse.error || '未知错误'}`);
82
+ }
83
+ return issuesResponse.data?.issues || [];
84
+ }
85
+ /**
86
+ * 根据迭代ID和用户ID列表查询工作量列表(仅Task和Story)
87
+ * @param projectId 项目ID
88
+ * @param iterationId 迭代ID
89
+ * @param userIds 用户ID列表
90
+ * @returns Task和Story类型的工作项列表
91
+ */
92
+ async getWorkloadByIterationAndUsers(projectId, iterationId, userIds) {
93
+ const issuesResponse = await this.apiService.getIssues(projectId, {
94
+ iteration_ids: [iterationId],
95
+ tracker_ids: [2, 7], // 2=Task(任务), 7=Story
96
+ assigned_ids: userIds,
97
+ include_deleted: false,
98
+ limit: 100,
99
+ offset: 0,
100
+ });
101
+ if (!issuesResponse.success) {
102
+ throw new Error(`获取工作项列表失败: ${issuesResponse.error || '未知错误'}`);
103
+ }
104
+ return issuesResponse.data?.issues || [];
105
+ }
106
+ async addIssueNote(projectId, issueId, content) {
107
+ const result = await this.apiService.addIssueNotes({
108
+ projectUUId: projectId,
109
+ id: String(issueId),
110
+ notes: content,
111
+ });
112
+ if (result.data?.status === 'success') {
113
+ return result.data.result.issue;
114
+ }
115
+ throw new Error(`添加工作项备注失败: ${result.data?.status || '未知错误'}`);
116
+ }
117
+ /**
118
+ * 统计工作项进度信息
119
+ * @param issues 工作项列表
120
+ * @returns 工作项进度统计结果,包括总体统计和按用户分组统计
121
+ */
122
+ calculateWorkProgress(issues) {
123
+ // 总体统计
124
+ let totalExpectedHours = 0;
125
+ let totalActualHours = 0;
126
+ // 按用户统计
127
+ const userStatsMap = issues.reduce((stats, issue) => {
128
+ const userId = issue.assigned_user?.user_id || issue.assigned_user?.nick_name || 'unassigned';
129
+ const userName = issue.assigned_user?.nick_name || '未分配';
130
+ if (!stats[userId]) {
131
+ stats[userId] = {
132
+ userName,
133
+ count: 0,
134
+ expectedHours: 0,
135
+ actualHours: 0,
136
+ completionRate: 0,
137
+ };
138
+ }
139
+ stats[userId].count++;
140
+ // 如果状态是已关闭(id=5),expectedHours 取实际工时,否则取预估工时
141
+ const expectedHours = issue.status?.id === 5 ? issue.actual_work_hours || 0 : issue.expected_work_hours || 0;
142
+ stats[userId].expectedHours += expectedHours;
143
+ stats[userId].actualHours += issue.actual_work_hours || 0;
144
+ // 累计总工时
145
+ totalExpectedHours += expectedHours;
146
+ totalActualHours += issue.actual_work_hours || 0;
147
+ return stats;
148
+ }, {});
149
+ // 计算各用户的完成率
150
+ const userStats = Object.values(userStatsMap).map((stat) => ({
151
+ ...stat,
152
+ completionRate: stat.expectedHours > 0
153
+ ? Number(((stat.actualHours / stat.expectedHours) * 100).toFixed(2))
154
+ : 0,
155
+ }));
156
+ // 计算总体完成率
157
+ const overallCompletionRate = totalExpectedHours > 0
158
+ ? Number(((totalActualHours / totalExpectedHours) * 100).toFixed(2))
159
+ : 0;
160
+ return {
161
+ totalCount: issues.length,
162
+ totalExpectedHours,
163
+ totalActualHours,
164
+ overallCompletionRate,
165
+ userStats,
166
+ };
167
+ }
168
+ /**
169
+ * 查询指定用户在指定日期的工时统计
170
+ * @param projectId 项目ID
171
+ * @param userIds 用户ID列表
172
+ * @param date 查询日期,格式:YYYY-MM-DD
173
+ * @returns 工时统计结果,包括总工时和按用户分组的工时详情
174
+ */
175
+ async getDailyWorkHourStats(projectId, userIds, date) {
176
+ const workHoursResponse = await this.apiService.showProjectWorkHours(projectId, {
177
+ user_ids: userIds,
178
+ begin_time: date,
179
+ end_time: date,
180
+ limit: 100, // 设置较大的限制以获取当天所有工时记录
181
+ offset: 0,
182
+ });
183
+ if (!workHoursResponse.success) {
184
+ throw new Error(`获取工时数据失败: ${workHoursResponse.error || '未知错误'}`);
185
+ }
186
+ const workHours = workHoursResponse.data?.work_hours || [];
187
+ // 按用户分组统计工时
188
+ const userStatsMap = workHours.reduce((stats, workHour) => {
189
+ const userId = workHour.user_id;
190
+ const userName = workHour.nick_name || workHour.user_name;
191
+ if (!stats[userId]) {
192
+ stats[userId] = {
193
+ userName,
194
+ userId,
195
+ totalHours: 0,
196
+ workHours: [],
197
+ };
198
+ }
199
+ stats[userId].workHours.push(workHour);
200
+ stats[userId].totalHours += parseFloat(workHour.work_hours_num) || 0;
201
+ return stats;
202
+ }, {});
203
+ const userStats = Object.values(userStatsMap);
204
+ // 计算总工时
205
+ const totalHours = userStats.reduce((total, user) => total + user.totalHours, 0);
206
+ return {
207
+ date,
208
+ totalHours: Math.round(totalHours * 100) / 100, // 保留两位小数
209
+ totalEntries: workHours.length,
210
+ userStats,
211
+ };
212
+ }
213
+ /**
214
+ * 查询指定用户在指定时间段内的所有工时统计(按人和领域分组)
215
+ * @param projectId 项目ID
216
+ * @param userIds 用户ID列表
217
+ * @param beginDate 开始日期,格式:YYYY-MM-DD
218
+ * @param endDate 结束日期,格式:YYYY-MM-DD
219
+ * @returns 工时统计结果,按用户和领域两个维度分组
220
+ */
221
+ async getAllWorkHourStats(projectId, userIds, beginDate, endDate) {
222
+ // Step 1: 分页获取所有工时记录
223
+ const allWorkHours = [];
224
+ const pageSize = 100;
225
+ let offset = 0;
226
+ let hasMore = true;
227
+ while (hasMore) {
228
+ const workHoursResponse = await this.apiService.showProjectWorkHours(projectId, {
229
+ user_ids: userIds,
230
+ begin_time: beginDate,
231
+ end_time: endDate,
232
+ limit: pageSize,
233
+ offset: offset,
234
+ });
235
+ if (!workHoursResponse.success) {
236
+ throw new Error(`获取工时数据失败: ${workHoursResponse.error || '未知错误'}`);
237
+ }
238
+ const workHours = workHoursResponse.data?.work_hours || [];
239
+ allWorkHours.push(...workHours);
240
+ // 判断是否还有更多数据
241
+ const total = workHoursResponse.data?.total || 0;
242
+ offset += pageSize;
243
+ hasMore = offset < total;
244
+ }
245
+ // Step 2: 提取不重复的 issue IDs
246
+ const uniqueIssueIds = [...new Set(allWorkHours.map((wh) => wh.issue_id))];
247
+ // Step 3: 获取所有 issue 详情(分批处理,每批最多 50 条)
248
+ const issueDetailsMap = new Map();
249
+ if (uniqueIssueIds.length > 0) {
250
+ const batchSize = 50;
251
+ for (let i = 0; i < uniqueIssueIds.length; i += batchSize) {
252
+ const batchIds = uniqueIssueIds.slice(i, i + batchSize);
253
+ const issuesResponse = await this.apiService.getIssues(projectId, {
254
+ issue_ids: batchIds,
255
+ limit: 100,
256
+ offset: 0,
257
+ });
258
+ if (issuesResponse.success && issuesResponse.data?.issues) {
259
+ issuesResponse.data.issues.forEach((issue) => {
260
+ issueDetailsMap.set(issue.id, issue);
261
+ });
262
+ }
263
+ }
264
+ }
265
+ // Step 4: 为每个工时记录关联领域信息
266
+ const enrichedWorkHours = allWorkHours.map((workHour) => {
267
+ const issue = issueDetailsMap.get(workHour.issue_id);
268
+ const isBug = issue?.tracker?.id === 3; // 3=Bug
269
+ const type = isBug ? issue?.tracker?.name || '' : issue?.domain?.name || '未分配领域';
270
+ return {
271
+ ...workHour,
272
+ type,
273
+ };
274
+ });
275
+ // Step 5: 按用户分组
276
+ const userStatsMap = enrichedWorkHours.reduce((stats, workHour) => {
277
+ const userId = workHour.user_id;
278
+ const userName = workHour.nick_name || workHour.user_name;
279
+ if (!stats[userId]) {
280
+ stats[userId] = {
281
+ userName,
282
+ userId,
283
+ totalHours: 0,
284
+ domainStats: [],
285
+ domainStatsMap: new Map(),
286
+ };
287
+ }
288
+ const userStat = stats[userId];
289
+ const hours = parseFloat(workHour.work_hours_num) || 0;
290
+ userStat.totalHours += hours;
291
+ // 按领域分组
292
+ const type = workHour.type;
293
+ if (!userStat.domainStatsMap.has(type)) {
294
+ const domainStat = {
295
+ type,
296
+ totalHours: 0,
297
+ workHours: [],
298
+ };
299
+ userStat.domainStatsMap.set(type, domainStat);
300
+ userStat.domainStats.push(domainStat);
301
+ }
302
+ const domainStat = userStat.domainStatsMap.get(type);
303
+ domainStat.totalHours += hours;
304
+ domainStat.workHours.push(workHour);
305
+ return stats;
306
+ }, {});
307
+ // Step 6: 转换为最终结果格式
308
+ const userStats = Object.values(userStatsMap).map((stat) => {
309
+ // 移除临时的 domainStatsMap
310
+ const { domainStatsMap, ...userStat } = stat;
311
+ // 对每个领域的总工时保留两位小数
312
+ userStat.domainStats.forEach((domainStat) => {
313
+ domainStat.totalHours = Math.round(domainStat.totalHours * 100) / 100;
314
+ });
315
+ // 对用户总工时保留两位小数
316
+ userStat.totalHours = Math.round(userStat.totalHours * 100) / 100;
317
+ return userStat;
318
+ });
319
+ // 计算总工时
320
+ const totalHours = userStats.reduce((total, user) => total + user.totalHours, 0);
321
+ return {
322
+ beginDate,
323
+ endDate,
324
+ totalHours: Math.round(totalHours * 100) / 100,
325
+ totalEntries: allWorkHours.length,
326
+ userStats,
327
+ };
328
+ }
329
+ }
330
+ exports.BusinessService = BusinessService;