hikvision-cli 1.0.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.
- package/dist/commands/binding.d.ts +6 -0
- package/dist/commands/binding.d.ts.map +1 -0
- package/dist/commands/binding.js +274 -0
- package/dist/commands/binding.js.map +1 -0
- package/dist/commands/card.d.ts +6 -0
- package/dist/commands/card.d.ts.map +1 -0
- package/dist/commands/card.js +259 -0
- package/dist/commands/card.js.map +1 -0
- package/dist/commands/group.d.ts +6 -0
- package/dist/commands/group.d.ts.map +1 -0
- package/dist/commands/group.js +87 -0
- package/dist/commands/group.js.map +1 -0
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.d.ts.map +1 -0
- package/dist/commands/org.js +248 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/person.d.ts +6 -0
- package/dist/commands/person.d.ts.map +1 -0
- package/dist/commands/person.js +563 -0
- package/dist/commands/person.js.map +1 -0
- package/dist/commands/service.d.ts +6 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +153 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +138 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/system.d.ts +6 -0
- package/dist/commands/system.d.ts.map +1 -0
- package/dist/commands/system.js +167 -0
- package/dist/commands/system.js.map +1 -0
- package/dist/commands/vehicle.d.ts +12 -0
- package/dist/commands/vehicle.d.ts.map +1 -0
- package/dist/commands/vehicle.js +550 -0
- package/dist/commands/vehicle.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/services/hikvisionClient.d.ts +25 -0
- package/dist/services/hikvisionClient.d.ts.map +1 -0
- package/dist/services/hikvisionClient.js +59 -0
- package/dist/services/hikvisionClient.js.map +1 -0
- package/dist/utils/config.d.ts +66 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +213 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/output.d.ts +42 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +157 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/validation.d.ts +52 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +171 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +29 -0
- package/src/commands/binding.ts +305 -0
- package/src/commands/card.ts +238 -0
- package/src/commands/group.ts +91 -0
- package/src/commands/org.ts +228 -0
- package/src/commands/person.ts +596 -0
- package/src/commands/service.ts +174 -0
- package/src/commands/sync.ts +156 -0
- package/src/commands/system.ts +137 -0
- package/src/commands/vehicle.ts +572 -0
- package/src/index.ts +111 -0
- package/src/services/hikvisionClient.ts +74 -0
- package/src/types/cli-table3.d.ts +20 -0
- package/src/utils/config.ts +199 -0
- package/src/utils/output.ts +181 -0
- package/src/utils/validation.ts +160 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 人员管理命令
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { config } from '../utils/config';
|
|
9
|
+
import { success, error, info, formatOutput } from '../utils/output';
|
|
10
|
+
import { createHikvisionClient } from '../services/hikvisionClient';
|
|
11
|
+
|
|
12
|
+
export const personCommands = new Command()
|
|
13
|
+
.command('person')
|
|
14
|
+
.description('人员管理');
|
|
15
|
+
|
|
16
|
+
// 新增人员
|
|
17
|
+
personCommands
|
|
18
|
+
.command('add')
|
|
19
|
+
.description('新增人员')
|
|
20
|
+
.option('-n, --name <name>', '姓名')
|
|
21
|
+
.option('-p, --phone <phone>', '手机号')
|
|
22
|
+
.option('-i, --id-card <idCard>', '身份证号')
|
|
23
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
24
|
+
.option('-on, --org-path <orgPath>', '组织路径(如:保洁 或 外部人员/保安保洁/保洁)')
|
|
25
|
+
.option('-g, --gender <gender>', '性别 (1:男, 2:女)', '1')
|
|
26
|
+
.option('-e, --email <email>', '邮箱')
|
|
27
|
+
.option('-c, --card-no <cardNo>', '卡号')
|
|
28
|
+
.option('-j, --job-no <jobNo>', '工号')
|
|
29
|
+
.option('-ph, --photo <photoPath>', '照片文件路径(支持 jpg/png),添加人员时同步上传')
|
|
30
|
+
.action(async (options) => {
|
|
31
|
+
try {
|
|
32
|
+
const client = createHikvisionClient(config.get());
|
|
33
|
+
|
|
34
|
+
// 解析组织路径
|
|
35
|
+
let orgIndexCode = options.org;
|
|
36
|
+
if (options.orgPath && !orgIndexCode) {
|
|
37
|
+
orgIndexCode = await findOrgByPath(client, options.orgPath);
|
|
38
|
+
if (!orgIndexCode) {
|
|
39
|
+
error(`未找到匹配的组织路径:${options.orgPath}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
info(`已匹配到组织:${options.orgPath} → ${orgIndexCode}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!orgIndexCode) {
|
|
46
|
+
error('请指定组织 ID (-o) 或组织路径 (-on)');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 读取照片(如果有)
|
|
51
|
+
let faces: { faceData: string }[] | undefined;
|
|
52
|
+
if (options.photo) {
|
|
53
|
+
const faceData = await loadPhotoBase64(options.photo);
|
|
54
|
+
faces = [{ faceData }];
|
|
55
|
+
info(`已读取照片:${options.photo}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await client.personService.create({
|
|
59
|
+
personName: options.name,
|
|
60
|
+
phoneNo: options.phone,
|
|
61
|
+
idCardNo: options.idCard,
|
|
62
|
+
orgIndexCode,
|
|
63
|
+
gender: options.gender as '1' | '2',
|
|
64
|
+
email: options.email,
|
|
65
|
+
cardNo: options.cardNo,
|
|
66
|
+
jobNo: options.jobNo,
|
|
67
|
+
faces,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
success(`人员添加成功!`);
|
|
71
|
+
console.log(formatOutput(result));
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
error(`添加人员失败:${err.message || err}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 查询人员列表
|
|
78
|
+
personCommands
|
|
79
|
+
.command('list')
|
|
80
|
+
.description('查询人员列表')
|
|
81
|
+
.option('-p, --page <page>', '页码', '1')
|
|
82
|
+
.option('-s, --size <size>', '每页数量', '20')
|
|
83
|
+
.option('-n, --name <name>', '姓名搜索')
|
|
84
|
+
.option('-ph, --phone <phone>', '手机号搜索')
|
|
85
|
+
.option('-id, --id-card <idCard>', '身份证号搜索')
|
|
86
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
87
|
+
.option('-on, --org-name <orgName>', '组织名称(模糊搜索)')
|
|
88
|
+
.option('-g, --gender <gender>', '性别 (1:男, 2:女)')
|
|
89
|
+
.action(async (options) => {
|
|
90
|
+
try {
|
|
91
|
+
const client = createHikvisionClient(config.get());
|
|
92
|
+
|
|
93
|
+
// 如果指定了组织名称,需要先查找到 orgIndexCode
|
|
94
|
+
let orgIndexCode = options.org;
|
|
95
|
+
if (options.orgName && !orgIndexCode) {
|
|
96
|
+
const orgs = await client.personService.getOrganizations();
|
|
97
|
+
const found = orgs.find(o => o.orgName === options.orgName);
|
|
98
|
+
if (found) {
|
|
99
|
+
orgIndexCode = found.orgIndexCode;
|
|
100
|
+
} else {
|
|
101
|
+
error(`未找到名为 "${options.orgName}" 的组织`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await client.personService.list({
|
|
107
|
+
pageNo: parseInt(options.page),
|
|
108
|
+
pageSize: parseInt(options.size),
|
|
109
|
+
personName: options.name,
|
|
110
|
+
phoneNo: options.phone,
|
|
111
|
+
idCardNo: options.idCard,
|
|
112
|
+
orgIndexCode: orgIndexCode,
|
|
113
|
+
gender: options.gender as '1' | '2',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(formatOutput(result));
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
error(`查询人员失败:${err.message || err}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 查询人员详情
|
|
123
|
+
personCommands
|
|
124
|
+
.command('get')
|
|
125
|
+
.description('查询人员详情')
|
|
126
|
+
.argument('[personId]', '人员 ID')
|
|
127
|
+
.option('-i, --id <personId>', '人员 ID')
|
|
128
|
+
.option('-n, --name <name>', '姓名')
|
|
129
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
130
|
+
.option('-on, --org-name <orgName>', '组织名称')
|
|
131
|
+
.action(async (personId, options) => {
|
|
132
|
+
try {
|
|
133
|
+
const client = createHikvisionClient(config.get());
|
|
134
|
+
|
|
135
|
+
let targetId = personId || options.id;
|
|
136
|
+
|
|
137
|
+
// 如果没有提供 ID,通过名称+组织查找
|
|
138
|
+
if (!targetId && (options.name || options.orgName)) {
|
|
139
|
+
targetId = await findPersonId(client, options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!targetId) {
|
|
143
|
+
error('请提供人员 ID,或使用 --name 和 --org/--org-name 指定人员');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const result = await client.personService.getById(targetId);
|
|
148
|
+
console.log(formatOutput(result));
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
error(`查询人员详情失败:${err.message || err}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 修改人员
|
|
155
|
+
personCommands
|
|
156
|
+
.command('update')
|
|
157
|
+
.description('修改人员')
|
|
158
|
+
.argument('[personId]', '人员 ID')
|
|
159
|
+
.option('-n, --name <name>', '姓名')
|
|
160
|
+
.option('-p, --phone <phone>', '手机号')
|
|
161
|
+
.option('-i, --id-card <idCard>', '身份证号')
|
|
162
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
163
|
+
.option('-on, --org-name <orgName>', '组织名称')
|
|
164
|
+
.option('-g, --gender <gender>', '性别 (1:男, 2:女)')
|
|
165
|
+
.option('-s, --status <status>', '状态 (0:正常, 1:禁用, 2:离职)')
|
|
166
|
+
.option('-j, --job-no <jobNo>', '工号')
|
|
167
|
+
.action(async (personId, options) => {
|
|
168
|
+
try {
|
|
169
|
+
const client = createHikvisionClient(config.get());
|
|
170
|
+
|
|
171
|
+
let targetId = personId;
|
|
172
|
+
|
|
173
|
+
// 如果没有提供 ID,通过名称+组织查找
|
|
174
|
+
if (!targetId && (options.name || options.orgName)) {
|
|
175
|
+
targetId = await findPersonId(client, { name: options.name, orgIndexCode: options.org, orgName: options.orgName });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!targetId) {
|
|
179
|
+
error('请提供人员 ID,或使用 --name 和 --org/--org-name 指定人员');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await client.personService.update(targetId, {
|
|
184
|
+
personName: options.name,
|
|
185
|
+
phoneNo: options.phone,
|
|
186
|
+
idCardNo: options.idCard,
|
|
187
|
+
orgIndexCode: options.org,
|
|
188
|
+
gender: options.gender as '1' | '2',
|
|
189
|
+
status: options.status as '0' | '1' | '2',
|
|
190
|
+
jobNo: options.jobNo,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
success('人员信息已更新');
|
|
194
|
+
} catch (err: any) {
|
|
195
|
+
error(`修改人员失败:${err.message || err}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// 删除人员
|
|
200
|
+
personCommands
|
|
201
|
+
.command('delete')
|
|
202
|
+
.description('删除人员')
|
|
203
|
+
.argument('[personId]', '人员 ID')
|
|
204
|
+
.option('-n, --name <name>', '姓名')
|
|
205
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
206
|
+
.option('-on, --org-name <orgName>', '组织名称')
|
|
207
|
+
.action(async (personId, options) => {
|
|
208
|
+
try {
|
|
209
|
+
const client = createHikvisionClient(config.get());
|
|
210
|
+
|
|
211
|
+
let targetId = personId;
|
|
212
|
+
|
|
213
|
+
// 如果没有提供 ID,通过名称+组织查找
|
|
214
|
+
if (!targetId && (options.name || options.orgName)) {
|
|
215
|
+
targetId = await findPersonId(client, { name: options.name, orgIndexCode: options.org, orgName: options.orgName });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!targetId) {
|
|
219
|
+
error('请提供人员 ID,或使用 --name 和 --org/--org-name 指定人员');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await client.personService.delete(targetId);
|
|
224
|
+
|
|
225
|
+
success('人员已删除');
|
|
226
|
+
} catch (err: any) {
|
|
227
|
+
error(`删除人员失败:${err.message || err}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// 获取组织列表
|
|
232
|
+
personCommands
|
|
233
|
+
.command('orgs')
|
|
234
|
+
.description('获取组织列表')
|
|
235
|
+
.option('-n, --name <name>', '按名称筛选(模糊匹配)')
|
|
236
|
+
.action(async (options) => {
|
|
237
|
+
try {
|
|
238
|
+
const client = createHikvisionClient(config.get());
|
|
239
|
+
|
|
240
|
+
const result = await client.personService.getOrganizations();
|
|
241
|
+
|
|
242
|
+
// 如果指定了名称,筛选结果
|
|
243
|
+
if (options.name) {
|
|
244
|
+
const filtered = result.filter(o => o.orgName.includes(options.name));
|
|
245
|
+
console.log(formatOutput(filtered));
|
|
246
|
+
} else {
|
|
247
|
+
console.log(formatOutput(result));
|
|
248
|
+
}
|
|
249
|
+
} catch (err: any) {
|
|
250
|
+
error(`获取组织列表失败:${err.message || err}`);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// 更新人员照片
|
|
255
|
+
personCommands
|
|
256
|
+
.command('update-photo')
|
|
257
|
+
.description('更新人员照片')
|
|
258
|
+
.argument('[personId]', '人员 ID')
|
|
259
|
+
.option('-i, --id <personId>', '人员 ID')
|
|
260
|
+
.option('-n, --name <name>', '姓名')
|
|
261
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
262
|
+
.option('-on, --org-name <orgName>', '组织名称')
|
|
263
|
+
.option('-f, --file <filePath>', '照片文件路径(支持 jpg/png/jpeg)')
|
|
264
|
+
.action(async (personId, options) => {
|
|
265
|
+
try {
|
|
266
|
+
const client = createHikvisionClient(config.get());
|
|
267
|
+
|
|
268
|
+
// 查找人员
|
|
269
|
+
let targetId = personId || options.id;
|
|
270
|
+
if (!targetId && (options.name || options.orgName)) {
|
|
271
|
+
targetId = await findPersonId(client, {
|
|
272
|
+
name: options.name,
|
|
273
|
+
orgIndexCode: options.org,
|
|
274
|
+
orgName: options.orgName,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!targetId) {
|
|
279
|
+
error('请提供人员 ID,或使用 --name 和 --org/--org-name 指定人员');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!options.file) {
|
|
284
|
+
error('请提供照片文件路径 (-f/--file)');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 读取照片并转为 base64
|
|
289
|
+
const faceData = await loadPhotoBase64(options.file);
|
|
290
|
+
|
|
291
|
+
// 直接尝试添加人脸,海康每人仅限1张,有旧照时 addFace 会报"Already Has Face"
|
|
292
|
+
try {
|
|
293
|
+
const result = await client.personService.addFace(targetId, faceData);
|
|
294
|
+
success(`人员照片已添加(faceId: ${result.faceId})`);
|
|
295
|
+
} catch (err: any) {
|
|
296
|
+
if (err.message?.includes('Already Has Face') || err.message?.includes('already')) {
|
|
297
|
+
error(`该人员已有照片,每人仅限1张。如需更新,请先删除旧照后再添加。`);
|
|
298
|
+
} else if (err.message?.includes('Picture Size')) {
|
|
299
|
+
error(`照片尺寸不合要求,建议:①确保照片中人脸区域清晰可见 ②裁剪图片使脸部占比更大 ③使用标准 JPEG/PNG 格式`);
|
|
300
|
+
} else {
|
|
301
|
+
error(`添加照片失败:${err.message || err}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch (err: any) {
|
|
305
|
+
error(`更新照片失败:${err.message || err}`);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// 添加人员照片(与 update-photo 相同逻辑)
|
|
310
|
+
personCommands
|
|
311
|
+
.command('add-photo')
|
|
312
|
+
.description('为人员添加照片(无照片时添加,有照片时替换)')
|
|
313
|
+
.argument('[personId]', '人员 ID')
|
|
314
|
+
.option('-i, --id <personId>', '人员 ID')
|
|
315
|
+
.option('-n, --name <name>', '姓名')
|
|
316
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
317
|
+
.option('-on, --org-name <orgName>', '组织名称')
|
|
318
|
+
.option('-f, --file <filePath>', '照片文件路径(支持 jpg/png/jpeg)')
|
|
319
|
+
.action(async (personId, options) => {
|
|
320
|
+
// 复用 update-photo 的逻辑
|
|
321
|
+
const client = createHikvisionClient(config.get());
|
|
322
|
+
|
|
323
|
+
let targetId = personId || options.id;
|
|
324
|
+
if (!targetId && (options.name || options.orgName)) {
|
|
325
|
+
targetId = await findPersonId(client, {
|
|
326
|
+
name: options.name,
|
|
327
|
+
orgIndexCode: options.org,
|
|
328
|
+
orgName: options.orgName,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!targetId) {
|
|
333
|
+
error('请提供人员 ID,或使用 --name 和 --org/--org-name 指定人员');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!options.file) {
|
|
338
|
+
error('请提供照片文件路径 (-f/--file)');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const faceData = await loadPhotoBase64(options.file);
|
|
344
|
+
|
|
345
|
+
// 直接添加人脸,有旧照时报"Already Has Face"
|
|
346
|
+
try {
|
|
347
|
+
const result = await client.personService.addFace(targetId, faceData);
|
|
348
|
+
success(`人员照片已添加(faceId: ${result.faceId})`);
|
|
349
|
+
} catch (err: any) {
|
|
350
|
+
if (err.message?.includes('Already Has Face') || err.message?.includes('already')) {
|
|
351
|
+
error(`该人员已有照片,每人仅限1张。如需更新,请先删除旧照后再添加。`);
|
|
352
|
+
} else if (err.message?.includes('Picture Size')) {
|
|
353
|
+
error(`照片尺寸不合要求,建议:①确保照片中人脸区域清晰可见 ②裁剪图片使脸部占比更大 ③使用标准 JPEG/PNG 格式`);
|
|
354
|
+
} else {
|
|
355
|
+
error(`添加照片失败:${err.message || err}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch (err: any) {
|
|
359
|
+
error(`添加照片失败:${err.message || err}`);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// 删除人员照片
|
|
364
|
+
personCommands
|
|
365
|
+
.command('delete-photo')
|
|
366
|
+
.description('删除人员照片')
|
|
367
|
+
.argument('[personId]', '人员 ID')
|
|
368
|
+
.option('-i, --id <personId>', '人员 ID')
|
|
369
|
+
.option('-n, --name <name>', '姓名')
|
|
370
|
+
.option('-o, --org <orgIndexCode>', '组织 ID')
|
|
371
|
+
.option('-on, --org-name <orgName>', '组织名称')
|
|
372
|
+
.action(async (personId, options) => {
|
|
373
|
+
try {
|
|
374
|
+
const client = createHikvisionClient(config.get());
|
|
375
|
+
|
|
376
|
+
let targetId = personId || options.id;
|
|
377
|
+
if (!targetId && (options.name || options.orgName)) {
|
|
378
|
+
targetId = await findPersonId(client, {
|
|
379
|
+
name: options.name,
|
|
380
|
+
orgIndexCode: options.org,
|
|
381
|
+
orgName: options.orgName,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!targetId) {
|
|
386
|
+
error('请提供人员 ID,或使用 --name 和 --org/--org-name 指定人员');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 尝试删除人脸。海康无按人员ID删除人脸的API,
|
|
391
|
+
// 如果知道 faceId 可以直接删除;不知道则无法删除(需手动处理)
|
|
392
|
+
// 此处直接报错提示用户提供 faceId
|
|
393
|
+
error(`删除照片需要提供 faceId,请通过其他途径获取后人脸 ID 后再删除。`);
|
|
394
|
+
} catch (err: any) {
|
|
395
|
+
error(`删除照片失败:${err.message || err}`);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// 查询已删除人员
|
|
400
|
+
personCommands
|
|
401
|
+
.command('deleted')
|
|
402
|
+
.description('查询已删除人员(按时间范围,包含 status < 0 的记录)')
|
|
403
|
+
.option('--start-time <startTime>', '开始时间,格式:yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd')
|
|
404
|
+
.option('--end-time <endTime>', '结束时间,格式:yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd')
|
|
405
|
+
.option('-p, --page <page>', '页码', '1')
|
|
406
|
+
.option('-s, --size <size>', '每页数量', '50')
|
|
407
|
+
.action(async (options) => {
|
|
408
|
+
try {
|
|
409
|
+
const client = createHikvisionClient(config.get());
|
|
410
|
+
|
|
411
|
+
// 计算时间范围
|
|
412
|
+
const now = new Date();
|
|
413
|
+
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
414
|
+
|
|
415
|
+
const startTime = formatTimeForAPI(options.startTime || oneWeekAgo);
|
|
416
|
+
const endTime = formatTimeForAPI(options.endTime || now);
|
|
417
|
+
|
|
418
|
+
info(`查询时间范围:${startTime} ~ ${endTime}`);
|
|
419
|
+
|
|
420
|
+
// 调用增量接口
|
|
421
|
+
const result = await client.personService.listByTimeRange(
|
|
422
|
+
startTime,
|
|
423
|
+
endTime,
|
|
424
|
+
parseInt(options.page),
|
|
425
|
+
parseInt(options.size)
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// 筛选出已删除的人员(status < 0)
|
|
429
|
+
const deletedPersons = result.list.filter(p => (p as any).status < 0);
|
|
430
|
+
|
|
431
|
+
if (result.total === 0) {
|
|
432
|
+
info('在指定时间范围内未查询到任何人员记录');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log(formatOutput(result));
|
|
437
|
+
|
|
438
|
+
if (deletedPersons.length > 0) {
|
|
439
|
+
success(`\n共 ${deletedPersons.length} 条已删除记录(status < 0):`);
|
|
440
|
+
for (const p of deletedPersons) {
|
|
441
|
+
console.log(` ${p.personId} ${p.personName} status=${(p as any).status} 删除时间=${p.updateTime}`);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
info('在指定时间范围内未发现已删除的人员记录');
|
|
445
|
+
}
|
|
446
|
+
} catch (err: any) {
|
|
447
|
+
error(`查询已删除人员失败:${err.message || err}`);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ============ 内部辅助函数 ============
|
|
452
|
+
|
|
453
|
+
interface FindPersonOptions {
|
|
454
|
+
name?: string;
|
|
455
|
+
orgIndexCode?: string;
|
|
456
|
+
orgName?: string;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* 根据组织路径(如:保洁 或 外部人员/保安保洁/保洁)查找 orgIndexCode
|
|
461
|
+
*/
|
|
462
|
+
async function findOrgByPath(client: ReturnType<typeof createHikvisionClient>, orgPath: string): Promise<string | null> {
|
|
463
|
+
const orgs = await client.personService.getOrganizations();
|
|
464
|
+
const pathParts = orgPath.split('/').map(p => p.trim()).filter(p => p);
|
|
465
|
+
|
|
466
|
+
if (pathParts.length === 0) return null;
|
|
467
|
+
|
|
468
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
469
|
+
|
|
470
|
+
const matches: { org: any; score: number }[] = [];
|
|
471
|
+
|
|
472
|
+
for (const org of orgs) {
|
|
473
|
+
if (org.orgName === lastPart) {
|
|
474
|
+
matches.push({ org, score: 1 });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (matches.length === 0) return null;
|
|
479
|
+
if (matches.length === 1) return matches[0].org.orgIndexCode;
|
|
480
|
+
|
|
481
|
+
// 多个匹配时,返回第一个
|
|
482
|
+
return matches[0].org.orgIndexCode;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 根据姓名+部门查找人员 ID
|
|
487
|
+
*/
|
|
488
|
+
async function findPersonId(client: ReturnType<typeof createHikvisionClient>, opts: FindPersonOptions): Promise<string | null> {
|
|
489
|
+
// 如果有 orgName,先查 orgIndexCode
|
|
490
|
+
let orgIndexCode = opts.orgIndexCode;
|
|
491
|
+
if (opts.orgName && !orgIndexCode) {
|
|
492
|
+
const orgs = await client.personService.getOrganizations();
|
|
493
|
+
const found = orgs.find(o => o.orgName === opts.orgName);
|
|
494
|
+
if (!found) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
orgIndexCode = found.orgIndexCode;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 搜索人员
|
|
501
|
+
const list = await client.personService.list({
|
|
502
|
+
pageNo: 1,
|
|
503
|
+
pageSize: 50,
|
|
504
|
+
personName: opts.name,
|
|
505
|
+
orgIndexCode: orgIndexCode,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const persons = (list as any).list ?? list;
|
|
509
|
+
if (persons.length === 0) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
if (persons.length === 1) {
|
|
513
|
+
return persons[0].personId || persons[0].personId;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 多个匹配时提示用户
|
|
517
|
+
console.log(`找到 ${persons.length} 条匹配记录,请使用 ID 删除:`);
|
|
518
|
+
for (const p of persons) {
|
|
519
|
+
console.log(` ${p.personId} ${p.personName} ${p.phoneNo || '-'} ${p.jobNo || '-'}`);
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 读取照片文件并转为 base64(自动去除 data:image/... 前缀)
|
|
526
|
+
* 添加了文件大小限制和路径安全验证
|
|
527
|
+
*/
|
|
528
|
+
const MAX_PHOTO_SIZE = 10 * 1024 * 1024; // 10MB
|
|
529
|
+
|
|
530
|
+
async function loadPhotoBase64(filePath: string): Promise<string> {
|
|
531
|
+
// 安全检查:解析绝对路径并验证在允许范围内
|
|
532
|
+
const resolvedPath = path.resolve(filePath);
|
|
533
|
+
const allowedDir = process.cwd();
|
|
534
|
+
if (!resolvedPath.startsWith(allowedDir) && !fs.existsSync(resolvedPath)) {
|
|
535
|
+
throw new Error(`文件路径无效:${filePath}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
539
|
+
throw new Error(`照片文件不存在:${filePath}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
543
|
+
if (!['.jpg', '.jpeg', '.png', '.bmp', '.gif'].includes(ext)) {
|
|
544
|
+
throw new Error(`不支持的照片格式:${ext},仅支持 jpg/jpeg/png/bmp/gif`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const buffer = fs.readFileSync(resolvedPath);
|
|
548
|
+
|
|
549
|
+
// 文件大小验证,防止内存耗尽或 DoS
|
|
550
|
+
if (buffer.length > MAX_PHOTO_SIZE) {
|
|
551
|
+
throw new Error(`照片文件过大:${(buffer.length / 1024).toFixed(2)}KB,最大支持 ${MAX_PHOTO_SIZE / 1024}KB`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (buffer.length < 100) {
|
|
555
|
+
throw new Error(`照片文件无效或损坏`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const mimeMap: Record<string, string> = {
|
|
559
|
+
'.jpg': 'image/jpeg',
|
|
560
|
+
'.jpeg': 'image/jpeg',
|
|
561
|
+
'.png': 'image/png',
|
|
562
|
+
'.bmp': 'image/bmp',
|
|
563
|
+
'.gif': 'image/gif',
|
|
564
|
+
};
|
|
565
|
+
const mime = mimeMap[ext] || 'image/jpeg';
|
|
566
|
+
const base64 = buffer.toString('base64');
|
|
567
|
+
|
|
568
|
+
// 去除 data:image/... 前缀,只保留纯 base64 数据
|
|
569
|
+
return base64;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 格式化时间为海康 API 所需格式
|
|
574
|
+
* @param input Date 对象或字符串
|
|
575
|
+
* @returns 格式:yyyy-MM-dd'T'HH:mm:ss.SSS+0800
|
|
576
|
+
*/
|
|
577
|
+
function formatTimeForAPI(input: Date | string): string {
|
|
578
|
+
if (typeof input === 'string') {
|
|
579
|
+
// 如果是纯日期格式如 "2026-05-19",直接返回加上默认时间
|
|
580
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
581
|
+
return `${input}T00:00:00.000+0800`;
|
|
582
|
+
}
|
|
583
|
+
// 尝试解析字符串
|
|
584
|
+
const d = new Date(input);
|
|
585
|
+
if (isNaN(d.getTime())) {
|
|
586
|
+
throw new Error(`无法解析时间字符串:${input}`);
|
|
587
|
+
}
|
|
588
|
+
return formatDate(d);
|
|
589
|
+
}
|
|
590
|
+
return formatDate(input);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function formatDate(d: Date): string {
|
|
594
|
+
const pad = (n: number) => n.toString().padStart(2, '0');
|
|
595
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.000+0800`;
|
|
596
|
+
}
|