param( [string] $ApiExecutablePath, [string] $ApiContentRoot ) $ErrorActionPreference = 'Stop' $workspaceRoot = Split-Path -Parent $PSScriptRoot $apiExecutable = if ([string]::IsNullOrWhiteSpace($ApiExecutablePath)) { Join-Path $workspaceRoot 'src/Jiaowu.Api/bin/Debug/net10.0/Jiaowu.Api.exe' } else { [System.IO.Path]::GetFullPath($ApiExecutablePath, $workspaceRoot) } $apiWorkingDirectory = if ([string]::IsNullOrWhiteSpace($ApiContentRoot)) { Join-Path $workspaceRoot 'src/Jiaowu.Api' } else { [System.IO.Path]::GetFullPath($ApiContentRoot, $workspaceRoot) } $stdoutPath = Join-Path $env:TEMP 'jiaowu-api-smoke.out.log' $stderrPath = Join-Path $env:TEMP 'jiaowu-api-smoke.err.log' $courseExcelToken = [guid]::NewGuid().ToString('N') $courseTemplatePath = Join-Path $env:TEMP "jiaowu-course-template-$courseExcelToken.xlsx" $courseExportPath = Join-Path $env:TEMP "jiaowu-course-export-$courseExcelToken.xlsx" $env:ASPNETCORE_ENVIRONMENT = 'Development' $env:ASPNETCORE_URLS = 'http://localhost:5255' $startParameters = @{ FilePath = $apiExecutable WorkingDirectory = $apiWorkingDirectory WindowStyle = 'Hidden' RedirectStandardOutput = $stdoutPath RedirectStandardError = $stderrPath PassThru = $true } $process = Start-Process @startParameters try { $healthy = $false for ($attempt = 0; $attempt -lt 30; $attempt++) { Start-Sleep -Milliseconds 500 try { $health = Invoke-RestMethod -Uri 'http://localhost:5255/health' -TimeoutSec 2 $healthy = $true break } catch { # The API may still be starting. } } if (-not $healthy) { Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue throw 'API did not become healthy.' } $loginBody = @{ userName = 'admin' password = 'Admin@123456' } | ConvertTo-Json $loginParameters = @{ Method = 'Post' Uri = 'http://localhost:5255/api/auth/login' ContentType = 'application/json' Body = $loginBody } $login = Invoke-RestMethod @loginParameters $headers = @{ Authorization = "Bearer $($login.token)" } $dashboard = Invoke-RestMethod -Uri 'http://localhost:5255/api/dashboard' -Headers $headers $campuses = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/campuses' -Headers $headers $classes = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/classes' -Headers $headers $counselors = Invoke-RestMethod -Uri 'http://localhost:5255/api/base-data/counselors' -Headers $headers $assignedClassCount = @($classes | Where-Object { $null -ne $_.counselorUserId -and @($counselors).id -contains $_.counselorUserId }).Count if ($assignedClassCount -lt 1) { throw 'No administrative class is linked to a counselor account.' } $teachers = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/teachers?page=1&pageSize=10' -Headers $headers $students = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/students?page=1&pageSize=10' -Headers $headers $courses = Invoke-RestMethod -Uri 'http://localhost:5255/api/courses?page=1&pageSize=10' -Headers $headers $courseCategories = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/base-data/course-categories' ` -Headers $headers if (@($courseCategories).Count -lt 10 -or @($courses.items | Where-Object { $null -eq $_.courseCategoryId }).Count -gt 0) { throw 'Course categories were not seeded or assigned to all courses.' } Invoke-WebRequest ` -Uri 'http://localhost:5255/api/courses/template' ` -Headers $headers ` -OutFile $courseTemplatePath | Out-Null Invoke-WebRequest ` -Uri 'http://localhost:5255/api/courses/export' ` -Headers $headers ` -OutFile $courseExportPath | Out-Null if ((Get-Item -LiteralPath $courseTemplatePath).Length -lt 1 -or (Get-Item -LiteralPath $courseExportPath).Length -lt 1) { throw 'Course Excel template or export is empty.' } $courseImport = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/courses/import' ` -Headers $headers ` -Form @{ file = Get-Item -LiteralPath $courseExportPath } if ($courseImport.created -ne 0 -or $courseImport.updated -ne $courses.total -or $courseImport.total -ne $courses.total) { throw 'Course Excel round-trip did not update the expected records.' } $curriculumPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/curriculum-plans?page=1&pageSize=10' -Headers $headers if ($curriculumPlans.total -gt 0) { $curriculumDetail = Invoke-RestMethod ` -Uri "http://localhost:5255/api/curriculum-plans/$($curriculumPlans.items[0].id)" ` -Headers $headers } $teachingTasks = Invoke-RestMethod -Uri 'http://localhost:5255/api/teaching-tasks?page=1&pageSize=10' -Headers $headers $schedulePlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/schedules/plans' -Headers $headers if (@($schedulePlans).Count -gt 0) { $scheduleDetail = Invoke-RestMethod ` -Uri "http://localhost:5255/api/schedules/plans/$($schedulePlans[0].id)" ` -Headers $headers } $selectionRounds = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/course-selections/rounds' ` -Headers $headers if (@($selectionRounds).Count -lt 1) { throw 'Development course-selection round was not seeded.' } $activeSelectionRound = @($selectionRounds) | Where-Object { $_.isAvailableNow } | Select-Object -First 1 if ($null -eq $activeSelectionRound) { throw 'No course-selection round is open for the smoke test.' } $selectionOfferings = Invoke-RestMethod ` -Uri "http://localhost:5255/api/course-selections/rounds/$($activeSelectionRound.id)/offerings" ` -Headers $headers if (@($selectionOfferings).Count -lt 1) { throw 'Development course-selection offering was not seeded.' } $managedUsers = Invoke-RestMethod -Uri 'http://localhost:5255/api/users' -Headers $headers $teacherAccount = @($managedUsers) | Where-Object { $_.userName -eq 'teacher' } | Select-Object -First 1 if ($null -eq $teacherAccount) { throw 'Development teacher account was not seeded.' } $teacherAccessBody = @{ staffNumber = $teacherAccount.staffNumber collegeId = $teacherAccount.collegeId roles = @('Teacher') } | ConvertTo-Json Invoke-RestMethod ` -Method Put ` -Uri "http://localhost:5255/api/users/$($teacherAccount.id)/roles" ` -Headers $headers ` -ContentType 'application/json' ` -Body $teacherAccessBody $scopeScenarios = @( @{ UserName = 'college'; Password = 'College@123456'; Scope = 'College' Teachers = 2; Students = 3; Courses = 3 }, @{ UserName = 'counselor'; Password = 'Counselor@123456'; Scope = 'Class' Teachers = 1; Students = 3; Courses = 1 }, @{ UserName = 'teacher'; Password = 'Teacher@123456'; Scope = 'Self' Teachers = 1; Students = 3; Courses = 1 }, @{ UserName = 'student'; Password = 'Student@123456'; Scope = 'Self' Teachers = 0; Students = 1; Courses = 1 } ) $scopeChecks = foreach ($scenario in $scopeScenarios) { $scenarioLoginBody = @{ userName = $scenario.UserName password = $scenario.Password } | ConvertTo-Json $scenarioLogin = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/auth/login' ` -ContentType 'application/json' ` -Body $scenarioLoginBody $scenarioHeaders = @{ Authorization = "Bearer $($scenarioLogin.token)" } $scenarioTeachers = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/personnel/teachers?page=1&pageSize=10' ` -Headers $scenarioHeaders $scenarioStudents = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/personnel/students?page=1&pageSize=10' ` -Headers $scenarioHeaders $scenarioCourses = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/courses?page=1&pageSize=10' ` -Headers $scenarioHeaders if ($scenarioLogin.user.effectiveDataScope -ne $scenario.Scope -or $scenarioTeachers.total -ne $scenario.Teachers -or $scenarioStudents.total -ne $scenario.Students -or $scenarioCourses.total -ne $scenario.Courses) { throw ("Data-scope check failed for {0}: scope={1}, teachers={2}, students={3}, courses={4}." -f $scenario.UserName, $scenarioLogin.user.effectiveDataScope, $scenarioTeachers.total, $scenarioStudents.total, $scenarioCourses.total) } "$($scenario.UserName):$($scenario.Scope)" } $studentLoginBody = @{ userName = 'student' password = 'Student@123456' } | ConvertTo-Json $studentLogin = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/auth/login' ` -ContentType 'application/json' ` -Body $studentLoginBody $studentHeaders = @{ Authorization = "Bearer $($studentLogin.token)" } $studentOptions = Invoke-RestMethod ` -Uri "http://localhost:5255/api/course-selections/student/options?roundId=$($activeSelectionRound.id)" ` -Headers $studentHeaders $studentOffering = @($studentOptions.offerings) | Select-Object -First 1 if ($null -eq $studentOffering) { throw 'Student has no eligible course-selection offering.' } if ($studentOffering.enrollmentStatus -ne 'Enrolled') { $enrollmentBody = @{ offeringId = $studentOffering.id } | ConvertTo-Json Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/course-selections/student/enrollments' ` -Headers $studentHeaders ` -ContentType 'application/json' ` -Body $enrollmentBody | Out-Null } $studentEnrollments = Invoke-RestMethod ` -Uri "http://localhost:5255/api/course-selections/student/enrollments?academicTermId=$($activeSelectionRound.academicTermId)" ` -Headers $studentHeaders $activeEnrollments = @($studentEnrollments) | Where-Object { $_.status -eq 'Enrolled' } if ($activeEnrollments.Count -lt 1) { throw 'Student course enrollment was not persisted.' } $selectionRoster = Invoke-RestMethod ` -Uri "http://localhost:5255/api/course-selections/offerings/$($studentOffering.id)/roster" ` -Headers $headers if ($selectionRoster.enrolledCount -lt 1) { throw 'Course-selection roster did not include the selected student.' } $gradeTasks = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/grades/sheets' ` -Headers $headers $gradeTask = @($gradeTasks) | Where-Object { $null -ne $_.sheet } | Select-Object -First 1 if ($null -eq $gradeTask) { throw 'Development grade sheet was not seeded.' } $gradeDetail = Invoke-RestMethod ` -Uri "http://localhost:5255/api/grades/sheets/$($gradeTask.sheet.id)" ` -Headers $headers if (@($gradeDetail.sheet.records).Count -lt 1) { throw 'Development grade sheet has no student records.' } $teacherLoginBody = @{ userName = 'teacher' password = 'Teacher@123456' } | ConvertTo-Json $teacherLogin = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/auth/login' ` -ContentType 'application/json' ` -Body $teacherLoginBody $teacherHeaders = @{ Authorization = "Bearer $($teacherLogin.token)" } $teacherGradeTasks = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/grades/sheets' ` -Headers $teacherHeaders $teacherGradeTask = @($teacherGradeTasks) | Where-Object { $_.id -eq $gradeTask.id } | Select-Object -First 1 if ($null -eq $teacherGradeTask) { throw 'Assigned teacher cannot access the grade sheet.' } if ($teacherGradeTask.sheet.status -eq 'Draft' -or $teacherGradeTask.sheet.status -eq 'Returned') { Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/grades/sheets/$($teacherGradeTask.sheet.id)/submit" ` -Headers $teacherHeaders | Out-Null } $collegeLoginBody = @{ userName = 'college' password = 'College@123456' } | ConvertTo-Json $collegeLogin = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/auth/login' ` -ContentType 'application/json' ` -Body $collegeLoginBody $collegeHeaders = @{ Authorization = "Bearer $($collegeLogin.token)" } $collegeGradeTasks = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/grades/sheets' ` -Headers $collegeHeaders $collegeGradeTask = @($collegeGradeTasks) | Where-Object { $_.id -eq $gradeTask.id } | Select-Object -First 1 if ($collegeGradeTask.sheet.status -eq 'Submitted') { Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/grades/sheets/$($collegeGradeTask.sheet.id)/approve" ` -Headers $collegeHeaders | Out-Null } $adminGradeTasks = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/grades/sheets' ` -Headers $headers $adminGradeTask = @($adminGradeTasks) | Where-Object { $_.id -eq $gradeTask.id } | Select-Object -First 1 if ($adminGradeTask.sheet.status -eq 'Approved') { Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/grades/sheets/$($adminGradeTask.sheet.id)/publish" ` -Headers $headers | Out-Null } $studentTranscript = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/grades/student/transcript' ` -Headers $studentHeaders if (@($studentTranscript.records).Count -lt 1) { throw 'Published grade is not visible in the student transcript.' } $examPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/exams/plans' -Headers $headers if (@($examPlans).Count -lt 1) { throw 'Development exam plan was not seeded.' } $examDetail = Invoke-RestMethod ` -Uri "http://localhost:5255/api/exams/plans/$($examPlans[0].id)" ` -Headers $headers if (@($examDetail.sessions).Count -lt 1) { throw 'Exam plan has no sessions.' } $examRoster = Invoke-RestMethod ` -Uri "http://localhost:5255/api/exams/sessions/$($examDetail.sessions[0].id)/roster" ` -Headers $headers $studentExams = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/exams/my-schedule' ` -Headers $studentHeaders $teacherExams = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/exams/my-schedule' ` -Headers $teacherHeaders if (@($studentExams).Count -lt 1 -or @($teacherExams).Count -lt 1) { throw 'Personal exam schedule is not visible to student or invigilator.' } $statusOptions = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/student-status-changes/options' ` -Headers $studentHeaders $statusChanges = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/student-status-changes' ` -Headers $studentHeaders $openStatusChange = @($statusChanges) | Where-Object { $_.state -in @('Submitted', 'CounselorApproved', 'CollegeApproved') } | Select-Object -First 1 if ($null -eq $openStatusChange) { $statusChangeType = if ($statusOptions.status -eq 'Active') { 'Suspension' } elseif ($statusOptions.status -eq 'Suspended') { 'Resumption' } else { throw "Smoke-test student status '$($statusOptions.status)' cannot start a reversible change." } $statusChangeBody = @{ type = $statusChangeType reason = '端到端验证学籍异动三级审核流程。' } | ConvertTo-Json $createdStatusChange = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/student-status-changes' ` -Headers $studentHeaders ` -ContentType 'application/json' ` -Body $statusChangeBody $statusChanges = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/student-status-changes' ` -Headers $studentHeaders $openStatusChange = @($statusChanges) | Where-Object { $_.id -eq $createdStatusChange.id } | Select-Object -First 1 } $expectedStudentStatus = $openStatusChange.targetStatus $approvalBody = @{ approved = $true; comment = '端到端审核通过。' } | ConvertTo-Json if ($openStatusChange.state -eq 'Submitted') { $prematureCollegeReview = Invoke-WebRequest ` -Method Post ` -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` -Headers $collegeHeaders ` -ContentType 'application/json' ` -Body $approvalBody ` -SkipHttpErrorCheck if ($prematureCollegeReview.StatusCode -ne 409) { throw 'College reviewer was able to bypass the counselor stage.' } $counselorLoginBody = @{ userName = 'counselor' password = 'Counselor@123456' } | ConvertTo-Json $counselorLogin = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/auth/login' ` -ContentType 'application/json' ` -Body $counselorLoginBody $counselorHeaders = @{ Authorization = "Bearer $($counselorLogin.token)" } Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` -Headers $counselorHeaders ` -ContentType 'application/json' ` -Body $approvalBody | Out-Null $openStatusChange.state = 'CounselorApproved' } if ($openStatusChange.state -eq 'CounselorApproved') { Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` -Headers $collegeHeaders ` -ContentType 'application/json' ` -Body $approvalBody | Out-Null $openStatusChange.state = 'CollegeApproved' } if ($openStatusChange.state -eq 'CollegeApproved') { Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/student-status-changes/$($openStatusChange.id)/review" ` -Headers $headers ` -ContentType 'application/json' ` -Body $approvalBody | Out-Null } $completedStatusChanges = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/student-status-changes' ` -Headers $studentHeaders $completedStatusChange = @($completedStatusChanges) | Where-Object { $_.id -eq $openStatusChange.id } | Select-Object -First 1 $updatedStatusOptions = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/student-status-changes/options' ` -Headers $studentHeaders if ($completedStatusChange.state -ne 'Approved' -or $updatedStatusOptions.status -ne $expectedStudentStatus) { throw 'Student status change was not finalized or did not update the student record.' } $graduationBatches = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/graduation-audits/batches' ` -Headers $headers $graduationBatch = @($graduationBatches) | Where-Object { $_.name -eq '2030届端到端毕业资格审核' } | Select-Object -First 1 if ($null -eq $graduationBatch) { $graduationBatchBody = @{ name = '2030届端到端毕业资格审核' graduationYear = 2030 enrollmentYear = 2026 notes = '用于验证培养方案、成绩快照、人工复核与结果发布。' } | ConvertTo-Json $createdGraduationBatch = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/graduation-audits/batches' ` -Headers $headers ` -ContentType 'application/json' ` -Body $graduationBatchBody Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdGraduationBatch.id)/calculate" ` -Headers $headers | Out-Null $graduationBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdGraduationBatch.id)" ` -Headers $headers $studentGraduationResult = @($graduationBatch.results) | Where-Object { $_.studentNumber -eq '202601001' } | Select-Object -First 1 if ($null -eq $studentGraduationResult) { throw 'Calculated graduation batch does not contain the smoke-test student.' } $overrideDecisionBody = @{ conclusion = 'Eligible' comment = '端到端验证人工复核调整。' } | ConvertTo-Json Invoke-RestMethod ` -Method Put ` -Uri "http://localhost:5255/api/graduation-audits/results/$($studentGraduationResult.id)" ` -Headers $headers ` -ContentType 'application/json' ` -Body $overrideDecisionBody | Out-Null $restoreDecisionBody = @{ conclusion = 'Ineligible' comment = '恢复规则结论后发布测试结果。' } | ConvertTo-Json Invoke-RestMethod ` -Method Put ` -Uri "http://localhost:5255/api/graduation-audits/results/$($studentGraduationResult.id)" ` -Headers $headers ` -ContentType 'application/json' ` -Body $restoreDecisionBody | Out-Null Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($graduationBatch.id)/publish" ` -Headers $headers | Out-Null } else { $graduationBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($graduationBatch.id)" ` -Headers $headers } $graduationBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($graduationBatch.id)" ` -Headers $headers $studentGraduationResult = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/graduation-audits/my-result' ` -Headers $studentHeaders if ($graduationBatch.status -ne 'Published' -or $studentGraduationResult.conclusion -ne 'Ineligible') { throw 'Published graduation result is not visible to the student.' } $graduationBatches = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/graduation-audits/batches' ` -Headers $headers $degreeSourceBatch = @($graduationBatches) | Where-Object { $_.name -eq '2030届学位授予资格演示' } | Select-Object -First 1 if ($null -eq $degreeSourceBatch) { $degreeSourceBody = @{ name = '2030届学位授予资格演示' graduationYear = 2030 enrollmentYear = 2026 notes = '为未关联登录账号的演示学生生成学位授予前置资格。' } | ConvertTo-Json $createdDegreeSource = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/graduation-audits/batches' ` -Headers $headers ` -ContentType 'application/json' ` -Body $degreeSourceBody Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdDegreeSource.id)/calculate" ` -Headers $headers | Out-Null $degreeSourceDetail = Invoke-RestMethod ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdDegreeSource.id)" ` -Headers $headers $degreeSourceResult = @($degreeSourceDetail.results) | Where-Object { $_.studentNumber -eq '202601002' } | Select-Object -First 1 $degreeSourceDecision = @{ conclusion = 'Eligible' comment = '演示学生经校级专项复核符合毕业条件。' } | ConvertTo-Json Invoke-RestMethod ` -Method Put ` -Uri "http://localhost:5255/api/graduation-audits/results/$($degreeSourceResult.id)" ` -Headers $headers ` -ContentType 'application/json' ` -Body $degreeSourceDecision | Out-Null Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/graduation-audits/batches/$($createdDegreeSource.id)/publish" ` -Headers $headers | Out-Null } $degreeBatches = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/degree-awards/batches' ` -Headers $headers $degreeBatch = @($degreeBatches) | Where-Object { $_.name -eq '2030届端到端学位授予审核' } | Select-Object -First 1 if ($null -eq $degreeBatch) { $degreeBatchBody = @{ name = '2030届端到端学位授予审核' graduationYear = 2030 degreeName = '工学学士' minimumGradePoint = 0 notes = '验证毕业资格来源、绩点快照、人工复核与发布锁定。' } | ConvertTo-Json $createdDegreeBatch = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/degree-awards/batches' ` -Headers $headers ` -ContentType 'application/json' ` -Body $degreeBatchBody Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/degree-awards/batches/$($createdDegreeBatch.id)/calculate" ` -Headers $headers | Out-Null $degreeBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/degree-awards/batches/$($createdDegreeBatch.id)" ` -Headers $headers $degreeResult = @($degreeBatch.results) | Select-Object -First 1 if ($null -eq $degreeResult -or $degreeResult.conclusion -ne 'Granted') { throw 'Degree-award calculation did not include the eligible graduated student.' } $degreeRejectBody = @{ conclusion = 'NotGranted' comment = '端到端验证学位结论人工调整。' } | ConvertTo-Json Invoke-RestMethod ` -Method Put ` -Uri "http://localhost:5255/api/degree-awards/results/$($degreeResult.id)" ` -Headers $collegeHeaders ` -ContentType 'application/json' ` -Body $degreeRejectBody | Out-Null $degreeRestoreBody = @{ conclusion = 'Granted' comment = '恢复规则结论并完成授予发布。' } | ConvertTo-Json Invoke-RestMethod ` -Method Put ` -Uri "http://localhost:5255/api/degree-awards/results/$($degreeResult.id)" ` -Headers $headers ` -ContentType 'application/json' ` -Body $degreeRestoreBody | Out-Null Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/degree-awards/batches/$($degreeBatch.id)/publish" ` -Headers $headers | Out-Null } $degreeBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/degree-awards/batches/$($degreeBatch.id)" ` -Headers $headers $collegeDegreeBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/degree-awards/batches/$($degreeBatch.id)" ` -Headers $collegeHeaders if ($degreeBatch.status -ne 'Published' -or @($degreeBatch.results).Count -lt 1 -or @($collegeDegreeBatch.results).Count -lt 1) { throw 'Published degree-award results are not visible in the authorized college scope.' } $clearanceBatches = Invoke-RestMethod ` -Uri 'http://localhost:5255/api/graduation-clearance/batches' ` -Headers $headers $clearanceBatch = @($clearanceBatches) | Where-Object { $_.name -eq '2030届端到端毕业离校手续' } | Select-Object -First 1 if ($null -eq $clearanceBatch) { $clearanceBatchBody = @{ name = '2030届端到端毕业离校手续' graduationYear = 2030 notes = '验证校级、学院、辅导员分工办理与批次关闭。' items = @( @{ code = 'FINANCE' name = '财务费用结清' responsibleUnit = '财务处' responsibleRole = 'AcademicAdmin' isRequired = $true }, @{ code = 'COLLEGE' name = '学院材料归档' responsibleUnit = '计算机学院' responsibleRole = 'CollegeAdmin' isRequired = $true }, @{ code = 'DORM' name = '宿舍退宿确认' responsibleUnit = '学生工作办公室' responsibleRole = 'Counselor' isRequired = $true } ) } | ConvertTo-Json -Depth 5 $createdClearanceBatch = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/graduation-clearance/batches' ` -Headers $headers ` -ContentType 'application/json' ` -Body $clearanceBatchBody Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/graduation-clearance/batches/$($createdClearanceBatch.id)/generate" ` -Headers $headers | Out-Null $clearanceBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/graduation-clearance/batches/$($createdClearanceBatch.id)" ` -Headers $headers if (@($clearanceBatch.records).Count -ne 3) { throw 'Graduation-clearance records were not generated for each configured item.' } $academicClearanceRecord = @($clearanceBatch.records) | Where-Object { $_.responsibleRole -eq 'AcademicAdmin' } | Select-Object -First 1 $wrongRoleResponse = Invoke-WebRequest ` -Method Put ` -Uri "http://localhost:5255/api/graduation-clearance/records/$($academicClearanceRecord.id)" ` -Headers $collegeHeaders ` -ContentType 'application/json' ` -Body (@{ status = 'Completed'; notes = '越权测试。' } | ConvertTo-Json) ` -SkipHttpErrorCheck if ($wrongRoleResponse.StatusCode -ne 403) { throw 'College role was able to complete an academic-level clearance item.' } $counselorLoginBody = @{ userName = 'counselor' password = 'Counselor@123456' } | ConvertTo-Json $counselorLogin = Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:5255/api/auth/login' ` -ContentType 'application/json' ` -Body $counselorLoginBody $counselorHeaders = @{ Authorization = "Bearer $($counselorLogin.token)" } foreach ($record in @($clearanceBatch.records)) { $recordHeaders = switch ($record.responsibleRole) { 'CollegeAdmin' { $collegeHeaders } 'Counselor' { $counselorHeaders } default { $headers } } $recordBody = @{ status = 'Completed' notes = "$($record.responsibleUnit)端到端办理完成。" } | ConvertTo-Json Invoke-RestMethod ` -Method Put ` -Uri "http://localhost:5255/api/graduation-clearance/records/$($record.id)" ` -Headers $recordHeaders ` -ContentType 'application/json' ` -Body $recordBody | Out-Null } Invoke-RestMethod ` -Method Post ` -Uri "http://localhost:5255/api/graduation-clearance/batches/$($clearanceBatch.id)/close" ` -Headers $headers | Out-Null } $clearanceBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/graduation-clearance/batches/$($clearanceBatch.id)" ` -Headers $headers $collegeClearanceBatch = Invoke-RestMethod ` -Uri "http://localhost:5255/api/graduation-clearance/batches/$($clearanceBatch.id)" ` -Headers $collegeHeaders if ($clearanceBatch.status -ne 'Closed' -or @($clearanceBatch.records | Where-Object { $_.status -eq 'Pending' }).Count -ne 0 -or @($collegeClearanceBatch.records).Count -lt 1) { throw 'Graduation-clearance batch was not closed or is missing from college scope.' } $frontend = Invoke-WebRequest -Uri 'http://localhost:5255/' -TimeoutSec 5 $spaFallback = Invoke-WebRequest -Uri 'http://localhost:5255/base-data' -TimeoutSec 5 $unknownApiParameters = @{ Uri = 'http://localhost:5255/api/does-not-exist' SkipHttpErrorCheck = $true TimeoutSec = 5 } $unknownApi = Invoke-WebRequest @unknownApiParameters [pscustomobject]@{ Health = $health.status Database = $health.database User = $login.user.displayName Term = $dashboard.currentTerm.name Campuses = @($campuses).Count CounselorAssignments = $assignedClassCount Teachers = $teachers.total Students = $students.total Courses = $courses.total CourseCategories = @($courseCategories).Count CourseExcelRows = $courseImport.total Plans = $curriculumPlans.total PlanModules = if ($null -ne $curriculumDetail) { @($curriculumDetail.modules).Count } else { 0 } TeachingTasks = $teachingTasks.total Schedules = @($schedulePlans).Count ScheduleEntries = if ($null -ne $scheduleDetail) { @($scheduleDetail.entries).Count } else { 0 } SelectionRounds = @($selectionRounds).Count SelectionOfferings = @($selectionOfferings).Count StudentEnrollments = $activeEnrollments.Count RosterStudents = $selectionRoster.enrolledCount GradeSheets = @($gradeTasks).Count GradeRecords = @($gradeDetail.sheet.records).Count TranscriptRecords = @($studentTranscript.records).Count ExamSessions = @($examDetail.sessions).Count ExamRoster = @($examRoster.students).Count StatusChanges = @($completedStatusChanges).Count StudentStatus = $updatedStatusOptions.status GraduationResults = @($graduationBatch.results).Count GraduationConclusion = $studentGraduationResult.conclusion DegreeResults = @($degreeBatch.results).Count DegreeConclusion = $degreeBatch.results[0].conclusion ClearanceRecords = @($clearanceBatch.records).Count ClearanceStatus = $clearanceBatch.status AccessUpdate = $true ScopeChecks = $scopeChecks -join ', ' StaticIndex = $frontend.Content.Contains('明序教务管理系统') SpaFallback = $spaFallback.StatusCode ApiNotFound = $unknownApi.StatusCode } | Format-List } finally { if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force } Remove-Item -LiteralPath $courseTemplatePath -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $courseExportPath -Force -ErrorAction SilentlyContinue }