@xiaoyuanhao/yunzhi-cli 2.0.0 → 2.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.
- package/lib/arguments.cjs +7 -4
- package/lib/attendance-clock-entry.cjs +31 -0
- package/lib/attendance-configuration-entry.cjs +35 -0
- package/lib/attendance-correction-applications.cjs +64 -0
- package/lib/attendance-correction-approval.cjs +135 -0
- package/lib/attendance-correction-start.cjs +47 -0
- package/lib/attendance-leave-enums.cjs +193 -0
- package/lib/attendance-leave-ids.cjs +15 -0
- package/lib/attendance-leave-list-filters.cjs +21 -0
- package/lib/attendance-leave-route.cjs +20 -0
- package/lib/attendance-operation-result.cjs +97 -0
- package/lib/attendance-report-inbox.cjs +42 -0
- package/lib/attendance-report-remarks.cjs +62 -0
- package/lib/attendance-statistics.cjs +49 -0
- package/lib/attendance.cjs +404 -0
- package/lib/auth.cjs +1 -1
- package/lib/business-schemas.cjs +3 -1
- package/lib/cli.cjs +294 -1
- package/lib/command-guidance.cjs +1162 -1
- package/lib/command-reference.cjs +54 -0
- package/lib/contact-help.cjs +43 -0
- package/lib/contact-mobile.cjs +14 -0
- package/lib/contact.cjs +85 -0
- package/lib/correction-help.cjs +57 -0
- package/lib/dormitory-replenishment.cjs +63 -0
- package/lib/dormitory-report-detail.cjs +76 -0
- package/lib/dormitory-report-id.cjs +8 -0
- package/lib/dormitory-report-summary.cjs +66 -0
- package/lib/dormitory-report-view.cjs +36 -0
- package/lib/dormitory-rooms.cjs +41 -0
- package/lib/dormitory-scope.cjs +18 -0
- package/lib/dormitory-statistics.cjs +30 -0
- package/lib/download.cjs +17 -8
- package/lib/gateway.cjs +9 -4
- package/lib/leave-approval-receipt.cjs +126 -0
- package/lib/leave-attachment-upload.cjs +110 -0
- package/lib/leave-form-users.cjs +48 -0
- package/lib/leave-start-receipt.cjs +64 -0
- package/lib/leave.cjs +457 -0
- package/lib/replenishment-records.cjs +39 -0
- package/lib/result.cjs +1 -0
- package/lib/saved-report-summary.cjs +16 -0
- package/lib/school-contacts.cjs +24 -0
- package/lib/school-help.cjs +53 -0
- package/lib/school-schema.cjs +38 -0
- package/lib/student-application-help.cjs +76 -0
- package/lib/student-leave-form.cjs +108 -0
- package/lib/student-leave-operations.cjs +24 -0
- package/lib/student-leave-query.cjs +74 -0
- package/lib/student-leave-resumption.cjs +2 -0
- package/lib/student-leave-start.cjs +34 -0
- package/lib/student-leave.cjs +51 -0
- package/lib/student-query-help.cjs +57 -0
- package/lib/student-replenishment.cjs +64 -0
- package/lib/student-report-detail.cjs +39 -0
- package/lib/student-report-summary.cjs +30 -0
- package/lib/teacher-approval-help.cjs +132 -0
- package/lib/teacher-leave-files.cjs +112 -0
- package/lib/teacher-leave-form-check.cjs +185 -0
- package/lib/teacher-leave-query.cjs +99 -0
- package/lib/teacher-own-leave-help.cjs +134 -0
- package/lib/workflow-entry.cjs +42 -0
- package/package.json +1 -1
package/lib/leave.cjs
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
const { options, required: requireArguments } = require('./arguments.cjs');
|
|
2
|
+
const { required, nullable, optional, invalid, internal, fields: projectFields } = require('./result.cjs');
|
|
3
|
+
const gateway = require('./gateway.cjs');
|
|
4
|
+
|
|
5
|
+
function formAnswer(value, path, field, configuredDefault = false) {
|
|
6
|
+
const fail = () => { throw new Error(`响应字段 ${path} 与表单类型 ${field.type} 不一致。`); };
|
|
7
|
+
const identityKey = { person: 'user_id', people: 'user_id', department: 'department_id', departments: 'department_id',
|
|
8
|
+
organization: 'tenant_id', organizations: 'tenant_id' }[field.type];
|
|
9
|
+
if (identityKey) {
|
|
10
|
+
if (!Array.isArray(value) || ['person', 'department', 'organization'].includes(field.type) && value.length > 1) return fail();
|
|
11
|
+
return projectFields(value, [{ tenant_id: required('string'), [identityKey]: required('string') }], path);
|
|
12
|
+
}
|
|
13
|
+
if (field.type === 'table' || field.type == null) return fail();
|
|
14
|
+
if (field.type === 'choices') return projectFields(value, [required('string')], path);
|
|
15
|
+
if (configuredDefault) return typeof value === 'string' ? value : fail();
|
|
16
|
+
if (field.type === 'choice') {
|
|
17
|
+
if (typeof value === 'string') return value;
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.other_text !== 'string') return fail();
|
|
19
|
+
return projectFields(value, { option_id: required('string'), other_text: 'string' }, path);
|
|
20
|
+
}
|
|
21
|
+
if (field.type === 'images') {
|
|
22
|
+
if (field.attachment_upload_supported === true && typeof value === 'string') return value;
|
|
23
|
+
if (typeof field.attachment_upload_supported !== 'boolean' || !Array.isArray(value)) return fail();
|
|
24
|
+
return value.map((item, index) => {
|
|
25
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return fail();
|
|
26
|
+
for (const key of ['width', 'height']) if (item[key] != null && (!Number.isFinite(item[key]) || item[key] < 0)) return fail();
|
|
27
|
+
return projectFields(item, { url: required('string'), width: optional('number'), height: optional('number') }, `${path}[${index}]`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (!['text', 'multiline_text', 'number', 'date', 'datetime', 'files', 'signature', 'location', 'link', 'separator'].includes(field.type)) return fail();
|
|
31
|
+
return typeof value === 'string' ? value : fail();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formField(field, path, scope = 'form') {
|
|
35
|
+
const result = projectFields(field, { ...formFieldSchema,
|
|
36
|
+
value: nullable((value, valuePath) => formAnswer(value, valuePath, field)),
|
|
37
|
+
default_value: nullable((value, valuePath) => formAnswer(value, valuePath, field, true)),
|
|
38
|
+
}, path);
|
|
39
|
+
if (result.visibility && (result.visibility.scope !== scope || !result.visibility.requires_visible_controller
|
|
40
|
+
|| !result.visibility.any_of.length)) throw new Error(`响应字段 ${path}.visibility 的作用域或显示规则无效。`);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const formOptions = nullable([{ option_id: required('string'), label: required('string'),
|
|
45
|
+
code: nullable('string'), default_selected: nullable('boolean'), shows_fields: nullable([required('string')]) }]);
|
|
46
|
+
const formConfiguration = nullable([{ code: nullable('string'), label: nullable('string'),
|
|
47
|
+
value: nullable('string'), default_selected: nullable('boolean') }]);
|
|
48
|
+
function validationRule(value, path) {
|
|
49
|
+
const rule = projectFields(value, { type: required('string'), minimum: nullable('string'),
|
|
50
|
+
maximum: nullable('string'), inclusive: nullable('boolean') }, path);
|
|
51
|
+
if (!['required', 'unique', 'phone', 'identity_card', 'number', 'number_range'].includes(rule.type))
|
|
52
|
+
throw new Error(`响应字段 ${path}.type 包含无法解释的校验规则。`);
|
|
53
|
+
if (rule.type === 'number_range') {
|
|
54
|
+
if (rule.inclusive !== true || !Object.hasOwn(value, 'minimum') || !Object.hasOwn(value, 'maximum')
|
|
55
|
+
|| [rule.minimum, rule.maximum].some(bound => bound !== null && !/^-?[0-9]+(\.[0-9]+)?$/.test(bound)))
|
|
56
|
+
throw new Error(`响应字段 ${path} 缺少有效的数值范围及包含边界说明。`);
|
|
57
|
+
} else if (rule.minimum != null || rule.maximum != null || rule.inclusive != null)
|
|
58
|
+
throw new Error(`响应字段 ${path} 在非范围规则中包含数值边界。`);
|
|
59
|
+
return rule;
|
|
60
|
+
}
|
|
61
|
+
function formatRule(value, path) {
|
|
62
|
+
const rule = projectFields(value, { type: required('string'), digits: nullable('integer'), rounding: nullable('string') }, path);
|
|
63
|
+
if (rule.type !== 'decimal_places' || !Object.hasOwn(value, 'digits') || !Object.hasOwn(value, 'rounding')
|
|
64
|
+
|| (rule.digits === null ? rule.rounding !== null : rule.digits < 0 || rule.digits > 2147483647 || rule.rounding !== 'half_up'))
|
|
65
|
+
throw new Error(`响应字段 ${path} 缺少有效的小数位数及格式化说明。`);
|
|
66
|
+
return rule;
|
|
67
|
+
}
|
|
68
|
+
function calculation(value, path) {
|
|
69
|
+
const result = projectFields(value, { evaluated_by: required('string'), parts: required([(part, partPath) => {
|
|
70
|
+
const projected = projectFields(part, { type: required('string'), text: nullable('string'), field_key: nullable('string') }, partPath);
|
|
71
|
+
if (projected.type === 'text' ? !projected.text || projected.field_key !== null
|
|
72
|
+
: projected.type !== 'field_reference' || !projected.field_key?.trim() || projected.text !== null)
|
|
73
|
+
throw new Error(`响应字段 ${partPath} 缺少有效的表达式片段或字段引用。`);
|
|
74
|
+
return projected;
|
|
75
|
+
}]) }, path);
|
|
76
|
+
if (result.evaluated_by !== 'server' || !result.parts.length)
|
|
77
|
+
throw new Error(`响应字段 ${path} 缺少服务端求值说明或表达式。`);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
const formFieldSchema = {
|
|
81
|
+
widget_id: required('string'), field_key: required('string'), type: required('string'),
|
|
82
|
+
editable: required('boolean'), required: required('boolean'), validation_rules: nullable([validationRule]),
|
|
83
|
+
format_rules: nullable([formatRule]),
|
|
84
|
+
calculation: nullable(calculation),
|
|
85
|
+
calculation_links: nullable([{ field_key: required('string'), calculation: required(calculation) }]),
|
|
86
|
+
attachment_upload_supported: nullable('boolean'),
|
|
87
|
+
options: formOptions, configuration: formConfiguration,
|
|
88
|
+
display_value: nullable('string'),
|
|
89
|
+
visibility: nullable({ scope: required('string'), requires_visible_controller: required('boolean'),
|
|
90
|
+
any_of: required([{ field_key: required('string'), option_code: required('string') }]) }),
|
|
91
|
+
candidate_users: nullable([{ tenant_id: required('string'), tenant_name: nullable('string'),
|
|
92
|
+
user_id: required('string'), name: nullable('string'), default_selected: nullable('boolean') }]),
|
|
93
|
+
candidate_departments: nullable([{ tenant_id: required('string'), department_id: required('string'),
|
|
94
|
+
name: nullable('string'), default_selected: nullable('boolean') }]),
|
|
95
|
+
candidate_organizations: nullable([{ tenant_id: required('string'), name: nullable('string'), default_selected: nullable('boolean') }]),
|
|
96
|
+
...Object.fromEntries(['label', 'description',
|
|
97
|
+
'file_type', 'select_id', 'user_assembly_id']
|
|
98
|
+
.map(key => [key, nullable('string')])),
|
|
99
|
+
};
|
|
100
|
+
formFieldSchema.groups = nullable([(group, path) => {
|
|
101
|
+
const value = projectFields(group, { group_code: nullable('string'),
|
|
102
|
+
fields: required([(field, childPath) => formField(field, childPath, 'row')]) }, path);
|
|
103
|
+
return value;
|
|
104
|
+
}]);
|
|
105
|
+
|
|
106
|
+
formFieldSchema.row_template = nullable([(field, path) => formField(field, path, 'row')]);
|
|
107
|
+
|
|
108
|
+
// Application preparation uses this compact view. Saved details and approvals retain their contracts.
|
|
109
|
+
function teacherFormField(field, path, scope = 'form') {
|
|
110
|
+
const schema = { ...formFieldSchema };
|
|
111
|
+
for (const key of ['widget_id', 'select_id', 'user_assembly_id', 'configuration', 'calculation', 'calculation_links']) delete schema[key];
|
|
112
|
+
Object.assign(schema, {
|
|
113
|
+
computed: required('boolean'), person_search_supported: optional(required('boolean')),
|
|
114
|
+
options: nullable([{ option_id: required('string'), label: required('string'), code: nullable('string'), default_selected: nullable('boolean') }]),
|
|
115
|
+
value: nullable((value, at) => formAnswer(value, at, field)),
|
|
116
|
+
default_value: nullable((value, at) => formAnswer(value, at, field, true)),
|
|
117
|
+
attachment_limits: optional({ upload_max_bytes: required('integer'), max_files: optional(required('integer')),
|
|
118
|
+
min_size_mib: optional(required('string')), max_size_mib: optional(required('string')) }),
|
|
119
|
+
groups: nullable([{ group_code: nullable('string'), fields: required([(child, at) => teacherFormField(child, at, 'row')]) }]),
|
|
120
|
+
row_template: nullable([(child, at) => teacherFormField(child, at, 'row')]),
|
|
121
|
+
});
|
|
122
|
+
const result = projectFields(field, schema, path);
|
|
123
|
+
if (result.visibility && (result.visibility.scope !== scope || !result.visibility.requires_visible_controller || !result.visibility.any_of.length))
|
|
124
|
+
throw new Error('表单显示规则的作用域无效。');
|
|
125
|
+
if (result.computed && !['number', 'text'].includes(result.type)) throw new Error('计算字段类型无效。');
|
|
126
|
+
if (['person', 'people'].includes(result.type) && typeof result.person_search_supported !== 'boolean')
|
|
127
|
+
throw new Error('选人字段缺少搜索能力说明。');
|
|
128
|
+
if (['files', 'images'].includes(result.type)) {
|
|
129
|
+
const limits = result.attachment_limits;
|
|
130
|
+
if (!limits || limits.upload_max_bytes !== 209715200 || result.type === 'images' && (limits.max_files !== 10
|
|
131
|
+
|| !/^\d+(\.\d+)?$/.test(limits.min_size_mib || '') || !/^\d+(\.\d+)?$/.test(limits.max_size_mib || '')))
|
|
132
|
+
throw new Error('附件字段缺少有效限制。');
|
|
133
|
+
}
|
|
134
|
+
// Unknown values stay null; absent optional capabilities do not clutter the form.
|
|
135
|
+
for (const [key, value] of Object.entries(result))
|
|
136
|
+
if (value === null && !['value', 'default_value', 'display_value'].includes(key)
|
|
137
|
+
&& !(key === 'candidate_users' && ['person', 'people'].includes(result.type))
|
|
138
|
+
&& !(key === 'candidate_departments' && ['department', 'departments'].includes(result.type))
|
|
139
|
+
&& !(key === 'candidate_organizations' && ['organization', 'organizations'].includes(result.type))) delete result[key];
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function teacherForm(data, path, request, resumption) {
|
|
144
|
+
if (!data || !['ready', 'selection_required', 'unavailable'].includes(data.status)) throw new Error('表单返回了未知状态。');
|
|
145
|
+
if (resumption && data.leave_id !== request.leave_id) throw new Error('销假表单与原请假不一致。');
|
|
146
|
+
if (data.status !== 'ready') {
|
|
147
|
+
const result = projectFields(data, { status: required('string'), ...(resumption ? { leave_id: required('string') } : {}),
|
|
148
|
+
choices: required([{ program_code: required('string'), name: required('string') }]) }, path);
|
|
149
|
+
if (request.program_code !== undefined || result.status === 'unavailable' && result.choices.length !== 0
|
|
150
|
+
|| result.status === 'selection_required' && (result.choices.length < 2 || new Set(result.choices.map(item => item.program_code)).size !== result.choices.length))
|
|
151
|
+
throw new Error('流程选择状态与候选项不一致。');
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
const result = projectFields(data, { status: required('string'), program_code: required('string'), fields: required([teacherFormField]),
|
|
155
|
+
...(resumption ? {
|
|
156
|
+
leave_id: required('string'), tenant_id: required('string'), user_id: required('string'),
|
|
157
|
+
user_name: nullable('string'), leave_type: nullable('string'), leave_start_time: required('string'), leave_end_time: required('string'),
|
|
158
|
+
field_mappings: required(Object.fromEntries(['leaveId', 'userName', 'leaveType', 'askForLeaveTime', 'askForBackTime', 'revokeLeaveStartTime', 'revokeLeaveReason']
|
|
159
|
+
.map(key => [key, optional(required('string'))]))),
|
|
160
|
+
} : {}),
|
|
161
|
+
}, path);
|
|
162
|
+
if (request.program_code !== undefined && result.program_code !== request.program_code) throw new Error('表单流程与指定流程不一致。');
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function readTeacherForm(payload, resumption) {
|
|
167
|
+
const request = { ...(payload.program_code !== undefined ? { program_code: payload.program_code } : {}),
|
|
168
|
+
...(resumption ? { leave_id: payload.leave_id } : {}) };
|
|
169
|
+
const result = await gateway.execute('attendance-leave:read', 'leave/' + (resumption ? 'resumption-form' : 'form'), request,
|
|
170
|
+
(data, path) => teacherForm(data, path, request, resumption));
|
|
171
|
+
if (result.value.data.status === 'ready') result.value.data.form_check = require('./teacher-leave-form-check.cjs')
|
|
172
|
+
.evaluate(result.value.data, payload.field_values, payload);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const approvalTask = {
|
|
177
|
+
task_id: required('string'), application_id: required('string'), title: nullable('string'),
|
|
178
|
+
applicant_id: required('string'), applicant_tenant_id: required('string'), applicant_name: nullable('string'),
|
|
179
|
+
submitted_at: nullable('string'), task_status: required('integer'), handler_id: nullable('string'),
|
|
180
|
+
handler_tenant_id: nullable('string'), handled_at: nullable('string'), notice_id: nullable('string'), notice_read: nullable('boolean'),
|
|
181
|
+
leave_subjects: required([{ leave_id: required('string'), user_id: required('string'), user_name: nullable('string') }]),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
function parseFieldValues(payload) {
|
|
185
|
+
if (payload.field_values !== undefined) {
|
|
186
|
+
let input = payload.field_values;
|
|
187
|
+
if (input.startsWith('@')) {
|
|
188
|
+
const file = input.slice(1);
|
|
189
|
+
if (!file.trim()) throw invalid('--field-values 的 @ 后必须提供本地 JSON 文件路径。');
|
|
190
|
+
try { input = require('node:fs').readFileSync(file, 'utf8').replace(/^\uFEFF/, ''); }
|
|
191
|
+
catch (error) { throw internal('read_field_values', '无法读取表单 JSON 文件,业务请求尚未发送:' + file, error); }
|
|
192
|
+
}
|
|
193
|
+
try { payload.field_values = JSON.parse(input); }
|
|
194
|
+
catch { throw invalid('--field-values 必须是有效的 JSON 数组。'); }
|
|
195
|
+
if (!validFieldValues(payload.field_values) || payload.field_values.length > 100)
|
|
196
|
+
throw invalid('--field-values 最多 100 项,每项只包含非空 field_key 和 value;value 为文本、数字、数组或对象,子表行直接包含 fields 数组。');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validFieldValues(fields) {
|
|
201
|
+
return Array.isArray(fields) && fields.every(field =>
|
|
202
|
+
field && typeof field === 'object' && !Array.isArray(field) && typeof field.field_key === 'string' && field.field_key.trim()
|
|
203
|
+
&& Object.keys(field).length === 2 && Object.hasOwn(field, 'value') && field.value != null
|
|
204
|
+
&& ['string', 'number', 'object'].includes(typeof field.value)
|
|
205
|
+
&& (typeof field.value !== 'number' || Number.isFinite(field.value))
|
|
206
|
+
&& (!Array.isArray(field.value) || field.value.every(row => !row || typeof row !== 'object' || !Object.hasOwn(row, 'fields') || validFieldValues(row.fields))));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function executeResumptionOperation(command, args, domain) {
|
|
210
|
+
const own = domain === 'leave', key = 'id';
|
|
211
|
+
const payload = options(args, [key], [], [], true);
|
|
212
|
+
requireArguments(payload, key);
|
|
213
|
+
if (own ? !require('./teacher-leave-query.cjs').validId(payload.id, 'resumption_')
|
|
214
|
+
: !require('./student-leave-query.cjs').validId(payload[key], true, true))
|
|
215
|
+
throw invalid('使用销假提交或查询返回的 ID。');
|
|
216
|
+
const result = await gateway.execute('attendance-leave:write', domain + '/' + command, payload, {
|
|
217
|
+
[key]: required('string'), event: required('string'),
|
|
218
|
+
...(command === 'resumption-remind' ? { notified_count: required('integer') } : { withdrawn: required('boolean') }),
|
|
219
|
+
});
|
|
220
|
+
if (result.value.data[key] !== payload[key]) {
|
|
221
|
+
const failure = new (require('./result.cjs').Failure)(1, 'api', 'invalid_response', '销假操作回执与指定申请不一致,请查询目标申请,不要自动重试。',
|
|
222
|
+
{ details: { [key]: payload[key], action: command === 'resumption-remind' ? 'remind' : 'withdraw' } });
|
|
223
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
224
|
+
throw failure;
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function validateStudentForm(payload) {
|
|
230
|
+
const domain = 'student-leave';
|
|
231
|
+
const result = await gateway.execute('attendance-leave:read', domain + '/form/check', payload, {
|
|
232
|
+
student_id: required('string'), class_id: required('string'),
|
|
233
|
+
tenant_id: required('string'), user_id: required('string'), program_code: required('string'),
|
|
234
|
+
form_id: required('string'), valid: required('boolean'),
|
|
235
|
+
issue: nullable({ field_key: nullable('string'), parent_field_key: nullable('string'),
|
|
236
|
+
row_index: nullable('integer'), message: required('string') }),
|
|
237
|
+
});
|
|
238
|
+
const data = result.value.data, issue = data.issue;
|
|
239
|
+
if (data.student_id !== payload.student_id || data.class_id !== payload.class_id
|
|
240
|
+
|| !data.tenant_id.trim() || !data.user_id.trim() || data.program_code !== payload.program_code
|
|
241
|
+
|| !/^[1-9][0-9]*$/.test(data.form_id) || BigInt(data.form_id) > 9223372036854775807n
|
|
242
|
+
|| data.valid !== (issue === null)
|
|
243
|
+
|| issue !== null && (!issue.message.trim() || issue.field_key !== null && !issue.field_key.trim()
|
|
244
|
+
|| issue.parent_field_key !== null && !issue.parent_field_key.trim()
|
|
245
|
+
|| (issue.parent_field_key === null) !== (issue.row_index === null)
|
|
246
|
+
|| issue.row_index !== null && issue.row_index < 0)) {
|
|
247
|
+
const failure = new (require('./result.cjs').Failure)(1, 'api', 'invalid_response',
|
|
248
|
+
'预检回执与流程、字段位置或校验状态不一致,未确认校验结果。');
|
|
249
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
250
|
+
throw failure;
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function execute(command, args) {
|
|
256
|
+
if (['list', 'show', 'resumption-list', 'resumption-show'].includes(command))
|
|
257
|
+
return require('./teacher-leave-query.cjs').execute(command, args, formField);
|
|
258
|
+
if (['resumption-remind', 'resumption-withdraw'].includes(command)) return executeResumptionOperation(command, args, 'leave');
|
|
259
|
+
if (command === 'form' || command === 'resumption-form') {
|
|
260
|
+
const resumption = command === 'resumption-form';
|
|
261
|
+
const payload = options(args, ['program_code', 'field_values', ...(resumption ? ['leave_id', 'resumption_time', 'reason'] : [])], [], [], true);
|
|
262
|
+
if (resumption) {
|
|
263
|
+
requireArguments(payload, 'leave_id');
|
|
264
|
+
if (!require('./teacher-leave-query.cjs').validId(payload.leave_id, 'leave_'))
|
|
265
|
+
throw invalid('--leave-id 必须是原请假的有效业务记录 ID。');
|
|
266
|
+
}
|
|
267
|
+
if (payload.program_code !== undefined && !payload.program_code.trim()) throw invalid('--workflow-code 不能是空白。');
|
|
268
|
+
if (payload.resumption_time !== undefined) validateResumptionTime(payload.resumption_time);
|
|
269
|
+
if (payload.reason !== undefined && payload.reason.length > 100) throw invalid('--reason 最多 100 字。');
|
|
270
|
+
parseFieldValues(payload);
|
|
271
|
+
return readTeacherForm(payload, resumption);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (['submit', 'resumption-submit'].includes(command)) return executeStart(command, args, 'leave');
|
|
275
|
+
if (['approve', 'reject', 'approval', 'approvals', 'resumption-approve', 'resumption-reject', 'resumption-approval', 'resumption-approvals'].includes(command))
|
|
276
|
+
return executeApproval(command, args, 'leave');
|
|
277
|
+
if (['remind', 'withdraw'].includes(command)) {
|
|
278
|
+
const payload = options(args, ['id'], [], [], true);
|
|
279
|
+
requireArguments(payload, 'id');
|
|
280
|
+
if (!require('./teacher-leave-query.cjs').validId(payload.id, 'leave_'))
|
|
281
|
+
throw invalid('--id 使用请假提交或查询返回的 ID。');
|
|
282
|
+
const result = await gateway.execute('attendance-leave:write', 'leave/' + command, payload, {
|
|
283
|
+
id: required('string'), event: required('string'),
|
|
284
|
+
...(command === 'remind' ? { notified_count: required('integer') } : { withdrawn: required('boolean') }),
|
|
285
|
+
});
|
|
286
|
+
if (result.value.data.id !== payload.id) {
|
|
287
|
+
const failure = new (require('./result.cjs').Failure)(1, 'api', 'invalid_response', '请假操作回执与目标不一致,请查询目标申请,不要自动重试。',
|
|
288
|
+
{ details: { id: payload.id, action: command } });
|
|
289
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
290
|
+
throw failure;
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
throw invalid('未知的本人请假命令。');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function executeApproval(command, args, domain) {
|
|
298
|
+
if (['approve', 'reject', 'resumption-approve', 'resumption-reject'].includes(command)) {
|
|
299
|
+
const payload = options(args, ['task_id', 'opinion', ...(['reject', 'resumption-reject'].includes(command) ? ['return_to'] : ['field_values'])], [], [], true);
|
|
300
|
+
requireArguments(payload, 'task_id');
|
|
301
|
+
if (!/^[1-9][0-9]*$/.test(payload.task_id) || BigInt(payload.task_id) > 9223372036854775807n)
|
|
302
|
+
throw invalid('--task-id 必须是业务返回的有效审批任务 ID。');
|
|
303
|
+
if (['reject', 'resumption-reject'].includes(command)) {
|
|
304
|
+
requireArguments(payload, 'return_to');
|
|
305
|
+
const enums = require('./attendance-leave-enums.cjs');
|
|
306
|
+
payload.return_type = enums.encode(enums.returnTo, payload.return_to, '--return-to');
|
|
307
|
+
delete payload.return_to;
|
|
308
|
+
}
|
|
309
|
+
parseFieldValues(payload);
|
|
310
|
+
if (['approve', 'resumption-approve'].includes(command)) {
|
|
311
|
+
let detail;
|
|
312
|
+
try {
|
|
313
|
+
detail = await executeApproval(command === 'approve' ? 'approval' : 'resumption-approval',
|
|
314
|
+
['--category', 'pending', '--task-id', payload.task_id], domain);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const failure = error.result?.value?.error;
|
|
317
|
+
if (failure) {
|
|
318
|
+
failure.execution_status = 'NOT_EXECUTED';
|
|
319
|
+
if (['auth_required', 'missing_scope'].includes(failure.subtype))
|
|
320
|
+
failure.required_scopes = ['attendance-leave:read', 'attendance-leave:approve'];
|
|
321
|
+
}
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
const check = require('./teacher-leave-form-check.cjs').evaluate(detail.value.data, payload.field_values, { approval: true });
|
|
325
|
+
if (detail.value.data.opinion_required && !payload.opinion?.trim()) {
|
|
326
|
+
check.issues.push({ code: 'missing_required', parameter: 'opinion', message: '请补充审批意见。' });
|
|
327
|
+
if (check.status === 'complete') check.status = 'needs_input';
|
|
328
|
+
}
|
|
329
|
+
if (check.status !== 'complete') throw new (require('./result.cjs').Failure)(2, 'validation', 'form_input_required',
|
|
330
|
+
'审批表单还有需要处理的信息,尚未上传附件或执行审批。', { execution_status: 'NOT_EXECUTED', form_check: check });
|
|
331
|
+
}
|
|
332
|
+
const submit = ['approve', 'resumption-approve'].includes(command) ? require('./leave-approval-receipt.cjs').execute : gateway.execute;
|
|
333
|
+
const send = async () => {
|
|
334
|
+
const result = await submit('attendance-leave:approve', domain + '/' + command, payload, {
|
|
335
|
+
task_id: required('string'), request_id: required('string'), action: required('string'),
|
|
336
|
+
execution_status: required('string'), post_processing_status: required('string'),
|
|
337
|
+
application_id: nullable('string'), application_status: nullable('integer'), message: required('string'),
|
|
338
|
+
});
|
|
339
|
+
if (result.value.data.task_id !== payload.task_id) {
|
|
340
|
+
const failure = new (require('./result.cjs').Failure)(1, 'api', 'invalid_response', '审批回执任务与请求不一致,结果待核实,不要自动重试。',
|
|
341
|
+
{ details: { task_id: payload.task_id, request_id: result.value.data.request_id } });
|
|
342
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
343
|
+
throw failure;
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
};
|
|
347
|
+
return ['approve', 'resumption-approve'].includes(command)
|
|
348
|
+
? require('./teacher-leave-files.cjs').submit(command, payload, send, domain) : send();
|
|
349
|
+
}
|
|
350
|
+
if (['approval', 'resumption-approval'].includes(command)) {
|
|
351
|
+
const payload = options(args, ['category', 'task_id', 'notice_id'], [], [], true);
|
|
352
|
+
requireArguments(payload, 'category', 'task_id');
|
|
353
|
+
if (!['pending', 'handled', 'copied'].includes(payload.category))
|
|
354
|
+
throw invalid('--category 必须为 pending、handled 或 copied。');
|
|
355
|
+
if (payload.category === 'copied') requireArguments(payload, 'notice_id');
|
|
356
|
+
else if (payload.notice_id !== undefined) throw invalid('仅抄送详情接受 --notice-id。');
|
|
357
|
+
for (const key of ['task_id', 'notice_id']) {
|
|
358
|
+
if (payload[key] !== undefined && (!/^[1-9][0-9]*$/.test(payload[key]) || BigInt(payload[key]) > 9223372036854775807n))
|
|
359
|
+
throw invalid('--' + key.replaceAll('_', '-') + ' 必须是业务返回的有效标识。');
|
|
360
|
+
}
|
|
361
|
+
const result = await gateway.execute('attendance-leave:read', domain + '/' + command, payload, {
|
|
362
|
+
category: required('string'), tenant_id: required('string'), reviewer_id: required('string'), task: required(approvalTask),
|
|
363
|
+
form_id: required('string'), status: required('integer'), status_description: nullable('string'),
|
|
364
|
+
fields: required([formField]), progress: required([{ task_id: nullable('string'), node_name: nullable('string'),
|
|
365
|
+
node_type: nullable('integer'), handler_name: nullable('string'), handler_time: nullable('string'),
|
|
366
|
+
status: required('integer'), status_description: nullable('string'), opinion: nullable('string') }]),
|
|
367
|
+
attachments: required([require('./teacher-leave-files.cjs').attachment]),
|
|
368
|
+
can_approve: required('boolean'), can_reject: required('boolean'), opinion_visible: required('boolean'),
|
|
369
|
+
opinion_required: required('boolean'), reject_return_types: required(['integer']),
|
|
370
|
+
});
|
|
371
|
+
require('./teacher-leave-files.cjs').displayAttachments(result.value.data);
|
|
372
|
+
if (payload.category === 'pending') result.value.data.form_check = require('./teacher-leave-form-check.cjs')
|
|
373
|
+
.evaluate(result.value.data, [], { approval: true });
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
if (['approvals', 'resumption-approvals'].includes(command)) {
|
|
377
|
+
const payload = { page: 1, page_size: 20, ...options(args, ['category', 'page', 'page_size', ...require('./attendance-leave-list-filters.cjs').dates, ...require('./attendance-leave-list-filters.cjs').applicants, ...require('./attendance-leave-list-filters.cjs').subjects], [], [], true) };
|
|
378
|
+
require('./attendance-leave-list-filters.cjs').validate(payload);
|
|
379
|
+
requireArguments(payload, 'category');
|
|
380
|
+
if (!['pending', 'handled', 'copied'].includes(payload.category))
|
|
381
|
+
throw invalid('--category 必须为 pending、handled 或 copied。');
|
|
382
|
+
for (const [key, maximum] of [['page', 2147483647], ['page_size', 50]]) {
|
|
383
|
+
if (!/^\d+$/.test(String(payload[key])) || +payload[key] < 1 || +payload[key] > maximum)
|
|
384
|
+
throw invalid('--' + key.replaceAll('_', '-') + ' 必须在 1 到 ' + maximum + ' 之间。');
|
|
385
|
+
payload[key] = +payload[key];
|
|
386
|
+
}
|
|
387
|
+
return gateway.execute('attendance-leave:read', domain + '/' + command, payload, {
|
|
388
|
+
category: required('string'), tenant_id: required('string'), reviewer_id: required('string'),
|
|
389
|
+
page: required('integer'), page_size: required('integer'), total: required('integer'), total_pages: required('integer'),
|
|
390
|
+
items: required([approvalTask]),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function executeStart(command, args, domain) {
|
|
395
|
+
const isResumption = command === 'resumption-submit';
|
|
396
|
+
const payload = options(args, ['program_code', 'field_values', ...(isResumption ? ['leave_id', 'resumption_time', 'reason', ...(domain === 'student-leave' ? ['student_id'] : [])] : [])], [], [], true);
|
|
397
|
+
requireArguments(payload, 'program_code');
|
|
398
|
+
if (isResumption) {
|
|
399
|
+
requireArguments(payload, 'leave_id', 'resumption_time');
|
|
400
|
+
if (domain === 'leave' ? !require('./teacher-leave-query.cjs').validId(payload.leave_id, 'leave_')
|
|
401
|
+
: !require('./student-leave-query.cjs').validId(payload.leave_id, false))
|
|
402
|
+
throw invalid('--leave-id 必须是原请假的有效业务记录 ID。');
|
|
403
|
+
validateResumptionTime(payload.resumption_time);
|
|
404
|
+
if (payload.reason !== undefined && payload.reason.length > 100) throw invalid('--reason 最多 100 字。');
|
|
405
|
+
}
|
|
406
|
+
parseFieldValues(payload);
|
|
407
|
+
if (domain === 'leave') {
|
|
408
|
+
try {
|
|
409
|
+
const form = (await readTeacherForm(payload, isResumption)).value.data;
|
|
410
|
+
if (form.status !== 'ready' || form.form_check.status !== 'complete')
|
|
411
|
+
throw new (require('./result.cjs').Failure)(2, 'validation', 'form_input_required', '表单还有需要处理的信息,尚未上传附件或提交申请。', {
|
|
412
|
+
form_check: form.form_check, hint: '合并处理 form_check.issues;只向老师追问缺少或有歧义的事实。信息齐全后直接提交,无需再单独检查。',
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error.result?.value?.error) {
|
|
416
|
+
const details = error.result.value.error;
|
|
417
|
+
details.execution_status = 'NOT_EXECUTED';
|
|
418
|
+
details.step = 'check_form_before_upload';
|
|
419
|
+
if (details.subtype === 'auth_required' || details.subtype === 'missing_scope')
|
|
420
|
+
details.required_scopes = ['attendance-leave:read', 'attendance-leave:write'];
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (domain === 'student-leave') await require('./student-leave-form.cjs').checkBeforeSubmit(payload, true);
|
|
426
|
+
const send = async () => {
|
|
427
|
+
const result = await require('./leave-start-receipt.cjs').execute('attendance-leave:write', domain + '/' + command, payload, {
|
|
428
|
+
...(isResumption ? { leave_id: required('string') } : {}),
|
|
429
|
+
request_id: required('string'), program_code: required('string'), execution_status: required('string'),
|
|
430
|
+
post_processing_status: required('string'), next_task_id: nullable('string'),
|
|
431
|
+
id: nullable('string'), application_status: nullable('integer'), message: required('string'),
|
|
432
|
+
}, '申请');
|
|
433
|
+
if (result.value.data.program_code !== payload.program_code || isResumption && result.value.data.leave_id !== payload.leave_id) {
|
|
434
|
+
const failure = new (require('./result.cjs').Failure)(1, 'api', 'invalid_response', '提交回执流程与请求不一致,结果待核实,不要自动重试。');
|
|
435
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id;
|
|
436
|
+
throw failure;
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
439
|
+
};
|
|
440
|
+
if (domain === 'student-leave') return require('./teacher-leave-files.cjs').submit(command, payload, send, domain);
|
|
441
|
+
try { return await require('./teacher-leave-files.cjs').submit(command, payload, send); }
|
|
442
|
+
catch (error) {
|
|
443
|
+
const details = error.result?.value?.error;
|
|
444
|
+
if (details && ['auth_required', 'missing_scope'].includes(details.subtype))
|
|
445
|
+
details.required_scopes = ['attendance-leave:read', 'attendance-leave:write'];
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function validateResumptionTime(value) {
|
|
451
|
+
const epoch = Date.parse(value.replace(' ', 'T') + '+08:00');
|
|
452
|
+
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value) || !Number.isFinite(epoch)
|
|
453
|
+
|| new Date(epoch + 8 * 3600000).toISOString().slice(0, 19).replace('T', ' ') !== value)
|
|
454
|
+
throw invalid('--resumption-time 必须是有效的 YYYY-MM-DD HH:mm:ss(Asia/Shanghai)时间。');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
module.exports = { execute, validateStudentForm, executeApproval, executeStart, executeResumptionOperation, formField, teacherFormField, parseFieldValues, validateResumptionTime };
|
|
@@ -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 { replenishmentId } = require('./attendance-leave-ids.cjs');
|
|
5
|
+
function validDate(value) { return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value; }
|
|
6
|
+
async function execute(args, domain, write = false) {
|
|
7
|
+
const keys = write ? ['id'] : ['date'];
|
|
8
|
+
const type = domain === 'dormitory-attendance' ? '2' : '1';
|
|
9
|
+
const payload = options(args, keys, [], [], true); requireArguments(payload, ...keys);
|
|
10
|
+
if (write ? !replenishmentId(payload.id, type) : !validDate(payload.date)) throw invalid(write ? '--id 必须来自本业务的补录提交回执或本人补录列表。' : '--date 必须为有效的 YYYY-MM-DD 日期。');
|
|
11
|
+
try {
|
|
12
|
+
const result = await gateway.execute('attendance-leave' + (write ? ':write' : ':read'), domain + (write ? '/replenishment-delete' : '/replenishments'), payload, {
|
|
13
|
+
tenant_id: required('string'), operator_id: required('string'), date: required('string'), attendance_type: required('string'),
|
|
14
|
+
...(write ? { subject_id: nullable('string'), id: required('string'), request_id: required('string'), execution_status: required('string'), message: required('string') }
|
|
15
|
+
: { total: required('integer'), records: required([{ id: required('string'), subject_id: required('string'), date: required('string'), subject_name: nullable('string'), department_id: nullable('string'), department_name: nullable('string'), recorded_time: nullable('string'), time_bucket: nullable('string'), direction: nullable('string'), handled_at: nullable('integer'), handler_name: nullable('string'), handler_role: nullable('string') }]) }),
|
|
16
|
+
});
|
|
17
|
+
const d = result.value.data;
|
|
18
|
+
if (!validDate(d.date) || d.attendance_type !== type
|
|
19
|
+
|| (write ? d.id !== payload.id || !replenishmentId(d.id, type, d.date)
|
|
20
|
+
|| d.execution_status === 'COMPLETED' && !d.subject_id
|
|
21
|
+
|| result.value.request_id && result.value.request_id !== d.request_id
|
|
22
|
+
|| !['COMPLETED', 'NOT_EXECUTED', 'UNCONFIRMED'].includes(d.execution_status)
|
|
23
|
+
: d.date !== payload.date || d.total !== d.records.length || new Set(d.records.map(row => row.id)).size !== d.records.length
|
|
24
|
+
|| d.records.some(row => !replenishmentId(row.id, type, payload.date) || row.date !== payload.date))) {
|
|
25
|
+
const failure = new Failure(1, 'api', 'invalid_response', '本人补签到返回的日期、类型、记录或删除阶段缺失或不一致。');
|
|
26
|
+
if (result.value.request_id) failure.result.value.request_id = result.value.request_id; throw failure;
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
} catch (failure) {
|
|
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' } : {}) };
|
|
34
|
+
if (unknown) error.message += '补签到删除结果待核实,请保留记录和请求标识,按需要查询本人补签到记录,不要自动重试。';
|
|
35
|
+
}
|
|
36
|
+
throw failure;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
module.exports = { execute };
|
package/lib/result.cjs
CHANGED
|
@@ -36,6 +36,7 @@ const present = schema => ({ $present: schema });
|
|
|
36
36
|
const optionalFields = schema => Object.fromEntries(Object.entries(schema).map(([key, type]) => [key, optional(type)]));
|
|
37
37
|
const nullableFields = schema => Object.fromEntries(Object.entries(schema).map(([key, type]) => [key, nullable(type)]));
|
|
38
38
|
function fields(value, schema, path = 'data') {
|
|
39
|
+
if (typeof schema === 'function') return schema(value, path);
|
|
39
40
|
if (schema.$required !== undefined) {
|
|
40
41
|
if (value == null || (schema.$required === 'string' && (typeof value !== 'string' || !value.trim()))) {
|
|
41
42
|
throw new Error(`响应字段 ${path} 缺失或无效。`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { required, nullable } = require('./result.cjs');
|
|
2
|
+
const reportSchema = { report_id: required('string'), content_date: required('string'), delivery_date: nullable('string'), time_bucket: required('string'),
|
|
3
|
+
scope_type: required('string'), scope_id: nullable('string'), scope_name: nullable('string'), snapshot_missing_count: required('integer'),
|
|
4
|
+
configuration_name: nullable('string'), calculation_time: nullable('string'), direction: nullable('string') };
|
|
5
|
+
function validDate(value) {
|
|
6
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
|
|
7
|
+
}
|
|
8
|
+
function validReportContext(report, reportId, scopeTypes) {
|
|
9
|
+
return report.report_id === reportId && scopeTypes.includes(report.scope_type) && validDate(report.content_date)
|
|
10
|
+
&& (report.delivery_date == null || validDate(report.delivery_date)) && !!report.time_bucket.trim()
|
|
11
|
+
&& report.snapshot_missing_count >= 0;
|
|
12
|
+
}
|
|
13
|
+
function validReport(report, reportId, scopeTypes) {
|
|
14
|
+
return validReportContext(report, reportId, scopeTypes);
|
|
15
|
+
}
|
|
16
|
+
module.exports = { reportSchema, validReport, validReportContext };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { options, required: requireArguments } = require('./arguments.cjs');
|
|
2
|
+
const { required, nullable, invalid, Failure } = require('./result.cjs');
|
|
3
|
+
const { confirmMobile } = require('./contact-mobile.cjs');
|
|
4
|
+
const gateway = require('./gateway.cjs');
|
|
5
|
+
|
|
6
|
+
async function execute(args) {
|
|
7
|
+
const payload = options(args, ['id'], ['include_mobile', 'yes'], [], true);
|
|
8
|
+
requireArguments(payload, 'id');
|
|
9
|
+
if (payload.id.length > 200) throw invalid('--id 最多200字符。');
|
|
10
|
+
confirmMobile(payload);
|
|
11
|
+
const result = await gateway.execute('school:read', 'school/students/contacts', payload, {
|
|
12
|
+
tenant_id: required('string'), operator_id: required('string'),
|
|
13
|
+
student: required({ id: required('string'), name: required('string') }), include_mobile: required('boolean'),
|
|
14
|
+
items: required([{ id: nullable('string'), relationship: required('string'), relationship_name: required('string'),
|
|
15
|
+
...(payload.include_mobile ? { mobile: nullable('string') } : {}) }]),
|
|
16
|
+
});
|
|
17
|
+
const d = result.value.data;
|
|
18
|
+
if (d.student.id !== payload.id || d.include_mobile !== payload.include_mobile
|
|
19
|
+
|| d.items.some(row => !['self', 'parent', 'father', 'mother', 'grandpa', 'grandma', 'wai_grandpa', 'wai_grandma', 'other', 'guardian'].includes(row.relationship)))
|
|
20
|
+
throw new Failure(1, 'api', 'invalid_response', '学生联系人查询返回的学生、关系或手机号请求状态不一致。');
|
|
21
|
+
d.message = d.items.length ? `返回该学生的${d.items.length}条联系人关系。` : '该学生未登记联系人。';
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
module.exports = { execute };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const paging = '[--page <n>] [--page-size <1-100>]';
|
|
2
|
+
const usage = {
|
|
3
|
+
'school students contacts': '--id <student-id> [--include-mobile] [--yes]',
|
|
4
|
+
'school departments search': '--name <text> [--department-id <id>] [--include-teachers] [--include-mobile] [--yes] ' + paging,
|
|
5
|
+
'school departments tree': '[--department-id <id>] [--depth <1-20>] [--include-teachers] [--include-mobile] [--yes] ' + paging,
|
|
6
|
+
'school departments members': '--department-id <id> [--department-id <id>...] [--include-descendants] ' + paging,
|
|
7
|
+
'school students search': '--name <text> [--department-id <id>...] [--include-descendants] ' + paging,
|
|
8
|
+
};
|
|
9
|
+
const summaries = {
|
|
10
|
+
'school students contacts': '查看学生登记的联系人',
|
|
11
|
+
school: '家校查询:找年级、班级和学生,查看负责人及任课教师',
|
|
12
|
+
'school departments': '查找家校部门、浏览结构和列出学生',
|
|
13
|
+
'school students': '查找在校学生及其联系人',
|
|
14
|
+
'school departments search': '按名称查找可见的年级、班级等家校部门',
|
|
15
|
+
'school departments tree': '查看校区、学段、年级和班级结构',
|
|
16
|
+
'school departments members': '查看班级或年级的学生名单',
|
|
17
|
+
'school students search': '按姓名找在校学生,可限定班级或年级',
|
|
18
|
+
};
|
|
19
|
+
const definitions = Object.fromEntries(Object.keys(usage).map(command => [command, {
|
|
20
|
+
purpose: summaries[command], scope: 'school:read', errors: 'read',
|
|
21
|
+
}]));
|
|
22
|
+
function render(command) {
|
|
23
|
+
if (!usage[command]) return null;
|
|
24
|
+
if (command === 'school students contacts') return contactsHelp(command);
|
|
25
|
+
const tree = command.endsWith(' tree'), members = command.endsWith(' members');
|
|
26
|
+
const students = members || command === 'school students search';
|
|
27
|
+
const params = [];
|
|
28
|
+
if (!tree && !members) params.push('--name 必填;' + (students ? '学生姓名' : '部门名称') + '文字片段,1到50字,按包含匹配;不解释为正则或通配符。');
|
|
29
|
+
params.push(students
|
|
30
|
+
? '--department-id ' + (members ? '必填' : '可选') + ';可重复指定不同班级或家校部门,最多200个,合并查询并去重;默认只查直属学生。'
|
|
31
|
+
: '--department-id 可选,只能指定一个;' + (tree ? '作为子树根节点并包含该节点。省略从当前可见顶层开始。' : '在该部门及其下级范围搜索。省略搜索当前学校可见家校部门。'));
|
|
32
|
+
if (students) params.push('--include-descendants 可选开关;包含所选部门的可见下级部门学生,必须同时指定部门。');
|
|
33
|
+
else params.push('--include-teachers 可选开关;展开班级、兴趣班、自定义班级的可见任课教师及任教学科。默认只返回负责人、班主任和其他非任课岗位。');
|
|
34
|
+
if (!students) params.push('--include-mobile 可选;仅老师明确要求或用途需要时,读取本次结果中可见人员的手机号。任课教师须同时用 --include-teachers 展开。',
|
|
35
|
+
'--yes 读取手机号时必填;先说明对象、范围和用途,等待老师二次确认后传入。首次提出需求不算二次确认;单独使用报错。');
|
|
36
|
+
if (tree) params.push('--depth 可选,默认2,范围1到20;查询根算第1层,省略根时可见顶层算第1层。');
|
|
37
|
+
params.push('--page 可选,从1开始,默认1。', '--page-size 可选,默认20,范围1到100。',
|
|
38
|
+
students ? '仅 --department-id 可重复传入;重复部门 ID 报错,其他参数不能重复。' : '所有参数均不能重复传入。');
|
|
39
|
+
const base = '部门含 id、name、type、type_name、parent_id、path(可见名称路径)、ancestors(可见祖先的 ID、名称和类型)、has_children、level。类型名称无法识别时为 null。隐藏祖先不返回,parent_id 指向最近可见祖先。';
|
|
40
|
+
const result = students
|
|
41
|
+
? 'items:学生列表,每项含 id、name、student_number(学号,未填写为 null)、departments。\n departments:所属部门。' + base + '指定部门时只返回本次所选范围内的部门关系,不展开任职人员。\n 一人多班只计一次;total 为筛选和权限过滤后的学生人数。'
|
|
42
|
+
: 'items:家校部门列表。\n ' + base + '\n 部门另含 alias(别名,未配置为 null)、leaders(负责人)、head_teachers(班主任)、subject_teachers(按需展开的任课教师)、other_positions(其他实际岗位)。\n 人员含 id、name、role、role_code、subjects;subjects 含 code、name、source,同一老师可任教多个学科,未取得的岗位或学科名称为 null。\n 默认不返回 mobile;确认展开后人员项增加 mobile,未登记为 null。' + (tree
|
|
43
|
+
? '\n 树为平铺节点列表,按 level、名称、ID 排序;指定根的 parent_id 为 null,level 从查询根开始。父节点可能在其他页。total 是当前深度内节点数;depth_limited=true 表示还有更深层节点。'
|
|
44
|
+
: '\n 精确名称匹配优先,再按名称、ID 排序;同名部门分别返回。level 为可见目录层数,total 为匹配部门数。');
|
|
45
|
+
const statuses = students ? '' : '\n 人员信息状态(*_status):\n complete:完整配置,空数组表示未配置。\n visible_only:仅当前可见人员,空数组不代表未配置。\n not_requested:未展开。\n not_applicable:此部门类型不提供该项。\n subject_teachers 未展开或不适用时为 null。负责人和班主任均为数组,自定义岗位保留实际名称。';
|
|
46
|
+
const example = tree ? '--depth 2' : members ? '--department-id <七年级ID> --include-descendants'
|
|
47
|
+
: students ? '--name 小明' : '--name 七年级一班 --include-teachers';
|
|
48
|
+
return `${summaries[command]}。\n\n用法:yunzhi-cli ${command} ${usage[command]}\n\n参数:\n ${params.join('\n ')}\n\n示例:\n yunzhi-cli ${command} ${example}\n\n返回:\n ${result}${statuses}\n tenant_id、operator_id 为当前学校和操作人;name、department_ids 返回本次查询条件;${students ? 'include_descendants 表示是否包含下级。' + (members ? '按姓名、ID 排序。' : '精确姓名优先,再按姓名、ID 排序。') : 'include_teachers、include_mobile 表示是否展开任课教师及手机号。' + (tree ? 'depth 返回查询深度。' : 'depth 为 null,depth_limited 为 false。')}page、page_size、total、total_pages 为分页信息。\n\n限制:\n ${students ? '仅查询当前学校可见的在校学生,不含毕业生或家长;联系人另用 school students contacts 查询。看见年级不代表能查其全部学生。' : '仅查询当前学校可见的家校部门及任职人员。'}指定不可见或不存在的部门整体报错;来源失败报错,不当作空结果。分页偏移不能超过2147483647。\n ${students ? '学生搜索和名单不接受手机号参数。' : '任课信息是当前目录任教关系,不是当天课表或代课安排。手机号缺少 --yes 时退出码10,不发起查询。'}所需权限:school:read。默认输出 JSON,错误见 error.message/error.hint。\n`;
|
|
49
|
+
}
|
|
50
|
+
function contactsHelp(command) {
|
|
51
|
+
return `${summaries[command]}。默认只返回联系人关系;手机号需二次确认后获取。\n\n用法:yunzhi-cli ${command} ${usage[command]}\n\n参数:\n --id 必填,学生查询返回的 ID,最多200字符。\n --include-mobile 可选;仅老师明确要求手机号,或明确用途需要时使用。\n --yes 读取手机号时必填;先说明对象、范围和用途,等待老师二次确认后传入。首次要求查电话不算二次确认;单独使用报错。\n\n示例:\n yunzhi-cli ${command} --id <学生ID>\n # 老师二次确认之后:\n yunzhi-cli ${command} --id <学生ID> --include-mobile --yes\n\n返回:\n student:学生的 id、name。\n items:该学生登记的联系人,含 id(联系人ID,历史数据可能为空)、relationship、relationship_name。\n 关系包括本人、家长、父亲、母亲、爷爷、奶奶、外公、外婆、监护人、其他。保留每条登记关系。\n 不返回联系人姓名、身份证或关联的其他孩子。\n 默认不返回 mobile;确认展开后每项增加 mobile,未登记为 null。include_mobile 表示是否返回手机号,tenant_id、operator_id 为当前学校和操作人。\n 没有分页;空 items 表示未登记联系人,来源失败直接报错。\n\n限制:\n 仅查询当前老师可见的在校学生;确认不能扩大权限。请求手机号却缺少 --yes 时退出码10,不发起查询。所需权限:school:read。默认输出 JSON,错误见 error.message/error.hint。\n`;
|
|
52
|
+
}
|
|
53
|
+
module.exports = { usage, summaries, definitions, render };
|