@xiaoyuanhao/yunzhi-cli 2.0.0 → 2.1.1

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.
Files changed (63) hide show
  1. package/lib/arguments.cjs +7 -4
  2. package/lib/attendance-clock-entry.cjs +31 -0
  3. package/lib/attendance-configuration-entry.cjs +35 -0
  4. package/lib/attendance-correction-applications.cjs +64 -0
  5. package/lib/attendance-correction-approval.cjs +135 -0
  6. package/lib/attendance-correction-start.cjs +47 -0
  7. package/lib/attendance-leave-enums.cjs +193 -0
  8. package/lib/attendance-leave-ids.cjs +15 -0
  9. package/lib/attendance-leave-list-filters.cjs +21 -0
  10. package/lib/attendance-leave-route.cjs +20 -0
  11. package/lib/attendance-operation-result.cjs +97 -0
  12. package/lib/attendance-report-inbox.cjs +42 -0
  13. package/lib/attendance-report-remarks.cjs +62 -0
  14. package/lib/attendance-statistics.cjs +49 -0
  15. package/lib/attendance.cjs +404 -0
  16. package/lib/auth.cjs +1 -1
  17. package/lib/business-schemas.cjs +3 -1
  18. package/lib/cli.cjs +294 -1
  19. package/lib/command-guidance.cjs +1162 -1
  20. package/lib/command-reference.cjs +54 -0
  21. package/lib/contact-help.cjs +43 -0
  22. package/lib/contact-mobile.cjs +14 -0
  23. package/lib/contact.cjs +85 -0
  24. package/lib/correction-help.cjs +57 -0
  25. package/lib/dormitory-replenishment.cjs +63 -0
  26. package/lib/dormitory-report-detail.cjs +76 -0
  27. package/lib/dormitory-report-id.cjs +8 -0
  28. package/lib/dormitory-report-summary.cjs +66 -0
  29. package/lib/dormitory-report-view.cjs +36 -0
  30. package/lib/dormitory-rooms.cjs +41 -0
  31. package/lib/dormitory-scope.cjs +18 -0
  32. package/lib/dormitory-statistics.cjs +30 -0
  33. package/lib/download.cjs +17 -8
  34. package/lib/gateway.cjs +9 -4
  35. package/lib/leave-approval-receipt.cjs +126 -0
  36. package/lib/leave-attachment-upload.cjs +110 -0
  37. package/lib/leave-form-users.cjs +48 -0
  38. package/lib/leave-start-receipt.cjs +64 -0
  39. package/lib/leave.cjs +457 -0
  40. package/lib/replenishment-records.cjs +39 -0
  41. package/lib/result.cjs +1 -0
  42. package/lib/saved-report-summary.cjs +16 -0
  43. package/lib/school-contacts.cjs +24 -0
  44. package/lib/school-help.cjs +53 -0
  45. package/lib/school-schema.cjs +38 -0
  46. package/lib/student-application-help.cjs +76 -0
  47. package/lib/student-leave-form.cjs +108 -0
  48. package/lib/student-leave-operations.cjs +24 -0
  49. package/lib/student-leave-query.cjs +75 -0
  50. package/lib/student-leave-resumption.cjs +2 -0
  51. package/lib/student-leave-start.cjs +34 -0
  52. package/lib/student-leave.cjs +51 -0
  53. package/lib/student-query-help.cjs +57 -0
  54. package/lib/student-replenishment.cjs +64 -0
  55. package/lib/student-report-detail.cjs +39 -0
  56. package/lib/student-report-summary.cjs +30 -0
  57. package/lib/teacher-approval-help.cjs +132 -0
  58. package/lib/teacher-leave-files.cjs +112 -0
  59. package/lib/teacher-leave-form-check.cjs +185 -0
  60. package/lib/teacher-leave-query.cjs +99 -0
  61. package/lib/teacher-own-leave-help.cjs +134 -0
  62. package/lib/workflow-entry.cjs +42 -0
  63. package/package.json +1 -1
