生产环境
This commit is contained in:
1 parent
1da6543a38
commit
a576cc815d
30 files changed
+3325
-4129
No files matched your search
@@ -12,17 +12,14 @@
|
|||||||
|
|
||||||
## 本地开发:热更新模式
|
## 本地开发:热更新模式
|
||||||
|
|
||||||
本地开发固定使用 SQLite。首次启动会自动创建 `src/Jiaowu.Api/data/jiaowu-dev.sqlite` 并写入演示组织数据。
|
本地开发固定使用 SQLite。首次启动会自动创建空的
|
||||||
|
`src/Jiaowu.Api/data/jiaowu-dev.sqlite`,只初始化系统角色和课程分类,不再写入
|
||||||
Development 环境还会按业务编码幂等补齐一套大规模测试数据:16 个常见学院或教学单位、52 个常见本科专业、每个专业 2 个 2026 级行政班、每班 35 名学生、每学院 8 名教师,以及覆盖公共必修、公共选修、专业必修、专业选修和实践教学的 150 余门课程。测试教师同时生成当前学期已审核通过的授课资格;重复启动不会重复累加数据,也不会覆盖已有记录。
|
演示组织、人员、课程、业务记录或内置测试账号。
|
||||||
|
|
||||||
只执行数据库迁移和测试数据补齐、不启动 Web 服务时,可以运行:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet run --project src/Jiaowu.Api -- --seed-only
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
$env:SeedAdmin__UserName = 'admin'
|
||||||
|
$env:SeedAdmin__Password = '请替换为本机开发密码'
|
||||||
|
$env:SeedAdmin__DisplayName = '系统管理员'
|
||||||
dotnet run --project src/Jiaowu.Api
|
dotnet run --project src/Jiaowu.Api
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -34,17 +31,8 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
访问 `http://localhost:5173`。本地开发会自动创建以下分级权限账号:
|
访问 `http://localhost:5173`。首次创建管理员后可以清除三个
|
||||||
|
`SeedAdmin__*` 环境变量;后续账号和基础数据均通过管理界面维护。
|
||||||
| 数据范围 | 账号 | 密码 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 超级管理员 | `admin` | `Admin@123456` |
|
|
||||||
| 校级教务 | `academic` | `Academic@123456` |
|
|
||||||
| 学院教务 | `college` | `College@123456` |
|
|
||||||
| 所带班级 | `counselor` | `Counselor@123456` |
|
|
||||||
| 教师本人 | `teacher` | `Teacher@123456` |
|
|
||||||
| 学生本人 | `student` | `Student@123456` |
|
|
||||||
| 领导查看 | `leader` | `Leader@123456` |
|
|
||||||
|
|
||||||
## 本地开发:单服务模式
|
## 本地开发:单服务模式
|
||||||
|
|
||||||
@@ -57,36 +45,73 @@ dotnet run --project src/Jiaowu.Api
|
|||||||
|
|
||||||
访问 `http://localhost:5255`。`/api` 和静态页面由同一个 ASP.NET Core 服务提供,`/base-data` 等前端路由刷新时也会回退到 `index.html`。
|
访问 `http://localhost:5255`。`/api` 和静态页面由同一个 ASP.NET Core 服务提供,`/base-data` 等前端路由刷新时也会回退到 `index.html`。
|
||||||
|
|
||||||
## MySQL 部署
|
## MySQL 8.4 生产部署
|
||||||
|
|
||||||
非 Development 环境只允许使用 MySQL。`dotnet publish` 会自动执行 `npm ci` 和 `npm run build`,并将 Vue 静态文件放入发布目录的 `wwwroot`:
|
非 Development 环境只允许使用 MySQL。`dotnet publish` 会自动执行 `npm ci` 和 `npm run build`,并将 Vue 静态文件放入发布目录的 `wwwroot`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:ASPNETCORE_ENVIRONMENT = 'Production'
|
$env:ASPNETCORE_ENVIRONMENT = 'Production'
|
||||||
$env:Database__Provider = 'MySql'
|
$env:Database__Provider = 'MySql'
|
||||||
$env:ConnectionStrings__MySql = 'Server=127.0.0.1;Port=3306;Database=jiaowu;User=YOUR_USER;Password=YOUR_PASSWORD;'
|
$env:ConnectionStrings__MySql = 'Server=db.example.edu.cn;Port=3306;Database=jiaowu;User=YOUR_USER;Password=YOUR_PASSWORD;SslMode=VerifyFull;SslCa=C:\certs\mysql-ca.pem;'
|
||||||
$env:Jwt__Key = '至少32字节的随机生产密钥'
|
$env:Jwt__Key = '至少32字节的随机生产密钥'
|
||||||
|
$env:AllowedHosts = 'jiaowu.example.edu.cn'
|
||||||
dotnet publish src/Jiaowu.Api -c Release -o .artifacts/publish
|
dotnet publish src/Jiaowu.Api -c Release -o .artifacts/publish
|
||||||
.artifacts/publish/Jiaowu.Api.exe
|
|
||||||
```
|
```
|
||||||
|
|
||||||
如需在特殊流水线中跳过自动前端构建,可传入 `-p:BuildFrontendOnPublish=false`。
|
如需在特殊流水线中跳过自动前端构建,可传入 `-p:BuildFrontendOnPublish=false`。
|
||||||
|
|
||||||
生产环境不会创建默认管理员。首次部署前可以临时配置 `SeedAdmin__UserName`、`SeedAdmin__Password` 和 `SeedAdmin__DisplayName`,账号创建后移除这些配置。
|
数据库应明确使用 `utf8mb4`;MySQL 8.4 的默认排序规则为
|
||||||
|
`utf8mb4_0900_ai_ci`。新建数据库时可执行:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE `jiaowu`
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_0900_ai_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
发布前使用具备 DDL 权限的迁移账号单独执行迁移:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& '.artifacts\publish\Jiaowu.Api.exe' --migrate-only
|
||||||
|
```
|
||||||
|
|
||||||
|
迁移成功后,改用仅具备应用所需 DML 权限的运行账号并启动服务:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& '.artifacts\publish\Jiaowu.Api.exe'
|
||||||
|
```
|
||||||
|
|
||||||
|
`Database:ApplyMigrationsOnStartup` 默认关闭。普通启动会检查待执行迁移并在架构落后时
|
||||||
|
直接失败,避免多实例同时执行 DDL。只有明确接受启动期 DDL 风险的单实例部署才应将
|
||||||
|
`Database__ApplyMigrationsOnStartup` 设为 `true`。
|
||||||
|
|
||||||
|
生产环境不会创建默认管理员。首次部署可以临时配置
|
||||||
|
`SeedAdmin__UserName`、`SeedAdmin__Password` 和 `SeedAdmin__DisplayName`,
|
||||||
|
账号创建后立即移除这些配置。
|
||||||
|
|
||||||
生产数据库使用 MySQL 专用 EF Core 迁移。部署前先恢复仓库工具并检查迁移:
|
生产数据库使用 MySQL 专用 EF Core 迁移。部署前先恢复仓库工具并检查迁移:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet tool restore
|
dotnet tool restore
|
||||||
dotnet ef migrations list --project src/Jiaowu.Api --startup-project src/Jiaowu.Api
|
dotnet ef migrations list --no-connect --project src/Jiaowu.Api --startup-project src/Jiaowu.Api
|
||||||
```
|
```
|
||||||
|
|
||||||
应用启动时会自动执行尚未应用的 MySQL 迁移。SQLite 只用于本地开发:新库通过 `EnsureCreated` 建立,已有开发库通过轻量、版本化的本地升级脚本补齐结构,不需要手动删除数据文件。SQLite 文件不能用于生产。
|
不要使用当前提供程序生成的 `dotnet ef migrations script --idempotent` 作为 MySQL
|
||||||
|
部署脚本;其条件块不是 MySQL 8.4 可直接执行的语法。应使用上述
|
||||||
|
`--migrate-only` 入口,或在明确知道目标迁移状态时生成非幂等脚本并先做备份。
|
||||||
|
MySQL 的 DDL 会隐式提交,迁移不能依赖外层事务整体回滚。
|
||||||
|
|
||||||
|
SQLite 只用于本地开发:新库通过 `EnsureCreated` 建立,已有开发库通过轻量、版本化的
|
||||||
|
本地升级脚本补齐结构,不需要手动删除数据文件。SQLite 文件不能用于生产。
|
||||||
|
|
||||||
|
服务探针:
|
||||||
|
|
||||||
|
- `/health/live`:只检查进程存活。
|
||||||
|
- `/health`、`/health/ready`:实际检查数据库连接,失败时返回 HTTP 503。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet test Jiaowu.slnx
|
dotnet test Jiaowu.slnx
|
||||||
npm --prefix web run build
|
npm --prefix web run build
|
||||||
pwsh.exe -NoLogo -NoProfile -NonInteractive -File scripts/smoke-test.ps1
|
|
||||||
```
|
```
|
||||||
@@ -1,831 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$env:ASPNETCORE_ENVIRONMENT = 'Development'
|
|
||||||
$env:ASPNETCORE_URLS = 'http://localhost:5256'
|
|
||||||
|
|
||||||
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
|
||||||
$publishRoot = Join-Path $workspaceRoot '.artifacts/publish'
|
|
||||||
$process = Start-Process `
|
|
||||||
-FilePath (Join-Path $publishRoot 'Jiaowu.Api.exe') `
|
|
||||||
-WorkingDirectory $publishRoot `
|
|
||||||
-WindowStyle Hidden `
|
|
||||||
-PassThru
|
|
||||||
|
|
||||||
$process.Id
|
|
||||||
Loaded 3 of 30 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user