feat: schedule student supervision rules

This commit is contained in:
Codex
2026-06-30 17:40:03 +08:00
parent 0204c934d8
commit c7ad3450fd
21 changed files with 1679 additions and 482 deletions

View File

@@ -9426,6 +9426,57 @@ async function testTenantStudentOperations() {
'supervision generation should be idempotent for the same batch and student',
);
const supervisionRule = await request('/api/tenant-admin/students/supervision/rules', {
userId: TENANT_TEACHER_USER_ID,
method: 'PUT',
body: {
name: '集成测试教师班级督导规则',
classId: ids.tenantClass,
status: 'active',
rules: {
inactivityDays: 1,
wrongQuestionThreshold: 1,
minAnswers: 1,
lowAccuracyThreshold: 0.99,
vocabularyDueThreshold: 1,
},
schedule: {
enabled: true,
frequency: 'daily',
hour: 9,
minute: 0,
},
limit: 5,
},
});
assert.ok(supervisionRule.item?.id, 'teacher should create class-scoped supervision rule');
assert.equal(supervisionRule.item?.classId, ids.tenantClass, 'supervision rule should bind scoped class');
assert.ok(supervisionRule.item?.nextRunAt, 'active scheduled supervision rule should compute next run time');
const supervisionRules = await request('/api/tenant-admin/students/supervision/rules', {
userId: TENANT_TEACHER_USER_ID,
query: { limit: 20 },
});
assert.equal(supervisionRules.scoped, true, 'teacher supervision rule list should be scoped');
assert.ok(
supervisionRules.items?.some(item => item.id === supervisionRule.item.id),
'teacher should list own scoped supervision rule',
);
const teacherGlobalSupervisionRuleDenied = await request('/api/tenant-admin/students/supervision/rules', {
userId: TENANT_TEACHER_USER_ID,
method: 'PUT',
body: {
name: '不应保存全局规则',
status: 'active',
rules: { inactivityDays: 1 },
schedule: { enabled: true, frequency: 'daily' },
limit: 5,
},
expectStatus: 403,
});
assert.equal(teacherGlobalSupervisionRuleDenied.code, 'CLASS_SCOPE_REQUIRED', 'teacher should not create global supervision rules');
const teacherOtherSupervisionDenied = await request('/api/tenant-admin/students/supervision/generate', {
userId: TENANT_TEACHER_USER_ID,
method: 'POST',
@@ -9446,6 +9497,13 @@ async function testTenantStudentOperations() {
});
assert.equal(operatorSupervisionDenied.code, 'TENANT_PERMISSION_REQUIRED', 'operator without supervision permission should be denied');
const operatorSupervisionRuleDenied = await request('/api/tenant-admin/students/supervision/rules', {
userId: TENANT_OPERATOR_USER_ID,
query: { limit: 5 },
expectStatus: 403,
});
assert.equal(operatorSupervisionRuleDenied.code, 'TENANT_PERMISSION_REQUIRED', 'operator without supervision permission should not list rules');
const teacherOtherFollowupDenied = await request('/api/tenant-admin/students/followups', {
userId: TENANT_TEACHER_USER_ID,
method: 'PUT',
@@ -9504,6 +9562,7 @@ async function testTenantStudentOperations() {
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student_note.upserted'), 'audit logs should include student note upsert');
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student_followup.upserted'), 'audit logs should include student follow-up upsert');
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.students.supervision_generated'), 'audit logs should include supervision generation');
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.students.supervision_rule_upserted'), 'audit logs should include supervision rule upsert');
}
async function testReferralAndCrmGrowth() {