@@ -0,0 +1,38 @@
1
+ const { required, nullable } = require('./result.cjs');
2
+ const ref = { id: required('string'), name: required('string'), type: nullable('string'), type_name: nullable('string') };
3
+ const summary = { ...ref, parent_id: nullable('string'), path: required([required('string')]),
4
+ has_children: required('boolean'), level: required('integer'), ancestors: required([ref]) };
5
+ const staff = { id: required('string'), name: required('string'), role: nullable('string'), role_code: required('string'),
6
+ subjects: required([{ code: required('string'), name: nullable('string'), source: nullable('string') }]) };
7
+ const department = { ...summary, alias: nullable('string'),
8
+ leaders: required([staff]), leaders_status: required('string'),
9
+ head_teachers: required([staff]), head_teachers_status: required('string'),
10
+ subject_teachers: nullable([staff]), subject_teachers_status: required('string'),
11
+ other_positions: required([staff]), other_positions_status: required('string') };
12
+
13
+ function check(row, summaryOnly, includeTeachers, fail) {
14
+ if (row.ancestors.length !== row.path.length - 1
15
+ || new Set([row.id, ...row.ancestors.map(ref => ref.id)]).size !== row.ancestors.length + 1
16
+ || row.ancestors.some((ref, index) => ref.name !== row.path[index])
17
+ || row.parent_id !== null && row.parent_id !== row.ancestors.at(-1)?.id) fail();
18
+ if (summaryOnly) return;
19
+ const classNode = ['class', 'custom_class', 'interest_class'].includes(row.type);
20
+ for (const key of ['leaders', 'head_teachers', 'subject_teachers', 'other_positions']) {
21
+ const special = key === 'head_teachers' && !classNode ? 'not_applicable'
22
+ : key === 'subject_teachers' ? !classNode ? 'not_applicable' : !includeTeachers ? 'not_requested' : null : null;
23
+ const status = row[`${key}_status`], values = row[key];
24
+ if (special ? status !== special || values?.length : !['complete', 'visible_only'].includes(status) || values === null) fail();
25
+ if (values === null) continue;
26
+ if (new Set(values.map(person => JSON.stringify([person.id, person.role_code]))).size !== values.length) fail();
27
+ for (const person of values) {
28
+ if (new Set(person.subjects.map(subject => JSON.stringify([subject.source, subject.code]))).size !== person.subjects.length) fail();
29
+ }
30
+ }
31
+ }
32
+ function departmentWithMobile(include) {
33
+ if (!include) return department;
34
+ const withMobile = { ...staff, mobile: nullable('string') };
35
+ return { ...department, leaders: required([withMobile]), head_teachers: required([withMobile]),
36
+ subject_teachers: nullable([withMobile]), other_positions: required([withMobile]) };
37
+ }
38
+ module.exports = { summary, departmentWithMobile, check };
@@ -0,0 +1,76 @@
1
+ // Help only. Keep shared field encodings intact without changing teacher commands.
2
+ const { fieldInput } = require('./teacher-own-leave-help.cjs');
3
+ function render(command, usage) {
4
+ const [space, action] = command.split(' ');
5
+ if (space !== 'student-leave') return null;
6
+ const resumed = action.startsWith('resumption-'), kind = resumed ? action.slice(11) : action;
7
+ if (!['form', 'form-people-search', 'submit'].includes(kind)) return null;
8
+ const subject = resumed ? '销假' : '请假', prefix = resumed ? 'resumption-' : '';
9
+ const context = resumed ? ['--leave-id 必填;原学生请假 ID,来自 list/show。', '--student-id 可选;原请假有多名可办理学生时,用于指定学生。']
10
+ : ['--class-id 必填;学生所属的办理班级。', '--student-id 必填;请假学生,来自 students。'];
11
+ const process = `--workflow-code 必填;${prefix}form 返回的已选流程编码。`;
12
+ let purpose, parameters, result, limits, extra = '', failure = '', example;
13
+ if (kind === 'form') {
14
+ purpose = `查看代学生办理${subject}需要填写哪些内容,并一次列出当前答案中需要补充或修正的地方。`;
15
+ parameters = [...context, '--workflow-code 可选;唯一流程自动选择,多个流程返回选项。',
16
+ `--field-values 可选;当前完整答案,JSON 数组或 @UTF-8 文件;不是增量补丁。格式见 ${prefix}submit --help。`,
17
+ ...(resumed ? ['--resumption-time 可选;实际返校时间,YYYY-MM-DD HH:mm:ss,北京时间;提交时必填。', '--reason 可选;销假说明,最多 100 字。'] : [])];
18
+ example = resumed ? '--leave-id <leave_id>' : '--class-id <class_id> --student-id <student_id>';
19
+ result = [
20
+ 'status:ready=返回 program_code/fields;selection_required=从 choices[].name/program_code 选择后重读;unavailable 且 choices=[]=无可用流程。',
21
+ 'form_check.status:complete=检查未发现问题;needs_input=需补充或修正;blocked=规则异常或能力不支持。',
22
+ 'form_check.visible_fields:当前显示的字段。form_check.issues:全部当前问题的 code/message 和位置;子表用 parent_field_key/row_index(从 0 开始),专用参数用 parameter。',
23
+ resumed ? 'original_leave:原请假、学生班级、有效时段与状态;field_mappings 为系统绑定字段。'
24
+ : 'class_id/class_name、student_id/student_name:代办对象;student_field_key 为系统绑定的只读学生字段。',
25
+ ].join('\n ');
26
+ extra = `字段说明:\n ` + [
27
+ 'field_key/type/label/description:字段键、类型及学校说明。按 visible_fields 填写 editable=true、computed=false 的字段;required 在显示时生效。',
28
+ 'value/default_value/display_value:当前值、默认值、展示值;null 为未知或未提供。显式答案覆盖默认值。',
29
+ 'options[]:提交 option_id,展示 label;code 为条件值,default_selected 为默认选择。联动结果直接读 form_check,无需自行计算。',
30
+ `candidate_users/candidate_departments/candidate_organizations:可选身份,null=不可用,[]=空集合。person_search_supported=true 才可用 ${prefix}form-people-search。`,
31
+ 'validation_rules:必填、数字、手机、证件、unique 等规则;unique 由服务端查重。number_range.minimum/maximum 是精确十进制字符串,null=该端无界,inclusive=true=包含端点。',
32
+ 'format_rules:decimal_places/digits/rounding=half_up,由服务端格式化,不自行舍入。规则列表 null=未知,[]=未列出;提交仍会校验。',
33
+ 'attachment_upload_supported/file_type/attachment_limits:上传能力、类别和限制。upload_max_bytes 为单文件上限;图片 max_files=10,min_size_mib/max_size_mib 为十进制 MiB 边界,含端点。',
34
+ `groups/row_template:已有行和新行定义;子表输入格式见 ${prefix}submit --help,不支持嵌套子表写入。`,
35
+ ].join('\n ') + '\n\n';
36
+ limits = '不保存、上传或提交。CLI 检查联动、必填及基本格式;修改控制题后可能出现新问题。计算、精确数值、人员资格和文件内容由服务端最终校验。'
37
+ + (!resumed ? '提供完整答案且无本地文件时,还会执行只读业务检查并合并问题。' : '')
38
+ + 'complete 不代表已提交或已批准。';
39
+ } else if (kind === 'form-people-search') {
40
+ purpose = `填写学生${subject}表单中的人员字段时,按姓名查找可选人员。`;
41
+ parameters = [...context, process, '--field-key 必填;可编辑且允许自由搜索的人员字段。', '--keyword 必填;非空姓名线索。'];
42
+ example = `${resumed ? '--leave-id <leave_id>' : '--class-id <class_id> --student-id <student_id>'} --workflow-code <program_code> --field-key <field_key> --keyword 李`;
43
+ result = 'candidates:人员与部门信息,提交时取 tenant_id/user_id;未知部门为 null。最多 10 人,exhaustive=false,无分页。';
44
+ limits = '仅 person_search_supported=true 时使用;固定范围直接使用表单候选。子表父字段须可编辑。此命令不选择请假学生,提交时仍会校验人员资格。';
45
+ } else {
46
+ purpose = `代学生提交${subject},自动检查表单并上传本地附件。`;
47
+ parameters = [...context, process,
48
+ ...(resumed ? ['--resumption-time 必填;实际返校时间,YYYY-MM-DD HH:mm:ss,北京时间;不得晚于原有效假期结束。', '--reason 可选;销假说明,最多 100 字,学校要求时必填。'] : []),
49
+ '--field-values 可选;JSON 数组或 @UTF-8 文件,仍须满足学校必填项。'];
50
+ example = `${resumed ? '--leave-id <leave_id>' : '--class-id <class_id> --student-id <student_id>'} --workflow-code <program_code>${resumed ? ' --resumption-time "2026-10-15 09:00:00"' : ''} --field-values "@表单.json"`;
51
+ extra = '表单.json 示例(字段键按实际表单替换,并补齐必填项):\n'
52
+ + '[\n {"field_key":"实际事由字段键","value":"' + (resumed ? '已返校' : '身体不适') + '"},\n'
53
+ + ' {"field_key":"实际附件字段键","value":{"local_files":["./证明.pdf"]}}\n]\n\n'
54
+ + `字段输入格式:\n ${fieldInput}\n\n`;
55
+ result = [
56
+ `id:新${subject} ID;` + (resumed ? 'leave_id 为原请假 ID。' : 'class_id/student_id 为代办对象。'),
57
+ 'execution_status:COMPLETED=提交已确认;NOT_EXECUTED=未提交申请;UNCONFIRMED=结果不确定。',
58
+ 'application_status:processing 处理中、completed 流程完成,仅提交确认后返回。next_task_id 为下一任务;null/0 表示未返回单一任务。',
59
+ 'post_processing_status:SUBMITTED=后处理已提交、FAILED=后处理失败、NOT_STARTED=未开始;失败不撤销已确认的提交。message 为实际原因。',
60
+ 'attachment_uploads/attachment_preparations:本次上传/文件准备结果及字段、行位置;attachment_preparation_error 为附件局部异常。',
61
+ ].join('\n ');
62
+ failure = '异常处理:\n ' + [
63
+ 'step=check_form_before_upload:检查未通过,尚未上传或提交;error.form_check 汇总当前问题。',
64
+ ...(!resumed ? ['step=validate_form_before_submit:上传后的业务检查未通过,issue 为原因,回执保留已上传文件。'] : []),
65
+ 'NOT_EXECUTED 只说明申请未提交,附件可能已上传;缺失回执不等于未上传。文件准备的 COMPLETED 只确认该步骤,verified 为另一次回读检查。',
66
+ '附件结果按字段和行识别;提交或文件结果未知时先核实,不自动重提、重传或删除。',
67
+ ].join('\n ') + '\n\n';
68
+ limits = (resumed ? '原请假须可访问、已批准且尚未销假;允许假期结束后补办。学生、假种和原有效时段由服务端绑定。'
69
+ : '服务端复核当前班主任职责、班级和学生;不能覆盖绑定学生。')
70
+ + `签名或现场定位须本人完成。仅检查填写用 ${prefix}form;提交成功不代表审批通过或考勤已更新。`;
71
+ }
72
+ // Field encodings accompany start; form definitions follow the form response they describe.
73
+ const beforeResult = kind === 'form' ? '' : extra, afterResult = kind === 'form' ? extra : '';
74
+ return `${purpose}\n\n用法:yunzhi-cli ${command} ${usage}\n\n参数:\n ${parameters.join('\n ')}\n\n示例:\n yunzhi-cli ${command} ${example}\n\n${beforeResult}返回:\n ${result}\n\n${afterResult}${failure}注意:\n ${limits}\n 所需权限:${kind === 'submit' ? 'attendance-leave:read + attendance-leave:write' : 'attendance-leave:read'}。\n`;
75
+ }
76
+ module.exports = { render };
@@ -0,0 +1,108 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, optional, fields, invalid, Failure } = require('./result.cjs');
3
+ const { teacherFormField, parseFieldValues, validateResumptionTime } = require('./leave.cjs');
4
+ const { leaveRecord: record, validId } = require('./student-leave-query.cjs');
5
+ const gateway = require('./gateway.cjs');
6
+
7
+ async function execute(action, args) {
8
+ const resumption = action === 'resumption-form';
9
+ const payload = options(args, ['program_code', 'field_values', ...(resumption
10
+ ? ['leave_id', 'student_id', 'resumption_time', 'reason'] : ['class_id', 'student_id'])], [], [], true);
11
+ requireArguments(payload, ...(resumption ? ['leave_id'] : ['class_id', 'student_id']));
12
+ if (resumption) {
13
+ if (!validId(payload.leave_id, false)) throw invalid('--leave-id 必须是原请假提交或查询返回的 id。');
14
+ if (payload.resumption_time !== undefined) validateResumptionTime(payload.resumption_time);
15
+ if (payload.reason !== undefined && payload.reason.length > 100) throw invalid('--reason 最多 100 字。');
16
+ }
17
+ parseFieldValues(payload);
18
+ const result = await read(payload, resumption);
19
+ const form = result.value.data;
20
+ // Preserve the existing readonly business check when no file upload is needed.
21
+ // Local paths remain local; file content is checked after upload during submission.
22
+ const localFiles = values => (values || []).some(field => field.value?.local_files !== undefined
23
+ || Array.isArray(field.value) && field.value.some(row => row?.fields && localFiles(row.fields)));
24
+ if (!resumption && payload.field_values !== undefined && form.status === 'ready'
25
+ && form.form_check.status === 'complete' && !localFiles(payload.field_values)) {
26
+ const checked = (await require('./leave.cjs').validateStudentForm({ ...payload, program_code: form.program_code })).value.data;
27
+ if (!checked.valid) {
28
+ form.form_check.status = 'needs_input';
29
+ form.form_check.issues.push({ code: 'server_validation', ...checked.issue });
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+
35
+ async function read(payload, resumption) {
36
+ const request = Object.fromEntries((resumption ? ['leave_id', 'student_id', 'program_code'] : ['class_id', 'student_id', 'program_code'])
37
+ .filter(key => payload[key] !== undefined).map(key => [key, payload[key]]));
38
+ const result = await gateway.execute('attendance-leave:read', 'student-leave/' + (resumption ? 'resumption-form' : 'form'), request,
39
+ (data, path) => project(data, path, request, resumption));
40
+ const data = result.value.data;
41
+ if (data.status === 'ready') {
42
+ const original = data.original_leave;
43
+ const form = resumption ? { ...data, leave_id: original.id, user_name: original.student_name,
44
+ leave_type: original.leave_type, leave_start_time: original.start_time, leave_end_time: original.end_time } : data;
45
+ data.form_check = require('./teacher-leave-form-check.cjs').evaluate(form, payload.field_values, payload);
46
+ for (const issue of data.form_check.issues) if (issue.parameter === 'resumption_time') issue.message = '请补充实际返校时间。';
47
+ }
48
+ return result;
49
+ }
50
+
51
+ function project(data, path, request, resumption) {
52
+ if (!data || !['ready', 'selection_required', 'unavailable'].includes(data.status)) throw new Error('学生表单返回了未知状态。');
53
+ if (data.status !== 'ready') {
54
+ const value = fields(data, { status: required('string'), ...(resumption ? { leave_id: required('string') } : {}),
55
+ choices: required([{ program_code: required('string'), name: required('string') }]) }, path);
56
+ if (request.program_code !== undefined || resumption && value.leave_id !== request.leave_id
57
+ || value.status === 'unavailable' && value.choices.length !== 0
58
+ || value.status === 'selection_required' && (value.choices.length < 2
59
+ || new Set(value.choices.map(item => item.program_code)).size !== value.choices.length)
60
+ || value.choices.some(item => !item.program_code.trim() || !item.name.trim()))
61
+ throw new Error('学生流程选择状态或候选不一致。');
62
+ return value;
63
+ }
64
+ const value = fields(data, {
65
+ status: required('string'), tenant_id: required('string'), teacher_id: required('string'), program_code: required('string'),
66
+ fields: required([teacherFormField]),
67
+ ...(resumption ? { original_leave: required(record), field_mappings: required(Object.fromEntries(
68
+ ['leaveId', 'userName', 'leaveType', 'askForLeaveTime', 'askForBackTime', 'revokeLeaveStartTime', 'revokeLeaveReason']
69
+ .map(key => [key, optional(required('string'))]))) }
70
+ : Object.fromEntries(['class_id', 'class_name', 'student_id', 'student_name', 'student_field_key'].map(key => [key, required('string')]))),
71
+ }, path);
72
+ if (!value.program_code.trim() || request.program_code !== undefined && value.program_code !== request.program_code)
73
+ throw new Error('学生表单与指定流程不一致。');
74
+ if (resumption) {
75
+ const keys = new Set(value.fields.map(field => field.field_key));
76
+ if (!Object.values(value.field_mappings).every(key => keys.has(key))) throw new Error('销假绑定字段不在当前可见表单中。');
77
+ const original = value.original_leave;
78
+ if (original.id !== request.leave_id || request.student_id !== undefined && original.student_id !== request.student_id || !validId(original.id, false) || !original.workflow_code?.trim()
79
+ || original.start_time == null || original.end_time == null || original.start_time >= original.end_time
80
+ || original.status !== '1' || original.resumption_status !== '0') throw new Error('学生销假的原请假或有效假期不一致。');
81
+ } else {
82
+ const subjects = value.fields.filter(field => field.field_key === value.student_field_key);
83
+ const subject = subjects[0], selected = subject?.value;
84
+ if (value.class_id !== request.class_id || value.student_id !== request.student_id || subjects.length !== 1
85
+ || subject.editable !== false || !['person', 'people'].includes(subject.type)
86
+ || !Array.isArray(selected) || selected.length !== 1 || selected[0]?.tenant_id !== value.tenant_id
87
+ || selected[0]?.user_id !== value.student_id) throw new Error('学生表单的班级、学生或只读绑定不一致。');
88
+ }
89
+ return value;
90
+ }
91
+
92
+ async function checkBeforeSubmit(payload, resumption) {
93
+ try {
94
+ const form = (await read(payload, resumption)).value.data;
95
+ if (form.status !== 'ready' || form.form_check.status !== 'complete')
96
+ throw new Failure(2, 'validation', 'form_input_required', '表单还有需要处理的信息,尚未上传附件或提交申请。',
97
+ { form_check: form.form_check, hint: '合并处理 form_check.issues;信息齐全后直接提交,无需再单独检查。' });
98
+ } catch (error) {
99
+ const details = error.result?.value?.error;
100
+ if (details) {
101
+ details.execution_status = 'NOT_EXECUTED'; details.step = 'check_form_before_upload';
102
+ if (['auth_required', 'missing_scope'].includes(details.subtype)) details.required_scopes = ['attendance-leave:read', 'attendance-leave:write'];
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ module.exports = { execute, read, checkBeforeSubmit };
@@ -0,0 +1,24 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+ async function execute(action, args) {
5
+ const payload = options(args, ['id'], [], [], true);
6
+ requireArguments(payload, 'id');
7
+ if (!require('./student-leave-query.cjs').validId(payload.id, false, true))
8
+ throw invalid('使用带申请关联的学生请假提交或查询返回的 id。');
9
+ const urge = action === 'remind';
10
+ const result = await gateway.execute('attendance-leave:write', 'student-leave/' + action, payload, {
11
+ id: required('string'), event: required('string'),
12
+ ...(urge ? { notified_count: required('integer') } : { withdrawn: required('boolean') }),
13
+ });
14
+ const data = result.value.data;
15
+ if (data.id !== payload.id || data.event !== (urge ? 'leave_urged' : 'leave_withdrawn')
16
+ || (urge ? data.notified_count <= 0 : data.withdrawn !== true)) {
17
+ const failure = new Failure(1, 'api', 'invalid_response', '学生请假操作回执无效,请查询目标申请,不要自动重试。',
18
+ { details: { id: payload.id, action } });
19
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
20
+ throw failure;
21
+ }
22
+ return result;
23
+ }
24
+ module.exports = { execute };
@@ -0,0 +1,75 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, nullable, optional, fields, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+ function validId(id, resumption, applicationOnly = false) {
5
+ const prefix = resumption ? 'student_resumption_' : 'student_leave_';
6
+ return typeof id === 'string' && new RegExp('^' + prefix + (applicationOnly ? 'a' : '[ar]') + '_[1-9][0-9]{0,18}$').test(id)
7
+ && BigInt(id.slice(prefix.length + 2)) <= 9223372036854775807n;
8
+ }
9
+ const times = { start_time: nullable('string'), end_time: nullable('string'), created_at: nullable('string'), updated_at: nullable('string') };
10
+ const step = { task_id: nullable('string'), node_name: nullable('string'), node_type: nullable('integer'), handler_name: nullable('string'),
11
+ handler_time: nullable('string'), status: required('integer'), status_description: nullable('string'), opinion: nullable('string') };
12
+ const resumptionRecord = { id: required('string'), status: nullable('string'), status_name: nullable('string'), reason: nullable('string'), ...times,
13
+ workflow_status: optional(nullable('integer')), progress: optional(nullable([step])) };
14
+ const leaveRecord = { id: required('string'), student_id: required('string'), student_name: required('string'),
15
+ class_id: required('string'), class_name: nullable('string'), workflow_code: nullable('string'), ...times,
16
+ leave_type: nullable('string'), reason: nullable('string'), status: nullable('string'), status_name: nullable('string'),
17
+ resumption_status: nullable('string'), resumption_status_name: nullable('string'), resumption_source: nullable('integer'),
18
+ workflow_status: optional(nullable('integer')), progress: optional(nullable([step])), resumptions: optional(required([resumptionRecord])) };
19
+ function item(resumption, detail) {
20
+ return (source, at) => {
21
+ const data = fields(source, { id: required('string'), owned: required('boolean'), title: nullable('string'),
22
+ workflow_status: nullable('integer'), workflow_status_name: nullable('string'), created_at: nullable('string'),
23
+ records: nullable([resumption ? { ...resumptionRecord, leave_id: required('string'), original_leave: required(leaveRecord) } : leaveRecord]),
24
+ ...(detail ? { fields: nullable([require('./leave.cjs').formField]), progress: nullable([step]),
25
+ progress_unavailable_reason: optional(nullable('string')),
26
+ attachments: nullable([require('./teacher-leave-files.cjs').attachment]) } : {}) }, at);
27
+ if (!validId(data.id, resumption) || (data.records || []).some(row => !validId(row.id, resumption)
28
+ || resumption && (!validId(row.leave_id, false) || row.original_leave.id !== row.leave_id)
29
+ || !resumption && row.resumptions?.some(r => !validId(r.id, true)))) throw new Error('学生请假身份或关联无效。');
30
+ if ((detail || !data.owned) && data.records === null || !detail && data.owned && data.records !== null) throw new Error('学生业务关联加载状态不一致。');
31
+ if (detail && (data.owned ? data.fields === null || (data.progress === null ? !data.progress_unavailable_reason?.trim() : data.progress_unavailable_reason != null) || data.attachments === null
32
+ : data.fields !== null || data.progress !== null || data.progress_unavailable_reason != null || data.attachments !== null)) throw new Error('学生请假字段与可读范围不一致。');
33
+ if (detail) require('./teacher-leave-files.cjs').displayAttachments(data);
34
+ return data;
35
+ };
36
+ }
37
+ async function execute(action, args) {
38
+ const resumption = action.startsWith('resumption-'), detail = action.endsWith('show');
39
+ const payload = options(args, detail ? ['id'] : ['scope', 'class_id', 'student_id', 'start_date', 'end_date', 'submitted_start_date', 'submitted_end_date', 'status', 'page', 'page_size'], [], [], true);
40
+ requireArguments(payload, detail ? 'id' : 'scope');
41
+ if (detail && !validId(payload.id, resumption)) throw invalid('--id 使用对应请假或销假提交/查询返回的 id。');
42
+ if (!detail) {
43
+ if (!['mine', 'managed'].includes(payload.scope)) throw invalid('--scope 必须为 mine(本人代办)或 managed(职责内学生)。');
44
+ if (payload.scope !== 'mine' && ['submitted_start_date', 'submitted_end_date'].some(k => payload[k] !== undefined))
45
+ throw invalid('提交日期筛选仅用于 mine 本人代办范围。');
46
+ for (const [key, fallback, max] of [['page', 1, 2147483647], ['page_size', 20, 50]]) {
47
+ const value = payload[key] ?? fallback;
48
+ if (!/^\d+$/.test(String(value)) || +value < 1 || +value > max) throw invalid('--' + key.replaceAll('_', '-') + ' 超出范围。');
49
+ payload[key] = +value;
50
+ }
51
+ if (payload.status !== undefined) {
52
+ const enums = require('./attendance-leave-enums.cjs');
53
+ payload.status = enums.encode(payload.scope === 'mine' ? enums.workflowStatus : enums.studentBusinessStatus, payload.status, '--status');
54
+ }
55
+ const date = value => /^\d{4}-\d{2}-\d{2}$/.test(value) && +value.slice(0,4) > 0 && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0,10) === value;
56
+ for (const [start, end, label] of [['start_date', 'end_date', '业务时段'], ['submitted_start_date', 'submitted_end_date', '提交']]) {
57
+ if ((payload[start] === undefined) !== (payload[end] === undefined) || payload[start] !== undefined
58
+ && (!date(payload[start]) || !date(payload[end]) || payload[start] > payload[end]))
59
+ throw invalid(label + '起止日期须成对提供有效 YYYY-MM-DD,开始不晚于结束。');
60
+ }
61
+ }
62
+ const identity = { tenant_id: required('string'), teacher_id: required('string') };
63
+ const shape = detail ? (value, at) => ({ ...fields(value, identity, at), ...item(resumption, true)(value, at) }) : {
64
+ ...identity, scope: required('string'), items: required([item(resumption, false)]), page: required('integer'),
65
+ page_size: required('integer'), total: required('integer'), total_pages: required('integer') };
66
+ const result = await gateway.execute('attendance-leave:read', 'student-leave/' + action, payload, shape);
67
+ const data = result.value.data;
68
+ if (detail ? data.id !== payload.id : data.scope !== payload.scope || data.page !== payload.page || data.page_size !== payload.page_size
69
+ || data.total < 0 || data.total_pages !== Math.ceil(data.total / data.page_size)
70
+ || data.items.length !== Math.min(data.page_size, Math.max(0, data.total - (data.page - 1) * data.page_size))
71
+ || new Set(data.items.map(x=>x.id)).size !== data.items.length || data.items.some(x=>x.owned !== (payload.scope === 'mine')))
72
+ throw new Failure(1, 'api', 'invalid_response', '学生请假返回的身份、范围或分页不一致。');
73
+ return result;
74
+ }
75
+ module.exports = { execute, validId, leaveRecord };
@@ -0,0 +1,2 @@
1
+ // Student resumption preparation shares the application form/check contract.
2
+ module.exports = { execute: (action, args) => require('./student-leave-form.cjs').execute(action, args) };
@@ -0,0 +1,34 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, nullable, Failure } = require('./result.cjs');
3
+ const { parseFieldValues } = require('./leave.cjs');
4
+ const gateway = require('./leave-start-receipt.cjs');
5
+
6
+ async function execute(action, args) {
7
+ const payload = options(args, ['class_id', 'student_id', 'program_code', 'field_values'], [], [], true);
8
+ requireArguments(payload, 'class_id', 'student_id', 'program_code');
9
+ parseFieldValues(payload);
10
+ await require('./student-leave-form.cjs').checkBeforeSubmit(payload, false);
11
+ return require('./teacher-leave-files.cjs').submit(action, payload, async () => {
12
+ try {
13
+ const checked = (await require('./leave.cjs').validateStudentForm(payload)).value.data;
14
+ if (!checked.valid) throw new Failure(2, 'validation', 'form_validation_failed', checked.issue.message, { issue: checked.issue });
15
+ } catch (error) {
16
+ const details = error.result?.value?.error;
17
+ if (details) { details.execution_status = 'NOT_EXECUTED'; details.step = 'validate_form_before_submit'; }
18
+ throw error;
19
+ }
20
+ const result = await gateway.execute('attendance-leave:write', 'student-leave/' + action, payload, {
21
+ class_id: required('string'), student_id: required('string'), program_code: required('string'), request_id: required('string'),
22
+ execution_status: required('string'),
23
+ id: nullable('string'), application_status: nullable('integer'), message: required('string'),
24
+ post_processing_status: required('string'), next_task_id: nullable('string'),
25
+ }, '申请');
26
+ if (['class_id', 'student_id', 'program_code'].some(key => result.value.data[key] !== payload[key])) {
27
+ const failure = new Failure(1, 'api', 'invalid_response', `学生请假提交回执与指定学生、班级或流程不一致,结果待核实,不要自动重试。`);
28
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
29
+ throw failure;
30
+ }
31
+ return result;
32
+ }, 'student-leave');
33
+ }
34
+ module.exports = { execute };
@@ -0,0 +1,51 @@
1
+ const { options } = require('./arguments.cjs');
2
+ const { required, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+
5
+ const classroom = { class_id: required('string'), class_name: required('string') };
6
+ const identity = { tenant_id: required('string'), teacher_id: required('string') };
7
+ async function execute(action, args) {
8
+ if (['remind', 'withdraw'].includes(action)) return require('./student-leave-operations.cjs').execute(action, args);
9
+ if (['resumption-remind', 'resumption-withdraw'].includes(action)) return require('./leave.cjs').executeResumptionOperation(action, args, 'student-leave');
10
+ if (['resumption-submit'].includes(action)) return require('./leave.cjs').executeStart(action, args, 'student-leave');
11
+ if (['resumption-form'].includes(action)) return require('./student-leave-resumption.cjs').execute(action, args);
12
+ if (['approvals', 'approval', 'approve', 'reject', 'resumption-approvals', 'resumption-approval', 'resumption-approve', 'resumption-reject'].includes(action)) return require('./leave.cjs').executeApproval(action, args, 'student-leave');
13
+ if (['submit'].includes(action)) return require('./student-leave-start.cjs').execute(action, args);
14
+ if (['form'].includes(action)) return require('./student-leave-form.cjs').execute(action, args);
15
+ if (['list', 'show', 'resumption-list', 'resumption-show'].includes(action)) return require('./student-leave-query.cjs').execute(action, args);
16
+ const payload = options(args, action === 'classes' ? [] : ['class_id', 'name', 'page', 'page_size'], [], [], true);
17
+ if (action === 'students') {
18
+ if (payload.name != null && payload.name.length > 10) throw invalid('--name 为1到10字的姓名片段。');
19
+ for (const [key, fallback, max] of [['page', 1, 2147483647], ['page_size', 20, 50]]) {
20
+ const value = payload[key] ?? fallback;
21
+ if (!/^[0-9]+$/.test(String(value)) || +value < 1 || +value > max)
22
+ throw invalid('--' + key.replaceAll('_', '-') + ' 必须为1到' + max + '的整数。');
23
+ payload[key] = +value;
24
+ }
25
+ }
26
+ const result = await gateway.execute('attendance-leave:read', 'student-leave/' + action, payload,
27
+ action === 'classes' ? { ...identity, items: required([classroom]) } : {
28
+ ...identity, scope_classes: required([classroom]),
29
+ items: required([{ student_id: required('string'), student_name: required('string'), classes: required([classroom]) }]),
30
+ page: required('integer'), page_size: required('integer'), total: required('integer'), total_pages: required('integer'),
31
+ });
32
+ const data = result.value.data;
33
+ const unique = (rows, key) => new Set(rows.map(row => row[key])).size === rows.length;
34
+ let valid = action === 'classes' ? unique(data.items, 'class_id') :
35
+ data.page === payload.page && data.page_size === payload.page_size && data.total >= 0
36
+ && data.total_pages === Math.ceil(data.total / data.page_size)
37
+ && data.items.length === Math.min(data.page_size, Math.max(0, data.total - (data.page - 1) * data.page_size))
38
+ && unique(data.scope_classes, 'class_id') && unique(data.items, 'student_id')
39
+ && (data.scope_classes.length > 0 || data.total === 0)
40
+ && (payload.class_id == null || (data.scope_classes.length === 1 && data.scope_classes[0].class_id === payload.class_id))
41
+ && data.items.every(row => (payload.name == null || row.student_name.includes(payload.name)) && row.classes.length > 0
42
+ && unique(row.classes, 'class_id') && row.classes.every(entry => data.scope_classes.some(scope =>
43
+ scope.class_id === entry.class_id && scope.class_name === entry.class_name)));
44
+ if (!valid) {
45
+ const failure = new Failure(1, 'api', 'invalid_response', '学生目录返回的身份、职责班级、筛选或分页信息不一致。');
46
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
47
+ throw failure;
48
+ }
49
+ return result;
50
+ }
51
+ module.exports = { execute };
@@ -0,0 +1,57 @@
1
+ // Help only; query and operation contracts stay in their execution modules.
2
+ function render(command, usage) {
3
+ const [space, action] = command.split(' ');
4
+ if (space !== 'student-leave' || !['list', 'show', 'remind', 'withdraw', 'resumption-list', 'resumption-show', 'resumption-remind', 'resumption-withdraw'].includes(action)) return null;
5
+ const resumption = action.startsWith('resumption-'), list = action.endsWith('list'), show = action.endsWith('show'), urge = action.endsWith('remind');
6
+ const subject = resumption ? '销假' : '请假', prefix = resumption ? 'resumption-' : '';
7
+ const purpose = list ? `查询职责内学生${subject},或本人代办的${subject}申请。`
8
+ : show ? `查看学生${subject}详情、进度和可读附件。`
9
+ : `${urge ? '催办' : '撤回'}本人代学生提交、仍在处理的${subject}申请。`;
10
+ const parameters = list ? [
11
+ '--scope 必填;mine=本人代办申请(含未同步申请);managed=职责内已同步记录。',
12
+ '--student-id / --class-id 可选;两个范围均支持,同时填写须匹配同一名学生及其班级。',
13
+ resumption ? '--start-date / --end-date 可选;销假时段与查询日期重叠。销假时段是实际返校至原假期结束,不是仅返校当天。'
14
+ : '--start-date / --end-date 可选;请假时段与查询日期重叠。mine 查表单时段,managed 查已同步的有效假期。',
15
+ '--submitted-start-date / --submitted-end-date 可选,仅 mine;最近一次提交日期,重提会更新,未提交草稿不匹配。',
16
+ '两组日期各自成对填写 YYYY-MM-DD,开始不晚于结束;按北京时间自然日,包含首尾日。全部条件取交集。',
17
+ '--status 可选,按 scope 使用以下业务状态:',
18
+ ' mine:withdrawn 撤回、returned 已退回、draft 草稿、processing 处理中、completed 已完成。',
19
+ ' managed:not-started 未轮到审核、handled-by-peer 同级人已处理、pending 待审核、approved 已同意、rejected 不同意、withdrawn 被撤销。',
20
+ '--page 可选,默认 1,范围 1..2147483647。', '--page-size 可选,默认 20,范围 1..50。',
21
+ ] : [`--id 必填;${show ? '' : '本人代办的'}学生${subject} ID,来自 ${prefix}submit、${prefix}list 或 ${prefix}show。`];
22
+ const result = list ? [
23
+ 'scope、items、page、page_size、total、total_pages;先筛选再分页,total 按申请或独立历史记录计数,一份申请多人不重复计数。',
24
+ 'items[]:id、owned、title、workflow_status/workflow_status_name、created_at。owned 表示按本人代办读取;没有对应申请信息时相关字段为 null。',
25
+ 'items[].records:managed 返回符合条件的可见学生记录;mine 为 null(未加载),需看明细时查详情。',
26
+ ] : show ? [
27
+ 'id、owned、title、workflow_status/workflow_status_name、created_at、records、fields、progress、attachments。',
28
+ 'records:可见学生、班级、业务时段、状态和进度;一份申请可含多人。[] 表示没有可见业务关联,不代表没有提交。',
29
+ resumption ? 'records[].id 标识本次销假;records[].leave_id 和 records[].original_leave.id 指向原请假。'
30
+ : 'records[].id 标识本次请假;records[].resumptions 为关联销假。',
31
+ 'progress 节点 status 使用 pending/transferred/completed/returned/withdrawn,node_type 使用 start/normal/end。本人代办详情返回 fields、progress、attachments;其他申请这三个顶层字段为 null,只返回职责允许的业务记录与进度。',
32
+ 'attachments:文件名、字段位置、download_url、expires_at。链接过期重查详情;历史 URL 图片保留在 fields 中。',
33
+ ] : urge ? [`id、event=${resumption ? 'resumption' : 'leave'}_urged、notified_count(实际通知人数)。`]
34
+ : [`id、event=${resumption ? 'resumption' : 'leave'}_withdrawn、withdrawn=true。`];
35
+ if (list || show) result.push('resumption_status=resumed/not-resumed 表示已销假/未销假;resumption_source=workflow/automatic-clock 表示工作流/打卡自动销假。未识别的状态返回 unknown,并在 status_code 或 workflow_status_code 保留原码;null 保持未知或未加载。');
36
+ const limits = list ? [
37
+ 'mine 的班级取申请保存的班级;销假取绑定原请假的班级,不使用学生当前班级。',
38
+ '筛选所需映射、可读字段或关联缺失时明确报错。managed 不包含尚未同步的记录。',
39
+ 'workflow_status 使用 mine 的流程状态名称,records[].status 使用 managed 的业务状态名称;列表和详情一致,未知或未加载仍为 null。流程“已完成”不一定是同意,也不代表考勤已经更新。',
40
+ ] : show ? [
41
+ 'workflow_status 为流程状态名称,records[].status 为业务状态名称,与 list --help 的状态一致;流程状态、业务状态和考勤结果分别判断。' + (resumption ? '历史或自动销假可能没有流程进度。' : ''),
42
+ ] : [
43
+ '仅本人提交且仍在处理的申请;不支持家长或其他老师的申请,以及无申请关联的历史记录。',
44
+ urge ? '催办间隔 30 分钟;成功只说明已通知待处理人。' : resumption ? '撤回本次销假申请,原请假不会因此撤回。' : '撤回终止本次请假申请;结束已批准假期应办理销假。',
45
+ `结果不确定时查 ${prefix}show 核实,不自动重试。`,
46
+ ];
47
+ const examples = list ? [
48
+ '# 本人代办:按学生和提交日期查询',
49
+ `yunzhi-cli ${command} --scope mine --student-id <student_id> --submitted-start-date 2026-10-01 --submitted-end-date 2026-10-07`,
50
+ '# 本人代办:按班级和业务时段查询',
51
+ `yunzhi-cli ${command} --scope mine --class-id <class_id> --start-date 2026-10-08 --end-date 2026-10-10`,
52
+ '# 职责内:按班级和业务时段查询',
53
+ `yunzhi-cli ${command} --scope managed --class-id <class_id> --start-date 2026-10-08 --end-date 2026-10-10`,
54
+ ] : [`yunzhi-cli ${command} --id <id>`];
55
+ return `${purpose}\n\n用法:yunzhi-cli ${command} ${usage}\n\n参数:\n ${parameters.join('\n ')}\n\n示例:\n ${examples.join('\n ')}\n\n返回:\n ${result.join('\n ')}\n\n注意:\n ${limits.join('\n ')}\n 所需权限:attendance-leave:${list || show ? 'read' : 'write'}。\n`;
56
+ }
57
+ module.exports = { render };
@@ -0,0 +1,64 @@
1
+ const { replenishmentId } = require('./attendance-leave-ids.cjs');
2
+ const { options, required: requireArguments } = require('./arguments.cjs');
3
+ const { required, nullable, invalid, Failure } = require('./result.cjs');
4
+ const gateway = require('./gateway.cjs');
5
+ const identity = { tenant_id: required('string'), operator_id: required('string'), subject_id: required('string'), class_id: required('string'), date: required('string') };
6
+ function validWindow(value) {
7
+ return typeof value === 'string' && /^[0-9]{1,2}:[0-9]{2}-[0-9]{1,2}:[0-9]{2}$/.test(value)
8
+ && value.split('-').every(time => { const [h, m] = time.split(':').map(Number); return h < 24 && m < 60 || h === 24 && m === 0; });
9
+ }
10
+ function parse(args) {
11
+ const keys = ['target_user_id', 'class_id', 'date', 'time_bucket', 'direction'];
12
+ const payload = options(args, keys, [], ['time_bucket'], true); requireArguments(payload, 'target_user_id', 'class_id', 'date');
13
+ if ((payload.time_bucket !== undefined) !== (payload.direction !== undefined))
14
+ throw invalid('--time-window 和 --direction 必须同时提供才能提交;两项都省略时只查询可补选项。');
15
+ const write = payload.time_bucket !== undefined;
16
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(payload.date) || !Number.isFinite(Date.parse(payload.date)) || new Date(payload.date).toISOString().slice(0, 10) !== payload.date)
17
+ throw invalid('--date 必须是有效的 YYYY-MM-DD 日期;服务端只允许业务当天补签到。');
18
+ if (write) {
19
+ if (!validWindow(payload.time_bucket)) throw invalid('--time-window 必须使用可补选项中的时段,不能指定任意打卡时间。');
20
+ const enums = require('./attendance-leave-enums.cjs');
21
+ payload.direction = enums.encode(enums.direction, payload.direction, '--direction');
22
+ }
23
+ return payload;
24
+ }
25
+ function invalidReply(result) {
26
+ const failure = new Failure(1, 'api', 'invalid_response', '校门补签到返回的学生、班级、时段、规则或执行阶段缺失或不一致。');
27
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id; return failure;
28
+ }
29
+ function preserve(failure, payload, write) {
30
+ if (failure instanceof Failure) {
31
+ const error = failure.result.value.error;
32
+ const unknown = write && (error.type === 'network' && error.subtype !== 'connection_failed' || error.type === 'api' && error.subtype === 'invalid_response');
33
+ error.details = { ...error.details, ...payload, ...(unknown ? { execution_status: 'UNCONFIRMED', notification_status: 'UNCONFIRMED' } : {}) };
34
+ if (unknown) error.message += '补签到及通知结果待核实,请保留目标和请求标识,按需要查询当天打卡,不要自动重试。';
35
+ }
36
+ throw failure;
37
+ }
38
+ async function execute(args) {
39
+ const payload = parse(args);
40
+ const write = payload.time_bucket !== undefined;
41
+ try {
42
+ const result = await gateway.execute('attendance-leave:' + (write ? 'write' : 'read'), 'student-attendance/' + (write ? 'replenish' : 'replenishment-context'), payload, {
43
+ ...identity,
44
+ ...(write ? { id: nullable('string'), time_bucket: required('string'), direction: required('string'), request_id: required('string'), execution_status: required('string'), notification_status: nullable('string'), recorded_time: nullable('string'), handled_at: nullable('integer'), message: required('string') }
45
+ : { subject_name: required('string'), class_name: required('string'), rule_id: required('string'), rule_name: nullable('string'), options: required([{ time_bucket: required('string'), direction: required('string') }]) }),
46
+ });
47
+ const d = result.value.data;
48
+ if (d.subject_id !== payload.target_user_id || d.class_id !== payload.class_id || d.date !== payload.date) throw invalidReply(result);
49
+ if (write) {
50
+ if (d.execution_status === 'COMPLETED' ? !replenishmentId(d.id, '1', payload.date) : d.id != null) throw invalidReply(result);
51
+ if (d.time_bucket !== payload.time_bucket || d.direction !== payload.direction || result.value.request_id && result.value.request_id !== d.request_id
52
+ || d.handled_at != null && d.handled_at < 0
53
+ || (d.execution_status === 'COMPLETED' ? d.recorded_time !== payload.time_bucket.split('-')[1] + ':00' || d.handled_at == null
54
+ : d.execution_status !== 'UNCONFIRMED' || !['NOT_SUBMITTED', 'UNCONFIRMED'].includes(d.notification_status) || d.recorded_time != null)) throw invalidReply(result);
55
+ if (d.execution_status === 'COMPLETED' && !['SUBMITTED', 'UNCONFIRMED'].includes(d.notification_status)) {
56
+ d.notification_status = 'UNCONFIRMED';
57
+ d.message = '补签到记录已保存,后续家长通知状态无法核实;不要重复补签到。';
58
+ }
59
+ } else if (!/^[1-9][0-9]{0,9}$/.test(d.rule_id) || Number(d.rule_id) > 2147483647 || !d.options.length || d.options.some(o => !validWindow(o.time_bucket) || !['1', '2'].includes(o.direction))) throw invalidReply(result);
60
+ if (!write) d.message = '已查询可补签到的时段和方向,本次未提交补签到;需要办理时同时提供 --time-window 和 --direction。';
61
+ return result;
62
+ } catch (failure) { preserve(failure, payload, write); }
63
+ }
64
+ module.exports = { execute };
@@ -0,0 +1,39 @@
1
+ const { options, required: requireArguments } = require('./arguments.cjs');
2
+ const { required, nullable, invalid, Failure } = require('./result.cjs');
3
+ const gateway = require('./gateway.cjs');
4
+ const { reportSchema, validReportContext } = require('./saved-report-summary.cjs');
5
+ const rowSchema = Object.fromEntries(['record_id', 'user_name', 'date', 'status', 'time_bucket', 'at_time', 'direction', 'site', 'remark',
6
+ 'replenish_at_time', 'replenish_user_role', 'replenish_user_name', 'pic_url', 'video_url'].map(name => [name, nullable('string')]));
7
+ rowSchema.user_id = required('string'); rowSchema.leave_ids = nullable(['string']);
8
+ function shape(label = false) {
9
+ return {
10
+ tenant_id: required('string'), user_id: required('string'), ...(label ? {} : { class_id: required('string'), class_name: nullable('string') }),
11
+ report: required({ ...reportSchema }),
12
+ groups: required([{ type: required('string'), count: required('integer'), rows: required([rowSchema]) }]),
13
+ };
14
+ }
15
+ function validateQuery(payload, label = false) {
16
+ requireArguments(payload, ...(label ? ['report_id'] : ['report_id', 'class_id']));
17
+ if (!/^[1-9][0-9]{0,9}$/.test(payload.report_id) || Number(payload.report_id) > 2147483647 || !label && !payload.class_id.trim()) throw invalid(label ? '必须提供有效的标签报告 --report-id。' : '必须提供有效的 --report-id 和明确的 --class-id。');
18
+ }
19
+ function validate(result, payload, label = false) {
20
+ const d = result.value.data;
21
+ if ((label ? !d.report.scope_id?.trim() : d.class_id !== payload.class_id) || !validReportContext(d.report, payload.report_id, label ? ['6'] : ['1', '2', '3'])
22
+ || !d.groups.length
23
+ || new Set(d.groups.map(g => g.type)).size !== d.groups.length
24
+ || d.groups.some(g => !g.type.trim() || g.count < 0 || g.rows.some(r => !r.user_id.trim()
25
+ || r.leave_ids != null && r.leave_ids.some(id => !require('./student-leave-query.cjs').validId(id, false))))) {
26
+ const failure = new Failure(1, 'api', 'invalid_response', '校门报告明细返回的报告关联、分类或人员信息缺失或不一致。');
27
+ if (result.value.request_id) failure.result.value.request_id = result.value.request_id; throw failure;
28
+ }
29
+ return result;
30
+ }
31
+ function addContext(failure, payload, label = false) {
32
+ if (failure instanceof Failure) failure.result.value.error.details = { ...failure.result.value.error.details, report_id: payload.report_id, ...(label ? {} : { class_id: payload.class_id }) };
33
+ }
34
+ async function execute(args, label = false) {
35
+ const payload = options(args, label ? ['report_id'] : ['report_id', 'class_id'], [], [], true); validateQuery(payload, label);
36
+ try { return validate(await gateway.execute('attendance-leave:read', 'student-attendance/report-' + (label ? 'label' : 'class'), payload, shape(label)), payload, label); }
37
+ catch (failure) { addContext(failure, payload, label); throw failure; }
38
+ }
39
+ module.exports = { execute };