@dreamor/atlas-cli 0.7.20 → 0.7.22
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.
- package/dist/adapters/atlas/cli.js +5 -4
- package/dist/adapters/atlas/commands/actual/_logic.js +62 -27
- package/dist/adapters/atlas/commands/actual/index.js +22 -20
- package/dist/adapters/atlas/commands/baseline/index.js +30 -33
- package/dist/adapters/atlas/commands/compare/index.js +27 -23
- package/dist/adapters/atlas/util/version.js +1 -1
- package/package.json +1 -1
|
@@ -291,6 +291,7 @@ function registerActualCommands(program) {
|
|
|
291
291
|
.command('show <staffId>')
|
|
292
292
|
.description('查看单个人员的实际工时明细'))
|
|
293
293
|
.option('--month <yyyymm>', '查询月份(YYYY-MM,默认当前月)')
|
|
294
|
+
.option('--status <status>', '筛选审批状态: approved | pending | all', 'approved')
|
|
294
295
|
.option('--json', '输出 JSON 信封')
|
|
295
296
|
.action(async (staffId, opts) => {
|
|
296
297
|
try {
|
|
@@ -307,7 +308,7 @@ function registerActualCommands(program) {
|
|
|
307
308
|
.option('--month <yyyymm>', '查询月份(YYYY-MM,与 --from/--to 互斥)')
|
|
308
309
|
.option('--from <yyyymm>', '起始月份(YYYY-MM,包含,与 --month 互斥)')
|
|
309
310
|
.option('--to <yyyymm>', '结束月份(YYYY-MM,包含,与 --month 互斥)')
|
|
310
|
-
.option('--status <status>', '筛选审批状态:
|
|
311
|
+
.option('--status <status>', '筛选审批状态: approved | pending | all', 'approved')
|
|
311
312
|
.option('--department <name>', '按团队负责人/部门筛选(子串,不区分大小写)')
|
|
312
313
|
.option('--role <name>', '按角色/备注筛选(子串,不区分大小写)')
|
|
313
314
|
.option('--staff-name <name>', '按姓名/工号筛选(子串,不区分大小写)')
|
|
@@ -326,7 +327,7 @@ function registerActualCommands(program) {
|
|
|
326
327
|
.description('按月/部门/角色汇总实际工时'))
|
|
327
328
|
.option('--by <axis>', 'month | department | role', 'month')
|
|
328
329
|
.option('--month <yyyymm>', '查询月份')
|
|
329
|
-
.option('--status <status>', '
|
|
330
|
+
.option('--status <status>', 'approved | pending | all', 'approved')
|
|
330
331
|
.option('--department <name>', '按部门筛选')
|
|
331
332
|
.option('--role <name>', '按角色筛选')
|
|
332
333
|
.option('--from <yyyymm>', '起始月份')
|
|
@@ -347,7 +348,7 @@ function registerActualCommands(program) {
|
|
|
347
348
|
.requiredOption('--format <fmt>', 'csv | json')
|
|
348
349
|
.requiredOption('--out <path>', '输出文件路径')
|
|
349
350
|
.option('--by <axis>', 'month | department | role', 'month')
|
|
350
|
-
.option('--status <status>', '
|
|
351
|
+
.option('--status <status>', 'approved | pending | all', 'approved')
|
|
351
352
|
.option('--department <name>', '按部门筛选')
|
|
352
353
|
.option('--role <name>', '按角色筛选')
|
|
353
354
|
.option('--from <yyyymm>', '起始月份')
|
|
@@ -372,7 +373,7 @@ function registerCompareCommands(program) {
|
|
|
372
373
|
.option('--month <yyyymm>', '查询月份(YYYY-MM,优先级高于 from/to 用于实际数据 API)')
|
|
373
374
|
.option('--department <name>', '按部门名称/ID 筛选(子串,不区分大小写)')
|
|
374
375
|
.option('--role <name>', '按角色/备注筛选(子串,不区分大小写)')
|
|
375
|
-
.option('--status <status>', '筛选审批状态:
|
|
376
|
+
.option('--status <status>', '筛选审批状态: approved | pending | all', 'approved')
|
|
376
377
|
.option('--threshold <n>', '差异绝对值阈值(人月),低于此值不标记', '0')
|
|
377
378
|
.option('--flag-overrun', '用 ⚠️ 标记实际 > 基线的情况')
|
|
378
379
|
.option('--page <n>', '页码(从 1 开始)')
|
|
@@ -4,8 +4,14 @@
|
|
|
4
4
|
* 与命令解析层(index.ts)分离:这里不做 IO、不调 API、不输出,
|
|
5
5
|
* 只做数据变换。便于单元测试(参见 tests/actual_logic.test.ts)。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* 单位约定(关键):
|
|
8
|
+
* - API detail.manpower 字段单位为**人天**,不是人月。
|
|
9
|
+
* - 网页"人力汇总"页显示的人月 = Σ(叶子节点 detail.manpower) ÷ WORK_DAYS_PER_MONTH。
|
|
10
|
+
* - 经 2026-07-01 实测验证:BMW(2548) 2026-03 叶子节点合计 321.9 人天 ÷ 22 = 14.63 人月,
|
|
11
|
+
* 与网页"各项目显示值:已确认"完全一致。
|
|
8
12
|
*/
|
|
13
|
+
/** 每月工作日,用于把 API 返回的人天换算为人月(与网页口径一致) */
|
|
14
|
+
export const WORK_DAYS_PER_MONTH = 22;
|
|
9
15
|
/**
|
|
10
16
|
* expandMonths 已移至 util/months.ts,此处不再导出。
|
|
11
17
|
*/
|
|
@@ -13,6 +19,21 @@
|
|
|
13
19
|
export function annotateWithMonth(entries, month) {
|
|
14
20
|
return entries.map((p) => ({ ...p, month }));
|
|
15
21
|
}
|
|
22
|
+
/** 将 CLI 字符串选项规范化为 StatusFilter,默认 approved(与网站默认视图对齐) */
|
|
23
|
+
export function normalizeStatusFilter(raw) {
|
|
24
|
+
if (raw === 'all' || raw === 'pending' || raw === 'approved')
|
|
25
|
+
return raw;
|
|
26
|
+
return 'approved';
|
|
27
|
+
}
|
|
28
|
+
/** 判断单条 detail 记录是否通过状态筛选 */
|
|
29
|
+
function passesStatusFilter(statusRaw, filter) {
|
|
30
|
+
const status = Number(statusRaw);
|
|
31
|
+
if (filter === 'approved')
|
|
32
|
+
return status === 2;
|
|
33
|
+
if (filter === 'pending')
|
|
34
|
+
return status !== 2;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
16
37
|
/**
|
|
17
38
|
* 按轴聚合人月。
|
|
18
39
|
* - month:按月份分组(需先用 annotateWithMonth 打标)
|
|
@@ -40,42 +61,56 @@ export function filterByStaff(entries, staffId) {
|
|
|
40
61
|
return entries.filter((p) => p.staffId === staffId || p.staffName?.includes(staffId));
|
|
41
62
|
}
|
|
42
63
|
/**
|
|
43
|
-
* 走查
|
|
64
|
+
* 走查 summaryByProject 返回的树结构,提取指定项目的实际工时条目(人月)。
|
|
65
|
+
*
|
|
66
|
+
* 关键口径(与网页"人力汇总"页一致,2026-07-01 实测验证):
|
|
67
|
+
* 1. **只在叶子节点累加**:manager 节点的 detail 是下属汇总的副本,
|
|
68
|
+
* 若一并计入会把同一笔工时重复累加(实测会把 321.9 人天虚增到 440.6)。
|
|
69
|
+
* 叶子 = children 为 null 或空数组的节点。
|
|
70
|
+
* 2. **人天 ÷ 工作日 → 人月**:API detail.manpower 单位是人天,
|
|
71
|
+
* 需除以 WORK_DAYS_PER_MONTH(22) 才是网页显示的人月。
|
|
72
|
+
* 3. **状态筛选**:默认 approved 只统计 status===2(已确认),与网页默认视图对齐。
|
|
44
73
|
*
|
|
45
|
-
*
|
|
46
|
-
* 采取走最深层的策略:越深的节点 detail 越精确。
|
|
74
|
+
* 返回按员工去重的条目,manpower 为人月(四舍五入 2 位)。
|
|
47
75
|
*/
|
|
48
|
-
export function flattenTree(nodes, targetPid) {
|
|
76
|
+
export function flattenTree(nodes, targetPid, statusFilter = 'approved') {
|
|
49
77
|
const byStaffId = new Map();
|
|
50
|
-
function
|
|
78
|
+
function isLeaf(node) {
|
|
79
|
+
const kids = node.children;
|
|
80
|
+
return !Array.isArray(kids) || kids.length === 0;
|
|
81
|
+
}
|
|
82
|
+
function walk(items) {
|
|
51
83
|
for (const raw of items ?? []) {
|
|
52
84
|
const node = raw;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
// 只在叶子节点累加 detail,避免 manager 层重复计入
|
|
86
|
+
if (isLeaf(node)) {
|
|
87
|
+
const details = (node.detail ?? []);
|
|
88
|
+
let personDays = 0;
|
|
89
|
+
for (const d of details) {
|
|
90
|
+
if (String(d.projectId) === targetPid && passesStatusFilter(d.status, statusFilter)) {
|
|
91
|
+
personDays += Number(d.manpower ?? 0);
|
|
92
|
+
}
|
|
58
93
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
if (personDays > 0) {
|
|
95
|
+
const sid = String(node.staffId ?? '');
|
|
96
|
+
const manMonths = personDays / WORK_DAYS_PER_MONTH;
|
|
97
|
+
// 同一叶子节点不可能重复出现;若员工跨多节点,按 manpower 累加
|
|
98
|
+
const existing = byStaffId.get(sid);
|
|
99
|
+
const total = (existing?.manpower ?? 0) + manMonths;
|
|
64
100
|
byStaffId.set(sid, {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
departmentName: String(node.department ?? ''),
|
|
71
|
-
role: String(node.role ?? ''),
|
|
72
|
-
},
|
|
101
|
+
staffId: sid,
|
|
102
|
+
staffName: String(node.realname ?? ''),
|
|
103
|
+
manpower: Math.round(total * 100) / 100,
|
|
104
|
+
departmentName: String(node.department ?? ''),
|
|
105
|
+
role: String(node.role ?? ''),
|
|
73
106
|
});
|
|
74
107
|
}
|
|
75
108
|
}
|
|
76
|
-
|
|
109
|
+
else {
|
|
110
|
+
walk(node.children);
|
|
111
|
+
}
|
|
77
112
|
}
|
|
78
113
|
}
|
|
79
|
-
walk(nodes
|
|
80
|
-
return [...byStaffId.values()]
|
|
114
|
+
walk(nodes);
|
|
115
|
+
return [...byStaffId.values()];
|
|
81
116
|
}
|
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
import { createClient } from '../../http/client.js';
|
|
2
2
|
import { isJsonMode, jsonOk, log } from '../../util/output.js';
|
|
3
3
|
import { enforceOutputLimit } from '../../util/output-limit.js';
|
|
4
|
-
import { SessionExpiredError } from '../../util/errors.js';
|
|
5
4
|
import { expandMonths, expandMonthsDefault } from '../../util/months.js';
|
|
6
5
|
import { resolveSecureExportPath, secureWriteFile } from '../../util/secure-fs.js';
|
|
7
|
-
import { annotateWithMonth, aggregateByAxis, filterByStaff, flattenTree, } from './_logic.js';
|
|
6
|
+
import { annotateWithMonth, aggregateByAxis, filterByStaff, flattenTree, normalizeStatusFilter, } from './_logic.js';
|
|
8
7
|
import { resolveProjectId } from '../../util/env.js';
|
|
9
8
|
import { readBanmaIdentity } from '../../auth/session.js';
|
|
10
9
|
function getProjectId(opts) {
|
|
11
10
|
return resolveProjectId(opts.projectId);
|
|
12
11
|
}
|
|
13
|
-
async function fetchActual(pid, month) {
|
|
12
|
+
async function fetchActual(pid, month, statusFilter) {
|
|
14
13
|
const client = createClient();
|
|
15
14
|
const identity = await readBanmaIdentity();
|
|
16
15
|
const loginStaffId = identity?.staffId ?? '';
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
const raw = await client.post('/yuntu-service/manpower/weekly/
|
|
16
|
+
// 项目视图:使用 summaryByProject 获取全项目维度的实际工时。
|
|
17
|
+
// API 返回全部状态的数据,statusFilter 在客户端按 status 字段过滤(与网站一致)。
|
|
18
|
+
const raw = await client.post('/yuntu-service/manpower/weekly/summaryByProject.json', { month, staffIds: [], projectIds: [pid], isConfirm: false, loginStaffId });
|
|
20
19
|
const tree = (Array.isArray(raw) ? raw : []);
|
|
21
20
|
// 根节点为当前用户,children 为团队成员
|
|
22
21
|
if (tree.length > 0 && tree[0].children) {
|
|
23
|
-
return flattenTree(tree[0].children, pid);
|
|
22
|
+
return flattenTree(tree[0].children, pid, statusFilter);
|
|
24
23
|
}
|
|
25
24
|
return [];
|
|
26
25
|
}
|
|
27
26
|
export async function showCmd(staffId, opts) {
|
|
28
27
|
const pid = getProjectId(opts);
|
|
29
28
|
const m = opts.month ?? expandMonthsDefault()[0];
|
|
30
|
-
const data = await fetchActual(pid, m);
|
|
29
|
+
const data = await fetchActual(pid, m, normalizeStatusFilter(opts.status));
|
|
31
30
|
const filtered = filterByStaff(data, staffId);
|
|
32
31
|
if (opts.json || isJsonMode()) {
|
|
33
32
|
jsonOk({ staffId, month: m, personnel: filtered });
|
|
@@ -45,18 +44,19 @@ export async function monthCmd(opts) {
|
|
|
45
44
|
const pid = getProjectId(opts);
|
|
46
45
|
// 不传月份时默认最近 12 个月,与 baseline month 行为一致
|
|
47
46
|
const months = opts.month ? [opts.month] : (opts.from || opts.to ? expandMonths(opts.from, opts.to) : expandMonthsDefault());
|
|
47
|
+
const statusFilter = normalizeStatusFilter(opts.status);
|
|
48
48
|
const all = [];
|
|
49
49
|
const failedMonths = [];
|
|
50
50
|
for (const m of months) {
|
|
51
51
|
try {
|
|
52
|
-
const data = await fetchActual(pid, m);
|
|
52
|
+
const data = await fetchActual(pid, m, statusFilter);
|
|
53
53
|
all.push(...annotateWithMonth(data, m));
|
|
54
54
|
}
|
|
55
55
|
catch (e) {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// 实际工时 API 对无数据月份也返回 501("当前token已失效"),
|
|
57
|
+
// 不是真正的 token 过期。不 rethrow,只记录到 failedMonths。
|
|
58
58
|
failedMonths.push({ month: m, error: e instanceof Error ? e.message : String(e) });
|
|
59
|
-
log(`月 ${m}
|
|
59
|
+
log(`月 ${m} 无实际工时数据`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
// Apply --department / --role filter
|
|
@@ -77,17 +77,18 @@ export async function monthCmd(opts) {
|
|
|
77
77
|
export async function summaryCmd(opts) {
|
|
78
78
|
const pid = getProjectId(opts);
|
|
79
79
|
const months = opts.month ? [opts.month] : (opts.from || opts.to ? expandMonths(opts.from, opts.to) : expandMonthsDefault());
|
|
80
|
+
const statusFilter = normalizeStatusFilter(opts.status);
|
|
80
81
|
const all = [];
|
|
81
82
|
const failedMonths = [];
|
|
82
83
|
for (const m of months) {
|
|
83
84
|
try {
|
|
84
|
-
all.push(...annotateWithMonth(await fetchActual(pid, m), m));
|
|
85
|
+
all.push(...annotateWithMonth(await fetchActual(pid, m, statusFilter), m));
|
|
85
86
|
}
|
|
86
87
|
catch (e) {
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// 实际工时 API 对无数据月份也返回 501("当前token已失效"),
|
|
89
|
+
// 不是真正的 token 过期。不 rethrow,只记录到 failedMonths。
|
|
89
90
|
failedMonths.push({ month: m, error: e instanceof Error ? e.message : String(e) });
|
|
90
|
-
log(`月 ${m}
|
|
91
|
+
log(`月 ${m} 无实际工时数据`);
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
// Apply --department / --role filter
|
|
@@ -106,18 +107,19 @@ export async function summaryCmd(opts) {
|
|
|
106
107
|
export async function exportCmd(opts) {
|
|
107
108
|
const pid = getProjectId(opts);
|
|
108
109
|
const months = expandMonths(opts.from, opts.to);
|
|
110
|
+
const statusFilter = normalizeStatusFilter(opts.status);
|
|
109
111
|
const rows = [];
|
|
110
112
|
const failedMonths = [];
|
|
111
113
|
for (const m of months) {
|
|
112
114
|
try {
|
|
113
|
-
const data = await fetchActual(pid, m);
|
|
115
|
+
const data = await fetchActual(pid, m, statusFilter);
|
|
114
116
|
rows.push(...annotateWithMonth(data, m));
|
|
115
117
|
}
|
|
116
118
|
catch (e) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
// 实际工时 API 对无数据月份也返回 501("当前token已失效"),
|
|
120
|
+
// 不是真正的 token 过期。不 rethrow,只记录到 failedMonths。
|
|
119
121
|
failedMonths.push({ month: m, error: e instanceof Error ? e.message : String(e) });
|
|
120
|
-
log(`月 ${m}
|
|
122
|
+
log(`月 ${m} 无实际工时数据`);
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
// Apply --department / --role filter
|
|
@@ -36,25 +36,24 @@ function filterBaseline(details, opts) {
|
|
|
36
36
|
}
|
|
37
37
|
export async function monthCmd(opts) {
|
|
38
38
|
const pid = getProjectId(opts);
|
|
39
|
+
// 基线 API 一次返回项目全部月份数据,不在月份循环内调用(B1)
|
|
39
40
|
const months = opts.month ? [opts.month] : expandMonths(opts.from, opts.to);
|
|
41
|
+
const data = await fetchBaselineMonth(pid, months[0]);
|
|
40
42
|
const allDetails = [];
|
|
41
|
-
for (const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
areaCode,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
43
|
+
for (const item of (data ?? [])) {
|
|
44
|
+
const details = (item.linePlanMonthDetailList ?? []);
|
|
45
|
+
const mpType = String(item.mpType ?? '');
|
|
46
|
+
const areaCode = item.areaCode;
|
|
47
|
+
for (const d of details) {
|
|
48
|
+
if (isMonthKey(d)) {
|
|
49
|
+
allDetails.push({
|
|
50
|
+
month: monthTsToKey(d.month),
|
|
51
|
+
manpower: d.manpower ?? 0,
|
|
52
|
+
role: item.role,
|
|
53
|
+
departmentName: item.departmentName,
|
|
54
|
+
mpType,
|
|
55
|
+
areaCode,
|
|
56
|
+
});
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
}
|
|
@@ -79,17 +78,16 @@ export async function monthCmd(opts) {
|
|
|
79
78
|
}
|
|
80
79
|
export async function summaryCmd(opts) {
|
|
81
80
|
const pid = getProjectId(opts);
|
|
81
|
+
// 基线 API 一次返回项目全部月份数据,不在月份循环内调用(B1)
|
|
82
82
|
const months = opts.month ? [opts.month] : expandMonths(opts.from, opts.to);
|
|
83
|
+
const data = await fetchBaselineMonth(pid, months[0]);
|
|
83
84
|
const allDetails = [];
|
|
84
|
-
for (const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (isMonthKey(d)) {
|
|
91
|
-
allDetails.push({ month: monthTsToKey(d.month), manpower: d.manpower ?? 0, role: item.role, departmentName: item.departmentName, mpType, areaCode });
|
|
92
|
-
}
|
|
85
|
+
for (const item of (data ?? [])) {
|
|
86
|
+
const mpType = String(item.mpType ?? '');
|
|
87
|
+
const areaCode = item.areaCode;
|
|
88
|
+
for (const d of (item.linePlanMonthDetailList ?? [])) {
|
|
89
|
+
if (isMonthKey(d)) {
|
|
90
|
+
allDetails.push({ month: monthTsToKey(d.month), manpower: d.manpower ?? 0, role: item.role, departmentName: item.departmentName, mpType, areaCode });
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
}
|
|
@@ -113,15 +111,14 @@ export async function summaryCmd(opts) {
|
|
|
113
111
|
}
|
|
114
112
|
export async function exportCmd(opts) {
|
|
115
113
|
const pid = getProjectId(opts);
|
|
114
|
+
// 基线 API 一次返回项目全部月份数据,不在月份循环内调用(B1)
|
|
116
115
|
const months = expandMonths(opts.from, opts.to);
|
|
116
|
+
const data = await fetchBaselineMonth(pid, months[0]);
|
|
117
117
|
const rows = [];
|
|
118
|
-
for (const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (isMonthKey(d)) {
|
|
123
|
-
rows.push({ projectId: pid, month: monthTsToKey(d.month), role: item.role ?? '', manpower: d.manpower ?? 0 });
|
|
124
|
-
}
|
|
118
|
+
for (const item of (data ?? [])) {
|
|
119
|
+
for (const d of (item.linePlanMonthDetailList ?? [])) {
|
|
120
|
+
if (isMonthKey(d)) {
|
|
121
|
+
rows.push({ projectId: pid, month: monthTsToKey(d.month), role: item.role ?? '', manpower: d.manpower ?? 0 });
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
124
|
}
|
|
@@ -6,49 +6,51 @@ import { monthTsToKey } from '../../util/time.js';
|
|
|
6
6
|
import { groupByAxis, mergeBaselineActual, } from './_logic.js';
|
|
7
7
|
import { resolveProjectId, resolveProjectInfo } from '../../util/env.js';
|
|
8
8
|
import { readBanmaIdentity } from '../../auth/session.js';
|
|
9
|
-
import { flattenTree as flattenActualTree } from '../actual/_logic.js';
|
|
9
|
+
import { flattenTree as flattenActualTree, normalizeStatusFilter } from '../actual/_logic.js';
|
|
10
10
|
function getProjectId(opts) {
|
|
11
11
|
return resolveProjectId(opts.projectId);
|
|
12
12
|
}
|
|
13
13
|
export async function compareCmd(opts) {
|
|
14
14
|
const pid = getProjectId(opts);
|
|
15
15
|
const months = opts.month ? [opts.month] : (opts.from || opts.to ? expandMonths(opts.from, opts.to) : expandMonthsDefault());
|
|
16
|
+
const statusFilter = normalizeStatusFilter(opts.status);
|
|
16
17
|
const allBl = [];
|
|
17
18
|
const allAc = [];
|
|
18
19
|
const client = createClient();
|
|
19
20
|
const failedMonths = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
21
|
+
// 基线 API 一次返回项目全部月份数据,在循环外只调用一次(B1)
|
|
22
|
+
try {
|
|
23
|
+
const blData = await client.post('/yuntu-service/line/plan/month/select.json', { projectId: pid, month: months[0] ?? '' });
|
|
24
|
+
for (const item of (blData ?? [])) {
|
|
25
|
+
const details = (item.linePlanMonthDetailList ?? []);
|
|
26
|
+
for (const d of details) {
|
|
27
|
+
if (d && typeof d === 'object') {
|
|
28
|
+
const monthVal = d.month;
|
|
29
|
+
const monthKey = typeof monthVal === 'number'
|
|
30
|
+
? monthTsToKey(monthVal)
|
|
31
|
+
: months[0];
|
|
32
|
+
allBl.push({ month: monthKey, manpower: Number(d.manpower ?? 0), role: item.role, departmentName: item.departmentName });
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof SessionExpiredError)
|
|
39
|
+
throw e;
|
|
40
|
+
log(`基线数据拉取失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
41
|
+
}
|
|
42
|
+
for (const m of months) {
|
|
41
43
|
// 指数退避:避免连续触发限流(B2)
|
|
42
44
|
try {
|
|
43
45
|
const identity = await readBanmaIdentity();
|
|
44
46
|
const loginStaffId = identity?.staffId ?? '';
|
|
45
|
-
const raw = await client.post('/yuntu-service/manpower/weekly/
|
|
47
|
+
const raw = await client.post('/yuntu-service/manpower/weekly/summaryByProject.json', {
|
|
46
48
|
month: m, staffIds: [], projectIds: [pid], isConfirm: false, loginStaffId,
|
|
47
49
|
});
|
|
48
50
|
const tree = (Array.isArray(raw) ? raw : []);
|
|
49
51
|
const results = [];
|
|
50
52
|
if (tree.length > 0 && tree[0].children) {
|
|
51
|
-
const entries = flattenActualTree(tree[0].children, pid);
|
|
53
|
+
const entries = flattenActualTree(tree[0].children, pid, statusFilter);
|
|
52
54
|
for (const p of entries) {
|
|
53
55
|
results.push({ month: m, manpower: p.manpower ?? 0, role: p.role, departmentName: p.departmentName });
|
|
54
56
|
}
|
|
@@ -58,8 +60,10 @@ export async function compareCmd(opts) {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
catch (e) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
// 实际工时 API 对无数据月份也返回 501(错误信息"当前token已失效"),
|
|
64
|
+
// 这不是真正的 token 过期——基线 API 正常就说明 token 有效。
|
|
65
|
+
// 所以不 rethrow SessionExpiredError,只记录到 failedMonths。
|
|
66
|
+
// 真正的 token 过期会由基线 API 的 catch 块捕获并抛出。
|
|
63
67
|
failedMonths.push({ month: m, error: `实际工时拉取失败: ${e instanceof Error ? e.message : String(e)}` });
|
|
64
68
|
}
|
|
65
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ATLAS_VERSION = '0.7.
|
|
1
|
+
export const ATLAS_VERSION = '0.7.22';
|