From 7f757b6a633da6f91781f621d462093eee674c52 Mon Sep 17 00:00:00 2001 From: tonytech Date: Sat, 28 Feb 2026 17:51:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4=EF=BC=9A?= =?UTF-8?q?=E5=B9=BC=E5=84=BF=E5=9B=AD=E9=98=85=E8=AF=BB=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E4=B8=89=E7=AB=AF=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reading-platform-backend:NestJS 后端 - reading-platform-frontend:Vue3 前端 - reading-platform-java:Spring Boot 服务端 --- .gitignore | 50 + reading-platform-backend/.env.development | 6 + reading-platform-backend/nest-cli.json | 10 + reading-platform-backend/package-lock.json | 11688 ++++++++++++++++ reading-platform-backend/package.json | 66 + .../prisma/migrate-v1-to-v2.ts | 385 + .../20260210055321_init/migration.sql | 262 + .../20260210092744_init/migration.sql | 323 + .../migration.sql | 27 + .../20260227180846_init/migration.sql | 634 + .../prisma/migrations/migration_lock.toml | 3 + .../prisma/schema-v2.prisma | 1129 ++ reading-platform-backend/prisma/schema.prisma | 1073 ++ reading-platform-backend/prisma/seed.ts | 431 + .../scripts/create-test-parent.ts | 99 + reading-platform-backend/src/app.module.js | 92 + reading-platform-backend/src/app.module.ts | 68 + .../common/filters/http-exception.filter.ts | 44 + .../src/database/prisma.module.js | 67 + .../src/database/prisma.module.ts | 9 + .../src/database/prisma.service.js | 203 + .../src/database/prisma.service.ts | 38 + reading-platform-backend/src/main.js | 84 + reading-platform-backend/src/main.ts | 74 + .../admin/admin-settings.controller.ts | 67 + .../modules/admin/admin-settings.service.ts | 107 + .../modules/admin/admin-stats.controller.ts | 40 + .../src/modules/admin/admin-stats.service.ts | 236 + .../src/modules/admin/admin.module.ts | 14 + .../src/modules/auth/auth.controller.js | 133 + .../src/modules/auth/auth.controller.ts | 27 + .../src/modules/auth/auth.module.js | 128 + .../src/modules/auth/auth.module.ts | 29 + .../src/modules/auth/auth.service.js | 288 + .../src/modules/auth/auth.service.ts | 298 + .../src/modules/auth/dto/login.dto.js | 71 + .../src/modules/auth/dto/login.dto.ts | 16 + .../modules/auth/strategies/jwt.strategy.js | 140 + .../modules/auth/strategies/jwt.strategy.ts | 33 + .../src/modules/common/common.module.ts | 13 + .../decorators/log-operation.decorator.ts | 17 + .../common/decorators/roles.decorator.ts | 4 + .../modules/common/guards/jwt-auth.guard.ts | 24 + .../src/modules/common/guards/roles.guard.ts | 25 + .../common/interceptors/log.interceptor.ts | 135 + .../common/operation-log.controller.ts | 56 + .../modules/common/operation-log.service.ts | 171 + .../course-lesson/course-lesson.controller.ts | 198 + .../course-lesson/course-lesson.module.ts | 10 + .../course-lesson/course-lesson.service.ts | 311 + .../course-package.controller.ts | 164 + .../course-package/course-package.module.ts | 10 + .../course-package/course-package.service.ts | 372 + .../course/course-validation.service.ts | 270 + .../src/modules/course/course.controller.ts | 161 + .../src/modules/course/course.module.ts | 13 + .../src/modules/course/course.service.ts | 1033 ++ .../src/modules/export/export.controller.ts | 88 + .../src/modules/export/export.module.ts | 10 + .../src/modules/export/export.service.ts | 266 + .../file-upload/file-upload.controller.ts | 92 + .../modules/file-upload/file-upload.module.ts | 10 + .../file-upload/file-upload.service.ts | 203 + .../modules/growth/dto/create-growth.dto.ts | 81 + .../src/modules/growth/growth.controller.ts | 117 + .../src/modules/growth/growth.module.ts | 10 + .../src/modules/growth/growth.service.ts | 637 + .../modules/lesson/dto/create-lesson.dto.ts | 13 + .../modules/lesson/dto/finish-lesson.dto.ts | 19 + .../src/modules/lesson/lesson.controller.ts | 133 + .../src/modules/lesson/lesson.module.ts | 10 + .../src/modules/lesson/lesson.service.ts | 905 ++ .../notification/notification.controller.ts | 151 + .../notification/notification.module.ts | 21 + .../notification/notification.service.ts | 169 + .../schedule-notification.service.ts | 333 + .../src/modules/parent/parent.controller.ts | 71 + .../src/modules/parent/parent.module.ts | 10 + .../src/modules/parent/parent.service.ts | 309 + .../resource/dto/create-resource.dto.ts | 144 + .../modules/resource/resource.controller.ts | 90 + .../src/modules/resource/resource.module.ts | 10 + .../src/modules/resource/resource.service.ts | 357 + .../school-course/school-course.controller.ts | 227 + .../school-course/school-course.module.ts | 10 + .../school-course/school-course.service.ts | 396 + .../modules/school/dto/class-teacher.dto.ts | 40 + .../modules/school/dto/create-class.dto.ts | 31 + .../modules/school/dto/create-student.dto.ts | 56 + .../modules/school/dto/create-teacher.dto.ts | 51 + .../modules/school/dto/import-students.dto.ts | 7 + .../src/modules/school/dto/schedule.dto.ts | 166 + .../src/modules/school/export.controller.ts | 109 + .../src/modules/school/export.service.ts | 276 + .../src/modules/school/package.controller.ts | 100 + .../src/modules/school/school.controller.ts | 375 + .../src/modules/school/school.module.ts | 17 + .../src/modules/school/school.service.ts | 2414 ++++ .../src/modules/school/settings.controller.ts | 39 + .../src/modules/school/settings.service.ts | 93 + .../src/modules/school/stats.controller.ts | 79 + .../src/modules/school/stats.service.ts | 482 + .../src/modules/task/dto/create-task.dto.ts | 187 + .../src/modules/task/task.controller.ts | 256 + .../src/modules/task/task.module.ts | 12 + .../src/modules/task/task.service.ts | 930 ++ .../teacher-course.controller.ts | 128 + .../teacher-course/teacher-course.module.ts | 10 + .../teacher-course/teacher-course.service.ts | 1140 ++ .../src/modules/tenant/dto/tenant.dto.ts | 158 + .../src/modules/tenant/tenant.controller.js | 96 + .../src/modules/tenant/tenant.controller.ts | 74 + .../src/modules/tenant/tenant.module.js | 71 + .../src/modules/tenant/tenant.module.ts | 12 + .../src/modules/tenant/tenant.service.js | 192 + .../src/modules/tenant/tenant.service.ts | 422 + .../src/modules/theme/theme.controller.ts | 58 + .../src/modules/theme/theme.module.ts | 10 + .../src/modules/theme/theme.service.ts | 73 + reading-platform-backend/start-backend.sh | 18 + reading-platform-backend/tsconfig.build.json | 10 + reading-platform-backend/tsconfig.json | 27 + reading-platform-frontend/.env.development | 3 + reading-platform-frontend/index.html | 13 + reading-platform-frontend/package-lock.json | 3912 ++++++ reading-platform-frontend/package.json | 42 + reading-platform-frontend/public/logo.png | Bin 0 -> 135769 bytes reading-platform-frontend/src/App.vue | 27 + reading-platform-frontend/src/api/admin.ts | 215 + reading-platform-frontend/src/api/auth.ts | 51 + reading-platform-frontend/src/api/course.ts | 211 + reading-platform-frontend/src/api/file.ts | 134 + reading-platform-frontend/src/api/growth.ts | 130 + reading-platform-frontend/src/api/index.ts | 94 + reading-platform-frontend/src/api/lesson.ts | 159 + reading-platform-frontend/src/api/package.ts | 137 + reading-platform-frontend/src/api/parent.ts | 152 + reading-platform-frontend/src/api/resource.ts | 135 + .../src/api/school-course.ts | 178 + reading-platform-frontend/src/api/school.ts | 1003 ++ reading-platform-frontend/src/api/task.ts | 175 + reading-platform-frontend/src/api/teacher.ts | 660 + reading-platform-frontend/src/api/theme.ts | 58 + .../src/auto-imports.d.ts | 90 + reading-platform-frontend/src/components.d.ts | 89 + .../src/components/FilePreviewModal.vue | 379 + .../src/components/NotificationBell.vue | 260 + .../src/components/course/FileUploader.vue | 296 + .../components/course/LessonConfigPanel.vue | 317 + .../components/course/LessonStepsEditor.vue | 300 + reading-platform-frontend/src/main.ts | 15 + reading-platform-frontend/src/router/index.ts | 480 + reading-platform-frontend/src/stores/user.ts | 92 + .../src/utils/tagMaps.ts | 389 + .../src/views/NotFoundView.vue | 41 + .../src/views/admin/DashboardView.vue | 697 + .../src/views/admin/LayoutView.vue | 363 + .../src/views/admin/SettingsView.vue | 303 + .../views/admin/courses/CourseDetailView.vue | 1270 ++ .../views/admin/courses/CourseEditView.vue | 527 + .../views/admin/courses/CourseListView.vue | 560 + .../views/admin/courses/CourseReviewView.vue | 387 + .../views/admin/courses/CourseStatsView.vue | 318 + .../courses/components/Step1BasicInfo.vue | 295 + .../courses/components/Step2CourseIntro.vue | 261 + .../courses/components/Step3ScheduleRef.vue | 239 + .../courses/components/Step4IntroLesson.vue | 221 + .../components/Step5CollectiveLesson.vue | 226 + .../courses/components/Step6DomainLessons.vue | 375 + .../courses/components/Step7Environment.vue | 113 + .../admin/packages/PackageDetailView.vue | 177 + .../views/admin/packages/PackageEditView.vue | 285 + .../views/admin/packages/PackageListView.vue | 233 + .../admin/resources/ResourceListView.vue | 734 + .../views/admin/tenants/TenantListView.vue | 707 + .../src/views/admin/themes/ThemeListView.vue | 176 + .../src/views/auth/LoginView.vue | 428 + .../src/views/parent/DashboardView.vue | 592 + .../src/views/parent/LayoutView.vue | 640 + .../parent/children/ChildProfileView.vue | 387 + .../views/parent/children/ChildrenView.vue | 407 + .../views/parent/growth/GrowthRecordView.vue | 330 + .../parent/lessons/LessonHistoryView.vue | 353 + .../src/views/parent/tasks/TaskListView.vue | 362 + .../src/views/school/DashboardView.vue | 1177 ++ .../src/views/school/LayoutView.vue | 468 + .../src/views/school/PackageView.vue | 782 ++ .../src/views/school/ReportView.vue | 1127 ++ .../views/school/classes/ClassListView.vue | 1352 ++ .../views/school/courses/CourseDetailView.vue | 1343 ++ .../views/school/courses/CourseListView.vue | 1039 ++ .../views/school/feedback/FeedbackView.vue | 940 ++ .../views/school/growth/GrowthRecordView.vue | 812 ++ .../views/school/parents/ParentListView.vue | 1248 ++ .../views/school/schedule/CalendarView.vue | 417 + .../views/school/schedule/ScheduleView.vue | 964 ++ .../views/school/schedule/TimetableView.vue | 739 + .../school-courses/SchoolCourseDetailView.vue | 657 + .../school-courses/SchoolCourseEditView.vue | 162 + .../school-courses/SchoolCourseListView.vue | 731 + .../school/settings/OperationLogView.vue | 328 + .../views/school/settings/SettingsView.vue | 329 + .../views/school/students/StudentListView.vue | 1473 ++ .../src/views/school/tasks/TaskListView.vue | 743 + .../views/school/tasks/TaskTemplateView.vue | 474 + .../views/school/teachers/TeacherListView.vue | 841 ++ .../src/views/teacher/DashboardView.vue | 1331 ++ .../src/views/teacher/LayoutView.vue | 348 + .../views/teacher/classes/ClassListView.vue | 839 ++ .../teacher/classes/ClassStudentsView.vue | 473 + .../teacher/courses/CourseDetailView.vue | 1219 ++ .../views/teacher/courses/CourseListView.vue | 688 + .../views/teacher/courses/PrepareModeView.vue | 1426 ++ .../views/teacher/feedback/FeedbackView.vue | 850 ++ .../views/teacher/growth/GrowthRecordView.vue | 886 ++ .../views/teacher/lessons/BroadcastView.vue | 234 + .../views/teacher/lessons/LessonListView.vue | 615 + .../teacher/lessons/LessonRecordsView.vue | 622 + .../src/views/teacher/lessons/LessonView.vue | 1462 ++ .../teacher/lessons/components/KidsMode.vue | 1065 ++ .../components/viewers/AudioPlayer.vue | 559 + .../components/viewers/EbookViewer.vue | 684 + .../components/viewers/SlidesViewer.vue | 624 + .../components/viewers/VideoPlayer.vue | 811 ++ .../views/teacher/schedule/ScheduleView.vue | 684 + .../school-courses/SchoolCourseDetailView.vue | 110 + .../school-courses/SchoolCourseEditView.vue | 162 + .../school-courses/SchoolCourseListView.vue | 130 + .../src/views/teacher/tasks/TaskListView.vue | 1023 ++ reading-platform-frontend/src/vite-env.d.ts | 11 + reading-platform-frontend/start-frontend.sh | 18 + reading-platform-frontend/tsconfig.json | 31 + reading-platform-frontend/tsconfig.node.json | 10 + reading-platform-frontend/vite.config.ts | 72 + reading-platform-java/README.md | 170 + reading-platform-java/pom.xml | 128 + .../platform/ReadingPlatformApplication.java | 18 + .../common/annotation/RequireRole.java | 17 + .../platform/common/aspect/RoleAspect.java | 46 + .../common/config/MybatisPlusConfig.java | 42 + .../platform/common/config/OpenApiConfig.java | 37 + .../common/config/SecurityConfig.java | 77 + .../platform/common/enums/CourseStatus.java | 32 + .../platform/common/enums/ErrorCode.java | 51 + .../reading/platform/common/enums/Gender.java | 31 + .../platform/common/enums/LessonStatus.java | 33 + .../platform/common/enums/TaskStatus.java | 33 + .../platform/common/enums/TaskType.java | 33 + .../platform/common/enums/TenantStatus.java | 32 + .../platform/common/enums/UserRole.java | 33 + .../common/exception/BusinessException.java | 51 + .../exception/GlobalExceptionHandler.java | 106 + .../platform/common/response/PageResult.java | 42 + .../platform/common/response/Result.java | 50 + .../security/JwtAuthenticationFilter.java | 61 + .../platform/common/security/JwtPayload.java | 27 + .../common/security/JwtTokenProvider.java | 97 + .../common/security/SecurityUtils.java | 60 + .../platform/common/util/PageUtils.java | 25 + .../platform/controller/AuthController.java | 43 + .../admin/AdminCourseController.java | 80 + .../admin/AdminTenantController.java | 72 + .../parent/ParentChildController.java | 35 + .../parent/ParentGrowthController.java | 73 + .../parent/ParentNotificationController.java | 61 + .../parent/ParentTaskController.java | 50 + .../school/SchoolClassController.java | 80 + .../school/SchoolGrowthController.java | 65 + .../school/SchoolParentController.java | 88 + .../school/SchoolStudentController.java | 64 + .../school/SchoolTaskController.java | 66 + .../school/SchoolTeacherController.java | 70 + .../teacher/TeacherCourseController.java | 49 + .../teacher/TeacherGrowthController.java | 64 + .../teacher/TeacherLessonController.java | 89 + .../TeacherNotificationController.java | 61 + .../teacher/TeacherTaskController.java | 65 + .../dto/request/ClassCreateRequest.java | 24 + .../dto/request/ClassUpdateRequest.java | 25 + .../dto/request/CourseCreateRequest.java | 141 + .../dto/request/CourseUpdateRequest.java | 140 + .../request/GrowthRecordCreateRequest.java | 39 + .../request/GrowthRecordUpdateRequest.java | 31 + .../dto/request/LessonCreateRequest.java | 46 + .../dto/request/LessonUpdateRequest.java | 34 + .../platform/dto/request/LoginRequest.java | 22 + .../dto/request/ParentCreateRequest.java | 32 + .../dto/request/ParentUpdateRequest.java | 28 + .../dto/request/StudentCreateRequest.java | 38 + .../dto/request/StudentUpdateRequest.java | 42 + .../dto/request/TaskCreateRequest.java | 42 + .../dto/request/TaskUpdateRequest.java | 33 + .../dto/request/TeacherCreateRequest.java | 35 + .../dto/request/TeacherUpdateRequest.java | 31 + .../dto/request/TenantCreateRequest.java | 45 + .../dto/request/TenantUpdateRequest.java | 42 + .../platform/dto/response/CourseResponse.java | 205 + .../platform/dto/response/LoginResponse.java | 34 + .../platform/dto/response/TenantResponse.java | 53 + .../dto/response/UserInfoResponse.java | 40 + .../reading/platform/entity/AdminUser.java | 43 + .../reading/platform/entity/ClassTeacher.java | 30 + .../com/reading/platform/entity/Clazz.java | 39 + .../com/reading/platform/entity/Course.java | 126 + .../platform/entity/CourseActivity.java | 41 + .../platform/entity/CourseResource.java | 45 + .../reading/platform/entity/CourseScript.java | 35 + .../platform/entity/CourseScriptPage.java | 41 + .../platform/entity/CourseVersion.java | 35 + .../reading/platform/entity/GrowthRecord.java | 48 + .../com/reading/platform/entity/Lesson.java | 51 + .../platform/entity/LessonFeedback.java | 35 + .../reading/platform/entity/Notification.java | 44 + .../reading/platform/entity/OperationLog.java | 41 + .../com/reading/platform/entity/Parent.java | 47 + .../platform/entity/ParentStudent.java | 32 + .../reading/platform/entity/ResourceItem.java | 43 + .../platform/entity/ResourceLibrary.java | 35 + .../reading/platform/entity/SchedulePlan.java | 40 + .../platform/entity/ScheduleTemplate.java | 37 + .../com/reading/platform/entity/Student.java | 50 + .../platform/entity/StudentClassHistory.java | 35 + .../platform/entity/StudentRecord.java | 37 + .../platform/entity/SystemSetting.java | 35 + .../java/com/reading/platform/entity/Tag.java | 35 + .../com/reading/platform/entity/Task.java | 50 + .../platform/entity/TaskCompletion.java | 43 + .../reading/platform/entity/TaskTarget.java | 30 + .../reading/platform/entity/TaskTemplate.java | 39 + .../com/reading/platform/entity/Teacher.java | 49 + .../com/reading/platform/entity/Tenant.java | 49 + .../reading/platform/entity/TenantCourse.java | 33 + .../platform/mapper/AdminUserMapper.java | 9 + .../platform/mapper/ClassTeacherMapper.java | 9 + .../reading/platform/mapper/ClazzMapper.java | 9 + .../platform/mapper/CourseActivityMapper.java | 9 + .../reading/platform/mapper/CourseMapper.java | 9 + .../platform/mapper/CourseResourceMapper.java | 9 + .../platform/mapper/CourseScriptMapper.java | 9 + .../mapper/CourseScriptPageMapper.java | 9 + .../platform/mapper/CourseVersionMapper.java | 9 + .../platform/mapper/GrowthRecordMapper.java | 9 + .../platform/mapper/LessonFeedbackMapper.java | 9 + .../reading/platform/mapper/LessonMapper.java | 9 + .../platform/mapper/NotificationMapper.java | 9 + .../platform/mapper/OperationLogMapper.java | 9 + .../reading/platform/mapper/ParentMapper.java | 9 + .../platform/mapper/ParentStudentMapper.java | 9 + .../platform/mapper/ResourceItemMapper.java | 9 + .../mapper/ResourceLibraryMapper.java | 9 + .../platform/mapper/SchedulePlanMapper.java | 9 + .../mapper/ScheduleTemplateMapper.java | 9 + .../mapper/StudentClassHistoryMapper.java | 9 + .../platform/mapper/StudentMapper.java | 9 + .../platform/mapper/StudentRecordMapper.java | 9 + .../platform/mapper/SystemSettingMapper.java | 9 + .../reading/platform/mapper/TagMapper.java | 9 + .../platform/mapper/TaskCompletionMapper.java | 9 + .../reading/platform/mapper/TaskMapper.java | 9 + .../platform/mapper/TaskTargetMapper.java | 9 + .../platform/mapper/TaskTemplateMapper.java | 9 + .../platform/mapper/TeacherMapper.java | 9 + .../platform/mapper/TenantCourseMapper.java | 9 + .../reading/platform/mapper/TenantMapper.java | 9 + .../reading/platform/service/AuthService.java | 18 + .../platform/service/ClassService.java | 31 + .../platform/service/CourseService.java | 33 + .../platform/service/GrowthRecordService.java | 29 + .../platform/service/LessonService.java | 36 + .../platform/service/NotificationService.java | 28 + .../platform/service/ParentService.java | 31 + .../platform/service/StudentService.java | 29 + .../reading/platform/service/TaskService.java | 31 + .../platform/service/TeacherService.java | 25 + .../platform/service/TenantService.java | 28 + .../service/impl/AuthServiceImpl.java | 331 + .../service/impl/ClassServiceImpl.java | 175 + .../service/impl/CourseServiceImpl.java | 312 + .../service/impl/GrowthRecordServiceImpl.java | 149 + .../service/impl/LessonServiceImpl.java | 177 + .../service/impl/NotificationServiceImpl.java | 134 + .../service/impl/ParentServiceImpl.java | 162 + .../service/impl/StudentServiceImpl.java | 190 + .../service/impl/TaskServiceImpl.java | 212 + .../service/impl/TeacherServiceImpl.java | 131 + .../service/impl/TenantServiceImpl.java | 164 + .../src/main/resources/application-dev.yml | 19 + .../src/main/resources/application.yml | 47 + .../db/migration/V1__init_schema.sql | 522 + .../V2__add_course_package_fields.sql | 78 + 390 files changed, 100418 insertions(+) create mode 100644 .gitignore create mode 100644 reading-platform-backend/.env.development create mode 100644 reading-platform-backend/nest-cli.json create mode 100644 reading-platform-backend/package-lock.json create mode 100644 reading-platform-backend/package.json create mode 100644 reading-platform-backend/prisma/migrate-v1-to-v2.ts create mode 100644 reading-platform-backend/prisma/migrations/20260210055321_init/migration.sql create mode 100644 reading-platform-backend/prisma/migrations/20260210092744_init/migration.sql create mode 100644 reading-platform-backend/prisma/migrations/20260210093209_make_picture_book_nullable/migration.sql create mode 100644 reading-platform-backend/prisma/migrations/20260227180846_init/migration.sql create mode 100644 reading-platform-backend/prisma/migrations/migration_lock.toml create mode 100644 reading-platform-backend/prisma/schema-v2.prisma create mode 100644 reading-platform-backend/prisma/schema.prisma create mode 100644 reading-platform-backend/prisma/seed.ts create mode 100644 reading-platform-backend/scripts/create-test-parent.ts create mode 100644 reading-platform-backend/src/app.module.js create mode 100644 reading-platform-backend/src/app.module.ts create mode 100644 reading-platform-backend/src/common/filters/http-exception.filter.ts create mode 100644 reading-platform-backend/src/database/prisma.module.js create mode 100644 reading-platform-backend/src/database/prisma.module.ts create mode 100644 reading-platform-backend/src/database/prisma.service.js create mode 100644 reading-platform-backend/src/database/prisma.service.ts create mode 100644 reading-platform-backend/src/main.js create mode 100644 reading-platform-backend/src/main.ts create mode 100644 reading-platform-backend/src/modules/admin/admin-settings.controller.ts create mode 100644 reading-platform-backend/src/modules/admin/admin-settings.service.ts create mode 100644 reading-platform-backend/src/modules/admin/admin-stats.controller.ts create mode 100644 reading-platform-backend/src/modules/admin/admin-stats.service.ts create mode 100644 reading-platform-backend/src/modules/admin/admin.module.ts create mode 100644 reading-platform-backend/src/modules/auth/auth.controller.js create mode 100644 reading-platform-backend/src/modules/auth/auth.controller.ts create mode 100644 reading-platform-backend/src/modules/auth/auth.module.js create mode 100644 reading-platform-backend/src/modules/auth/auth.module.ts create mode 100644 reading-platform-backend/src/modules/auth/auth.service.js create mode 100644 reading-platform-backend/src/modules/auth/auth.service.ts create mode 100644 reading-platform-backend/src/modules/auth/dto/login.dto.js create mode 100644 reading-platform-backend/src/modules/auth/dto/login.dto.ts create mode 100644 reading-platform-backend/src/modules/auth/strategies/jwt.strategy.js create mode 100644 reading-platform-backend/src/modules/auth/strategies/jwt.strategy.ts create mode 100644 reading-platform-backend/src/modules/common/common.module.ts create mode 100644 reading-platform-backend/src/modules/common/decorators/log-operation.decorator.ts create mode 100644 reading-platform-backend/src/modules/common/decorators/roles.decorator.ts create mode 100644 reading-platform-backend/src/modules/common/guards/jwt-auth.guard.ts create mode 100644 reading-platform-backend/src/modules/common/guards/roles.guard.ts create mode 100644 reading-platform-backend/src/modules/common/interceptors/log.interceptor.ts create mode 100644 reading-platform-backend/src/modules/common/operation-log.controller.ts create mode 100644 reading-platform-backend/src/modules/common/operation-log.service.ts create mode 100644 reading-platform-backend/src/modules/course-lesson/course-lesson.controller.ts create mode 100644 reading-platform-backend/src/modules/course-lesson/course-lesson.module.ts create mode 100644 reading-platform-backend/src/modules/course-lesson/course-lesson.service.ts create mode 100644 reading-platform-backend/src/modules/course-package/course-package.controller.ts create mode 100644 reading-platform-backend/src/modules/course-package/course-package.module.ts create mode 100644 reading-platform-backend/src/modules/course-package/course-package.service.ts create mode 100644 reading-platform-backend/src/modules/course/course-validation.service.ts create mode 100644 reading-platform-backend/src/modules/course/course.controller.ts create mode 100644 reading-platform-backend/src/modules/course/course.module.ts create mode 100644 reading-platform-backend/src/modules/course/course.service.ts create mode 100644 reading-platform-backend/src/modules/export/export.controller.ts create mode 100644 reading-platform-backend/src/modules/export/export.module.ts create mode 100644 reading-platform-backend/src/modules/export/export.service.ts create mode 100644 reading-platform-backend/src/modules/file-upload/file-upload.controller.ts create mode 100644 reading-platform-backend/src/modules/file-upload/file-upload.module.ts create mode 100644 reading-platform-backend/src/modules/file-upload/file-upload.service.ts create mode 100644 reading-platform-backend/src/modules/growth/dto/create-growth.dto.ts create mode 100644 reading-platform-backend/src/modules/growth/growth.controller.ts create mode 100644 reading-platform-backend/src/modules/growth/growth.module.ts create mode 100644 reading-platform-backend/src/modules/growth/growth.service.ts create mode 100644 reading-platform-backend/src/modules/lesson/dto/create-lesson.dto.ts create mode 100644 reading-platform-backend/src/modules/lesson/dto/finish-lesson.dto.ts create mode 100644 reading-platform-backend/src/modules/lesson/lesson.controller.ts create mode 100644 reading-platform-backend/src/modules/lesson/lesson.module.ts create mode 100644 reading-platform-backend/src/modules/lesson/lesson.service.ts create mode 100644 reading-platform-backend/src/modules/notification/notification.controller.ts create mode 100644 reading-platform-backend/src/modules/notification/notification.module.ts create mode 100644 reading-platform-backend/src/modules/notification/notification.service.ts create mode 100644 reading-platform-backend/src/modules/notification/schedule-notification.service.ts create mode 100644 reading-platform-backend/src/modules/parent/parent.controller.ts create mode 100644 reading-platform-backend/src/modules/parent/parent.module.ts create mode 100644 reading-platform-backend/src/modules/parent/parent.service.ts create mode 100644 reading-platform-backend/src/modules/resource/dto/create-resource.dto.ts create mode 100644 reading-platform-backend/src/modules/resource/resource.controller.ts create mode 100644 reading-platform-backend/src/modules/resource/resource.module.ts create mode 100644 reading-platform-backend/src/modules/resource/resource.service.ts create mode 100644 reading-platform-backend/src/modules/school-course/school-course.controller.ts create mode 100644 reading-platform-backend/src/modules/school-course/school-course.module.ts create mode 100644 reading-platform-backend/src/modules/school-course/school-course.service.ts create mode 100644 reading-platform-backend/src/modules/school/dto/class-teacher.dto.ts create mode 100644 reading-platform-backend/src/modules/school/dto/create-class.dto.ts create mode 100644 reading-platform-backend/src/modules/school/dto/create-student.dto.ts create mode 100644 reading-platform-backend/src/modules/school/dto/create-teacher.dto.ts create mode 100644 reading-platform-backend/src/modules/school/dto/import-students.dto.ts create mode 100644 reading-platform-backend/src/modules/school/dto/schedule.dto.ts create mode 100644 reading-platform-backend/src/modules/school/export.controller.ts create mode 100644 reading-platform-backend/src/modules/school/export.service.ts create mode 100644 reading-platform-backend/src/modules/school/package.controller.ts create mode 100644 reading-platform-backend/src/modules/school/school.controller.ts create mode 100644 reading-platform-backend/src/modules/school/school.module.ts create mode 100644 reading-platform-backend/src/modules/school/school.service.ts create mode 100644 reading-platform-backend/src/modules/school/settings.controller.ts create mode 100644 reading-platform-backend/src/modules/school/settings.service.ts create mode 100644 reading-platform-backend/src/modules/school/stats.controller.ts create mode 100644 reading-platform-backend/src/modules/school/stats.service.ts create mode 100644 reading-platform-backend/src/modules/task/dto/create-task.dto.ts create mode 100644 reading-platform-backend/src/modules/task/task.controller.ts create mode 100644 reading-platform-backend/src/modules/task/task.module.ts create mode 100644 reading-platform-backend/src/modules/task/task.service.ts create mode 100644 reading-platform-backend/src/modules/teacher-course/teacher-course.controller.ts create mode 100644 reading-platform-backend/src/modules/teacher-course/teacher-course.module.ts create mode 100644 reading-platform-backend/src/modules/teacher-course/teacher-course.service.ts create mode 100644 reading-platform-backend/src/modules/tenant/dto/tenant.dto.ts create mode 100644 reading-platform-backend/src/modules/tenant/tenant.controller.js create mode 100644 reading-platform-backend/src/modules/tenant/tenant.controller.ts create mode 100644 reading-platform-backend/src/modules/tenant/tenant.module.js create mode 100644 reading-platform-backend/src/modules/tenant/tenant.module.ts create mode 100644 reading-platform-backend/src/modules/tenant/tenant.service.js create mode 100644 reading-platform-backend/src/modules/tenant/tenant.service.ts create mode 100644 reading-platform-backend/src/modules/theme/theme.controller.ts create mode 100644 reading-platform-backend/src/modules/theme/theme.module.ts create mode 100644 reading-platform-backend/src/modules/theme/theme.service.ts create mode 100644 reading-platform-backend/start-backend.sh create mode 100644 reading-platform-backend/tsconfig.build.json create mode 100644 reading-platform-backend/tsconfig.json create mode 100644 reading-platform-frontend/.env.development create mode 100644 reading-platform-frontend/index.html create mode 100644 reading-platform-frontend/package-lock.json create mode 100644 reading-platform-frontend/package.json create mode 100644 reading-platform-frontend/public/logo.png create mode 100644 reading-platform-frontend/src/App.vue create mode 100644 reading-platform-frontend/src/api/admin.ts create mode 100644 reading-platform-frontend/src/api/auth.ts create mode 100644 reading-platform-frontend/src/api/course.ts create mode 100644 reading-platform-frontend/src/api/file.ts create mode 100644 reading-platform-frontend/src/api/growth.ts create mode 100644 reading-platform-frontend/src/api/index.ts create mode 100644 reading-platform-frontend/src/api/lesson.ts create mode 100644 reading-platform-frontend/src/api/package.ts create mode 100644 reading-platform-frontend/src/api/parent.ts create mode 100644 reading-platform-frontend/src/api/resource.ts create mode 100644 reading-platform-frontend/src/api/school-course.ts create mode 100644 reading-platform-frontend/src/api/school.ts create mode 100644 reading-platform-frontend/src/api/task.ts create mode 100644 reading-platform-frontend/src/api/teacher.ts create mode 100644 reading-platform-frontend/src/api/theme.ts create mode 100644 reading-platform-frontend/src/auto-imports.d.ts create mode 100644 reading-platform-frontend/src/components.d.ts create mode 100644 reading-platform-frontend/src/components/FilePreviewModal.vue create mode 100644 reading-platform-frontend/src/components/NotificationBell.vue create mode 100644 reading-platform-frontend/src/components/course/FileUploader.vue create mode 100644 reading-platform-frontend/src/components/course/LessonConfigPanel.vue create mode 100644 reading-platform-frontend/src/components/course/LessonStepsEditor.vue create mode 100644 reading-platform-frontend/src/main.ts create mode 100644 reading-platform-frontend/src/router/index.ts create mode 100644 reading-platform-frontend/src/stores/user.ts create mode 100644 reading-platform-frontend/src/utils/tagMaps.ts create mode 100644 reading-platform-frontend/src/views/NotFoundView.vue create mode 100644 reading-platform-frontend/src/views/admin/DashboardView.vue create mode 100644 reading-platform-frontend/src/views/admin/LayoutView.vue create mode 100644 reading-platform-frontend/src/views/admin/SettingsView.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/CourseEditView.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/CourseListView.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/CourseReviewView.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/CourseStatsView.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/components/Step1BasicInfo.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/components/Step2CourseIntro.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/components/Step3ScheduleRef.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/components/Step4IntroLesson.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/components/Step5CollectiveLesson.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/components/Step6DomainLessons.vue create mode 100644 reading-platform-frontend/src/views/admin/courses/components/Step7Environment.vue create mode 100644 reading-platform-frontend/src/views/admin/packages/PackageDetailView.vue create mode 100644 reading-platform-frontend/src/views/admin/packages/PackageEditView.vue create mode 100644 reading-platform-frontend/src/views/admin/packages/PackageListView.vue create mode 100644 reading-platform-frontend/src/views/admin/resources/ResourceListView.vue create mode 100644 reading-platform-frontend/src/views/admin/tenants/TenantListView.vue create mode 100644 reading-platform-frontend/src/views/admin/themes/ThemeListView.vue create mode 100644 reading-platform-frontend/src/views/auth/LoginView.vue create mode 100644 reading-platform-frontend/src/views/parent/DashboardView.vue create mode 100644 reading-platform-frontend/src/views/parent/LayoutView.vue create mode 100644 reading-platform-frontend/src/views/parent/children/ChildProfileView.vue create mode 100644 reading-platform-frontend/src/views/parent/children/ChildrenView.vue create mode 100644 reading-platform-frontend/src/views/parent/growth/GrowthRecordView.vue create mode 100644 reading-platform-frontend/src/views/parent/lessons/LessonHistoryView.vue create mode 100644 reading-platform-frontend/src/views/parent/tasks/TaskListView.vue create mode 100644 reading-platform-frontend/src/views/school/DashboardView.vue create mode 100644 reading-platform-frontend/src/views/school/LayoutView.vue create mode 100644 reading-platform-frontend/src/views/school/PackageView.vue create mode 100644 reading-platform-frontend/src/views/school/ReportView.vue create mode 100644 reading-platform-frontend/src/views/school/classes/ClassListView.vue create mode 100644 reading-platform-frontend/src/views/school/courses/CourseDetailView.vue create mode 100644 reading-platform-frontend/src/views/school/courses/CourseListView.vue create mode 100644 reading-platform-frontend/src/views/school/feedback/FeedbackView.vue create mode 100644 reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue create mode 100644 reading-platform-frontend/src/views/school/parents/ParentListView.vue create mode 100644 reading-platform-frontend/src/views/school/schedule/CalendarView.vue create mode 100644 reading-platform-frontend/src/views/school/schedule/ScheduleView.vue create mode 100644 reading-platform-frontend/src/views/school/schedule/TimetableView.vue create mode 100644 reading-platform-frontend/src/views/school/school-courses/SchoolCourseDetailView.vue create mode 100644 reading-platform-frontend/src/views/school/school-courses/SchoolCourseEditView.vue create mode 100644 reading-platform-frontend/src/views/school/school-courses/SchoolCourseListView.vue create mode 100644 reading-platform-frontend/src/views/school/settings/OperationLogView.vue create mode 100644 reading-platform-frontend/src/views/school/settings/SettingsView.vue create mode 100644 reading-platform-frontend/src/views/school/students/StudentListView.vue create mode 100644 reading-platform-frontend/src/views/school/tasks/TaskListView.vue create mode 100644 reading-platform-frontend/src/views/school/tasks/TaskTemplateView.vue create mode 100644 reading-platform-frontend/src/views/school/teachers/TeacherListView.vue create mode 100644 reading-platform-frontend/src/views/teacher/DashboardView.vue create mode 100644 reading-platform-frontend/src/views/teacher/LayoutView.vue create mode 100644 reading-platform-frontend/src/views/teacher/classes/ClassListView.vue create mode 100644 reading-platform-frontend/src/views/teacher/classes/ClassStudentsView.vue create mode 100644 reading-platform-frontend/src/views/teacher/courses/CourseDetailView.vue create mode 100644 reading-platform-frontend/src/views/teacher/courses/CourseListView.vue create mode 100644 reading-platform-frontend/src/views/teacher/courses/PrepareModeView.vue create mode 100644 reading-platform-frontend/src/views/teacher/feedback/FeedbackView.vue create mode 100644 reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/BroadcastView.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/LessonListView.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/LessonRecordsView.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/LessonView.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/components/KidsMode.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/components/viewers/AudioPlayer.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/components/viewers/EbookViewer.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/components/viewers/SlidesViewer.vue create mode 100644 reading-platform-frontend/src/views/teacher/lessons/components/viewers/VideoPlayer.vue create mode 100644 reading-platform-frontend/src/views/teacher/schedule/ScheduleView.vue create mode 100644 reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseDetailView.vue create mode 100644 reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseEditView.vue create mode 100644 reading-platform-frontend/src/views/teacher/school-courses/SchoolCourseListView.vue create mode 100644 reading-platform-frontend/src/views/teacher/tasks/TaskListView.vue create mode 100644 reading-platform-frontend/src/vite-env.d.ts create mode 100644 reading-platform-frontend/start-frontend.sh create mode 100644 reading-platform-frontend/tsconfig.json create mode 100644 reading-platform-frontend/tsconfig.node.json create mode 100644 reading-platform-frontend/vite.config.ts create mode 100644 reading-platform-java/README.md create mode 100644 reading-platform-java/pom.xml create mode 100644 reading-platform-java/src/main/java/com/reading/platform/ReadingPlatformApplication.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/annotation/RequireRole.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/aspect/RoleAspect.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/config/MybatisPlusConfig.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/config/OpenApiConfig.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/config/SecurityConfig.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/CourseStatus.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/Gender.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/LessonStatus.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/TaskStatus.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/TaskType.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/TenantStatus.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/enums/UserRole.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/exception/BusinessException.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/exception/GlobalExceptionHandler.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/response/PageResult.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/response/Result.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/security/JwtPayload.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenProvider.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/security/SecurityUtils.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/common/util/PageUtils.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminCourseController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminTenantController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/parent/ParentChildController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/parent/ParentGrowthController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/parent/ParentNotificationController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/parent/ParentTaskController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolGrowthController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolParentController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStudentController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTaskController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTeacherController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherGrowthController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherLessonController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherNotificationController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherTaskController.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/CourseCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/CourseUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/LessonCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/LessonUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/LoginRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/ParentCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/ParentUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/StudentCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/StudentUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/TaskUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/TeacherUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/TenantCreateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/request/TenantUpdateRequest.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/LoginResponse.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/TenantResponse.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/UserInfoResponse.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/AdminUser.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/ClassTeacher.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Clazz.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Course.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/CourseActivity.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/CourseResource.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/CourseScript.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/CourseScriptPage.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/CourseVersion.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/GrowthRecord.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Lesson.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/LessonFeedback.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Notification.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/OperationLog.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Parent.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/ParentStudent.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/ResourceItem.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/ResourceLibrary.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/SchedulePlan.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/ScheduleTemplate.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Student.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/StudentClassHistory.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/StudentRecord.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/SystemSetting.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Tag.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Task.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/TaskCompletion.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/TaskTarget.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/TaskTemplate.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Teacher.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/Tenant.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/entity/TenantCourse.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/AdminUserMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/ClassTeacherMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/ClazzMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/CourseActivityMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/CourseMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/CourseResourceMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/CourseScriptMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/CourseScriptPageMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/CourseVersionMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/GrowthRecordMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/LessonFeedbackMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/LessonMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/NotificationMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/OperationLogMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/ParentMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/ParentStudentMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/ResourceItemMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/ResourceLibraryMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/SchedulePlanMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/ScheduleTemplateMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/StudentClassHistoryMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/StudentMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/StudentRecordMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/SystemSettingMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TagMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TaskCompletionMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TaskMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TaskTargetMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TaskTemplateMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TeacherMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TenantCourseMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/mapper/TenantMapper.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/AuthService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/CourseService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/GrowthRecordService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/LessonService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/NotificationService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/ParentService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/StudentService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/TaskService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/TeacherService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/GrowthRecordServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/LessonServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/NotificationServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/ParentServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java create mode 100644 reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java create mode 100644 reading-platform-java/src/main/resources/application-dev.yml create mode 100644 reading-platform-java/src/main/resources/application.yml create mode 100644 reading-platform-java/src/main/resources/db/migration/V1__init_schema.sql create mode 100644 reading-platform-java/src/main/resources/db/migration/V2__add_course_package_fields.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dcd2109 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# 依赖目录 +node_modules/ +.pnp +.pnp.js + +# 构建产物 +dist/ +build/ +target/ +*.class +*.jar +*.war + +# 数据库文件 +*.db +*.sqlite +*.sqlite3 + +# 环境变量(含敏感信息,不提交) +.env +.env.local +.env.production + +# 保留开发环境配置(可按需注释掉) +# .env.development + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# IDE +.idea/ +.vscode/ +*.iml +*.ipr +*.iws + +# 日志 +logs/ +*.log +npm-debug.log* + +# 临时文件 +tmp/ +temp/ + +# 只提交 reading-platform 三个子项目 +test-website/ +归档.zip diff --git a/reading-platform-backend/.env.development b/reading-platform-backend/.env.development new file mode 100644 index 0000000..2d55981 --- /dev/null +++ b/reading-platform-backend/.env.development @@ -0,0 +1,6 @@ +DATABASE_URL="file:/Users/retirado/ccProgram/reading-platform-backend/dev.db" +NODE_ENV=development +PORT=3000 +JWT_SECRET="your-super-secret-jwt-key" +JWT_EXPIRES_IN="7d" +FRONTEND_URL="http://localhost:5173" diff --git a/reading-platform-backend/nest-cli.json b/reading-platform-backend/nest-cli.json new file mode 100644 index 0000000..cbabe9f --- /dev/null +++ b/reading-platform-backend/nest-cli.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "webpack": false, + "tsConfigPath": "tsconfig.json" + } +} diff --git a/reading-platform-backend/package-lock.json b/reading-platform-backend/package-lock.json new file mode 100644 index 0000000..8997b3e --- /dev/null +++ b/reading-platform-backend/package-lock.json @@ -0,0 +1,11688 @@ +{ + "name": "reading-platform-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reading-platform-backend", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "^10.3.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.3.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.22", + "@nestjs/schedule": "^6.1.1", + "@nestjs/throttler": "^5.2.0", + "@prisma/client": "^5.22.0", + "@types/multer": "^2.0.0", + "ali-oss": "^6.18.1", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "compression": "^1.8.1", + "dayjs": "^1.11.10", + "exceljs": "^4.4.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "reflect-metadata": "^0.1.14", + "rxjs": "^7.8.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@nestjs/cli": "^10.3.0", + "@nestjs/schematics": "^10.1.0", + "@nestjs/testing": "^10.3.0", + "@types/bcrypt": "^5.0.2", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/node": "^20.10.6", + "@types/passport-jwt": "^4.0.0", + "@types/passport-local": "^1.0.38", + "@typescript-eslint/eslint-plugin": "^6.18.0", + "@typescript-eslint/parser": "^6.18.0", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "prisma": "^5.8.0", + "source-map-support": "^0.5.21", + "ts-jest": "^29.1.1", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "peer": true, + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schedule": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.1.tgz", + "integrity": "sha512-kQl1RRgi02GJ0uaUGCrXHCcwISsCsJDciCKe38ykJZgnAeeoeVWs8luWtBo4AqAAXm4nS5K8RlV0smHUJ4+2FA==", + "license": "MIT", + "dependencies": { + "cron": "4.4.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-5.2.0.tgz", + "integrity": "sha512-G/G/MV3xf6sy1DwmnJsgeL+d2tQ/xGRNa9ZhZjm9Kyxp+3+ylGzwJtcnhWlN82PMEp3TiDQpTt+9waOIg/bpPg==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", + "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.3.tgz", + "integrity": "sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ali-oss": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/ali-oss/-/ali-oss-6.23.0.tgz", + "integrity": "sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==", + "license": "MIT", + "dependencies": { + "address": "^1.2.2", + "agentkeepalive": "^3.4.1", + "bowser": "^1.6.0", + "copy-to": "^2.0.1", + "dateformat": "^2.0.0", + "debug": "^4.3.4", + "destroy": "^1.0.4", + "end-or-error": "^1.0.1", + "get-ready": "^1.0.0", + "humanize-ms": "^1.2.0", + "is-type-of": "^1.4.0", + "js-base64": "^2.5.2", + "jstoxml": "^2.0.0", + "lodash": "^4.17.21", + "merge-descriptors": "^1.0.1", + "mime": "^2.4.5", + "platform": "^1.3.1", + "pump": "^3.0.0", + "qs": "^6.4.0", + "sdk-base": "^2.0.1", + "stream-http": "2.8.2", + "stream-wormhole": "^1.0.4", + "urllib": "^2.44.0", + "utility": "^1.18.0", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz", + "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz", + "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + }, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/intcreator" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-user-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-user-agent/-/default-user-agent-1.0.0.tgz", + "integrity": "sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==", + "license": "MIT", + "dependencies": { + "os-name": "~1.0.3" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/digest-header": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/digest-header/-/digest-header-1.1.0.tgz", + "integrity": "sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==", + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/end-or-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/end-or-error/-/end-or-error-1.0.1.tgz", + "integrity": "sha512-OclLMSug+k2A0JKuf494im25ANRBVW8qsjmwbgX7lQ8P82H21PQ1PWkoYwb9y5yMBS69BPlwtzdIFClo3+7kOQ==", + "license": "MIT", + "engines": { + "node": ">= 0.11.14" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/formstream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/formstream/-/formstream-1.5.2.tgz", + "integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==", + "license": "MIT", + "dependencies": { + "destroy": "^1.0.4", + "mime": "^2.5.2", + "node-hex": "^1.0.1", + "pause-stream": "~0.0.11" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-ready": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-ready/-/get-ready-1.0.0.tgz", + "integrity": "sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==", + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-class-hotfix": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz", + "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "license": "BSD-3-Clause" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jstoxml": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/jstoxml/-/jstoxml-2.2.9.tgz", + "integrity": "sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.36", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.36.tgz", + "integrity": "sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==", + "license": "MIT" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-hex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-hex/-/node-hex-1.0.1.tgz", + "integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-name": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-1.0.3.tgz", + "integrity": "sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==", + "license": "MIT", + "dependencies": { + "osx-release": "^1.0.0", + "win-release": "^1.0.0" + }, + "bin": { + "os-name": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osx-release": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/osx-release/-/osx-release-1.1.0.tgz", + "integrity": "sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==", + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + }, + "bin": { + "osx-release": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sdk-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sdk-base/-/sdk-base-2.0.1.tgz", + "integrity": "sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==", + "license": "MIT", + "dependencies": { + "get-ready": "~1.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-http": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.2.tgz", + "integrity": "sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==", + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-http/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/stream-http/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/ts-node-dev/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-node-dev/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unescape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz", + "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urllib": { + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/urllib/-/urllib-2.44.0.tgz", + "integrity": "sha512-zRCJqdfYllRDA9bXUtx+vccyRqtJPKsw85f44zH7zPD28PIvjMqIgw9VwoTLV7xTBWZsbebUFVHU5ghQcWku2A==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.3.0", + "content-type": "^1.0.2", + "default-user-agent": "^1.0.0", + "digest-header": "^1.0.0", + "ee-first": "~1.1.1", + "formstream": "^1.1.0", + "humanize-ms": "^1.2.0", + "iconv-lite": "^0.6.3", + "pump": "^3.0.0", + "qs": "^6.4.0", + "statuses": "^1.3.1", + "utility": "^1.16.1" + }, + "engines": { + "node": ">= 0.10.0" + }, + "peerDependencies": { + "proxy-agent": "^5.0.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/urllib/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/urllib/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/win-release": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz", + "integrity": "sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==", + "license": "MIT", + "dependencies": { + "semver": "^5.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/win-release/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + } + } +} diff --git a/reading-platform-backend/package.json b/reading-platform-backend/package.json new file mode 100644 index 0000000..d93fa8e --- /dev/null +++ b/reading-platform-backend/package.json @@ -0,0 +1,66 @@ +{ + "name": "reading-platform-backend", + "version": "1.0.0", + "description": "幼儿阅读教学服务平台后端", + "main": "dist/src/main.js", + "scripts": { + "prestart:dev": "npx tsc", + "start:dev": "node dist/src/main.js", + "build": "npx tsc", + "start": "node dist/src/main.js", + "start:prod": "node dist/src/main.js", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "seed": "ts-node prisma/seed.ts" + }, + "dependencies": { + "@nestjs/common": "^10.3.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.3.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.22", + "@nestjs/schedule": "^6.1.1", + "@nestjs/throttler": "^5.2.0", + "@prisma/client": "^5.22.0", + "@types/multer": "^2.0.0", + "ali-oss": "^6.18.1", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "compression": "^1.8.1", + "dayjs": "^1.11.10", + "exceljs": "^4.4.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "reflect-metadata": "^0.1.14", + "rxjs": "^7.8.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@nestjs/cli": "^10.3.0", + "@nestjs/schematics": "^10.1.0", + "@nestjs/testing": "^10.3.0", + "@types/bcrypt": "^5.0.2", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/node": "^20.10.6", + "@types/passport-jwt": "^4.0.0", + "@types/passport-local": "^1.0.38", + "@typescript-eslint/eslint-plugin": "^6.18.0", + "@typescript-eslint/parser": "^6.18.0", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "prisma": "^5.8.0", + "source-map-support": "^0.5.21", + "ts-jest": "^29.1.1", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.6.3" + } +} diff --git a/reading-platform-backend/prisma/migrate-v1-to-v2.ts b/reading-platform-backend/prisma/migrate-v1-to-v2.ts new file mode 100644 index 0000000..c8c0154 --- /dev/null +++ b/reading-platform-backend/prisma/migrate-v1-to-v2.ts @@ -0,0 +1,385 @@ +/** + * 数据迁移脚本:V1 -> V2 + * + * 迁移内容: + * 1. 初始化主题字典(6个默认主题) + * 2. 为现有课程包创建默认的 CourseLesson(集体课) + * 3. 迁移 CourseScript → LessonStep + * 4. 迁移 CourseActivity → CourseLesson(领域课) + * 5. 创建默认套餐(按年级分组) + * 6. 迁移租户授权 TenantCourse → TenantPackage + * + * 执行命令:npx ts-node prisma/migrate-v1-to-v2.ts + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// 默认主题 +const DEFAULT_THEMES = [ + { name: '我爱幼儿园', description: '适应幼儿园生活,培养基本生活习惯', sortOrder: 1 }, + { name: '认识自我', description: '认识身体、情绪、能力,建立自我意识', sortOrder: 2 }, + { name: '我的家', description: '了解家庭成员,培养亲情和责任感', sortOrder: 3 }, + { name: '美丽的自然', description: '探索自然现象,培养环保意识', sortOrder: 4 }, + { name: '奇妙的世界', description: '认识社会和世界,拓展视野', sortOrder: 5 }, + { name: '成长的快乐', description: '培养学习兴趣和良好习惯', sortOrder: 6 }, +]; + +// 年级映射 +const GRADE_LEVELS = ['小班', '中班', '大班']; + +async function main() { + console.log('开始数据迁移...\n'); + + try { + // ==================== Step 1: 初始化主题字典 ==================== + console.log('Step 1: 初始化主题字典...'); + const existingThemes = await prisma.theme.count(); + + if (existingThemes === 0) { + for (const theme of DEFAULT_THEMES) { + await prisma.theme.create({ + data: { + name: theme.name, + description: theme.description, + sortOrder: theme.sortOrder, + status: 'ACTIVE', + }, + }); + } + console.log(` ✅ 已创建 ${DEFAULT_THEMES.length} 个默认主题`); + } else { + console.log(` ⏭️ 主题已存在 (${existingThemes} 个),跳过`); + } + + // ==================== Step 2: 创建 CourseLesson(集体课)并迁移 CourseScript ==================== + console.log('\nStep 2: 创建 CourseLesson 并迁移 CourseScript...'); + const courses = await prisma.course.findMany({ + where: { isLatest: true }, + include: { + scripts: { + orderBy: { sortOrder: 'asc' }, + }, + }, + }); + + let lessonCount = 0; + let stepCount = 0; + + for (const course of courses) { + // 检查是否已有集体课 + const existingLesson = await prisma.courseLesson.findFirst({ + where: { courseId: course.id, lessonType: 'COLLECTIVE' }, + }); + + if (existingLesson) { + console.log(` ⏭️ 课程 "${course.name}" 已有集体课,跳过`); + continue; + } + + // 创建集体课 CourseLesson + const courseLesson = await prisma.courseLesson.create({ + data: { + courseId: course.id, + lessonType: 'COLLECTIVE', + name: `${course.name} - 集体课`, + description: course.description, + duration: course.duration || 25, + pptPath: course.pptPath, + pptName: course.pptName, + objectives: course.lessonPlanData ? JSON.parse(course.lessonPlanData).objectives : null, + preparation: course.tools || course.studentMaterials, + sortOrder: 0, + }, + }); + lessonCount++; + + // 迁移 CourseScript → LessonStep + for (const script of course.scripts) { + await prisma.lessonStep.create({ + data: { + lessonId: courseLesson.id, + name: script.stepName || `环节 ${script.stepIndex}`, + content: script.teacherScript, + duration: script.duration || 5, + objective: script.objective, + resourceIds: script.resourceIds, + sortOrder: script.sortOrder || script.stepIndex, + }, + }); + stepCount++; + } + + console.log(` ✅ 课程 "${course.name}":创建集体课 + ${course.scripts.length} 个环节`); + } + + console.log(` 📊 共创建 ${lessonCount} 个集体课,${stepCount} 个教学环节`); + + // ==================== Step 3: 迁移 CourseActivity → CourseLesson(领域课) ==================== + console.log('\nStep 3: 迁移 CourseActivity → CourseLesson(领域课)...'); + const activities = await prisma.courseActivity.findMany({ + include: { course: true }, + }); + + // 按课程分组 + const activitiesByCourse = new Map(); + for (const activity of activities) { + if (!activitiesByCourse.has(activity.courseId)) { + activitiesByCourse.set(activity.courseId, []); + } + activitiesByCourse.get(activity.courseId)!.push(activity); + } + + let activityLessonCount = 0; + + for (const [courseId, courseActivities] of activitiesByCourse) { + const course = courseActivities[0].course; + + for (const activity of courseActivities) { + // 确定课程类型 + let lessonType = 'DOMAIN'; + const domainMap: Record = { + '健康': 'HEALTH', + '语言': 'LANGUAGE', + '社会': 'SOCIAL', + '科学': 'SCIENCE', + '艺术': 'ART', + }; + if (activity.domain && domainMap[activity.domain]) { + lessonType = domainMap[activity.domain]; + } + + // 检查是否已有该类型的领域课 + const existingActivityLesson = await prisma.courseLesson.findFirst({ + where: { courseId, lessonType }, + }); + + if (existingActivityLesson) { + continue; + } + + await prisma.courseLesson.create({ + data: { + courseId, + lessonType, + name: activity.name, + description: activity.activityGuide, + duration: activity.duration || 25, + objectives: activity.objectives, + preparation: activity.offlineMaterials, + extension: activity.onlineMaterials, + sortOrder: activity.sortOrder || 0, + }, + }); + activityLessonCount++; + } + + console.log(` ✅ 课程 "${course.name}":创建 ${courseActivities.length} 个领域课`); + } + + console.log(` 📊 共创建 ${activityLessonCount} 个领域课`); + + // ==================== Step 4: 创建默认套餐(按年级分组) ==================== + console.log('\nStep 4: 创建默认套餐...'); + const existingPackages = await prisma.coursePackage.count(); + + if (existingPackages === 0) { + // 获取所有已发布的课程 + const publishedCourses = await prisma.course.findMany({ + where: { status: 'PUBLISHED' }, + }); + + // 年级标签映射(统一格式) + const gradeTagMap: Record = { + 'SMALL': '小班', + 'small': '小班', + 'MIDDLE': '中班', + 'middle': '中班', + 'BIG': '大班', + 'big': '大班', + }; + + // 按年级分组课程 + const coursesByGrade = new Map(); + for (const course of publishedCourses) { + let gradeTags: string[] = []; + try { + gradeTags = JSON.parse(course.gradeTags || '[]'); + } catch { + gradeTags = []; + } + + for (const rawGrade of gradeTags) { + const grade = gradeTagMap[rawGrade] || rawGrade; + if (!coursesByGrade.has(grade)) { + coursesByGrade.set(grade, []); + } + coursesByGrade.get(grade)!.push(course); + } + } + + // 为每个年级创建套餐 + for (const grade of GRADE_LEVELS) { + const gradeCourses = coursesByGrade.get(grade) || []; + + if (gradeCourses.length === 0) { + console.log(` ⏭️ 年级 "${grade}" 没有课程,跳过套餐创建`); + continue; + } + + const packageName = `${grade}课程套餐`; + const coursePackage = await prisma.coursePackage.create({ + data: { + name: packageName, + description: `包含 ${gradeCourses.length} 个课程包,覆盖${grade}全学期教学内容`, + price: gradeCourses.length * 10000, // 假设每个课程包 100 元 + gradeLevels: JSON.stringify([grade]), + status: 'PUBLISHED', + courseCount: gradeCourses.length, + publishedAt: new Date(), + }, + }); + + // 创建套餐-课程关联 + for (let i = 0; i < gradeCourses.length; i++) { + const course = gradeCourses[i]; + await prisma.coursePackageCourse.create({ + data: { + packageId: coursePackage.id, + courseId: course.id, + gradeLevel: grade, + sortOrder: i, + }, + }); + } + + console.log(` ✅ 创建套餐 "${packageName}":包含 ${gradeCourses.length} 个课程包`); + } + } else { + console.log(` ⏭️ 套餐已存在 (${existingPackages} 个),跳过`); + } + + // ==================== Step 5: 迁移租户授权 TenantCourse → TenantPackage ==================== + console.log('\nStep 5: 迁移租户授权...'); + + // 年级标签映射(统一格式) + const gradeTagMapForAuth: Record = { + 'SMALL': '小班', + 'small': '小班', + 'MIDDLE': '中班', + 'middle': '中班', + 'BIG': '大班', + 'big': '大班', + }; + + // 获取所有套餐 + const packages = await prisma.coursePackage.findMany(); + const packageByGrade = new Map(); + for (const pkg of packages) { + const grades = JSON.parse(pkg.gradeLevels || '[]'); + for (const grade of grades) { + packageByGrade.set(grade, pkg); + } + } + + // 获取租户课程授权 + const tenantCourses = await prisma.tenantCourse.findMany({ + include: { course: true, tenant: true }, + }); + + // 按租户分组 + const tenantCoursesByTenant = new Map(); + for (const tc of tenantCourses) { + if (!tenantCoursesByTenant.has(tc.tenantId)) { + tenantCoursesByTenant.set(tc.tenantId, []); + } + tenantCoursesByTenant.get(tc.tenantId)!.push(tc); + } + + let tenantPackageCount = 0; + + for (const [tenantId, tcs] of tenantCoursesByTenant) { + const tenant = tcs[0].tenant; + + // 获取该租户需要的套餐 + const neededPackages = new Set(); + for (const tc of tcs) { + const gradeTags = JSON.parse(tc.course.gradeTags || '[]'); + for (const rawGrade of gradeTags) { + const grade = gradeTagMapForAuth[rawGrade] || rawGrade; + const pkg = packageByGrade.get(grade); + if (pkg) { + neededPackages.add(pkg); + } + } + } + + // 为租户创建套餐授权 + for (const pkg of neededPackages) { + // 检查是否已有授权 + const existingAuth = await prisma.tenantPackage.findFirst({ + where: { tenantId, packageId: pkg.id }, + }); + + if (existingAuth) { + continue; + } + + // 使用租户的订阅日期 + const startDate = tenant.startDate || new Date().toISOString().split('T')[0]; + const endDate = tenant.expireDate || new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + await prisma.tenantPackage.create({ + data: { + tenantId, + packageId: pkg.id, + startDate, + endDate, + status: 'ACTIVE', + pricePaid: pkg.price, + }, + }); + tenantPackageCount++; + } + + console.log(` ✅ 租户 "${tenant.name}":授权 ${neededPackages.size} 个套餐`); + } + + console.log(` 📊 共创建 ${tenantPackageCount} 个租户套餐授权`); + + // ==================== 完成 ==================== + console.log('\n✅ 数据迁移完成!'); + + // 输出统计信息 + const stats = { + themes: await prisma.theme.count(), + courses: await prisma.course.count({ where: { isLatest: true } }), + courseLessons: await prisma.courseLesson.count(), + lessonSteps: await prisma.lessonStep.count(), + packages: await prisma.coursePackage.count(), + tenantPackages: await prisma.tenantPackage.count(), + }; + + console.log('\n📊 迁移后数据统计:'); + console.log(` 主题:${stats.themes}`); + console.log(` 课程包:${stats.courses}`); + console.log(` 课程(CourseLesson):${stats.courseLessons}`); + console.log(` 教学环节:${stats.lessonSteps}`); + console.log(` 套餐:${stats.packages}`); + console.log(` 租户套餐授权:${stats.tenantPackages}`); + + } catch (error) { + console.error('❌ 迁移失败:', error); + throw error; + } +} + +main() + .catch((error) => { + console.error(error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/reading-platform-backend/prisma/migrations/20260210055321_init/migration.sql b/reading-platform-backend/prisma/migrations/20260210055321_init/migration.sql new file mode 100644 index 0000000..3362422 --- /dev/null +++ b/reading-platform-backend/prisma/migrations/20260210055321_init/migration.sql @@ -0,0 +1,262 @@ +-- CreateTable +CREATE TABLE "tenants" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "address" TEXT, + "contact_person" TEXT, + "contact_phone" TEXT, + "logo_url" TEXT, + "package_type" TEXT NOT NULL DEFAULT 'STANDARD', + "teacher_quota" INTEGER NOT NULL DEFAULT 20, + "student_quota" INTEGER NOT NULL DEFAULT 200, + "storage_quota" BIGINT NOT NULL DEFAULT 5368709120, + "start_date" TEXT NOT NULL, + "expire_date" TEXT NOT NULL, + "teacher_count" INTEGER NOT NULL DEFAULT 0, + "student_count" INTEGER NOT NULL DEFAULT 0, + "storage_used" BIGINT NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "teachers" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "tenant_id" BIGINT NOT NULL, + "name" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "email" TEXT, + "login_account" TEXT NOT NULL, + "password_hash" TEXT NOT NULL, + "class_ids" TEXT DEFAULT '[]', + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "feedback_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "last_login_at" DATETIME, + CONSTRAINT "teachers_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "classes" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "tenant_id" BIGINT NOT NULL, + "name" TEXT NOT NULL, + "grade" TEXT NOT NULL, + "teacher_id" BIGINT, + "student_count" INTEGER NOT NULL DEFAULT 0, + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "classes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "classes_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "students" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "tenant_id" BIGINT NOT NULL, + "class_id" BIGINT NOT NULL, + "name" TEXT NOT NULL, + "gender" TEXT, + "birth_date" DATETIME, + "parent_phone" TEXT, + "parent_name" TEXT, + "reading_count" INTEGER NOT NULL DEFAULT 0, + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "students_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "students_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "courses" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "picture_book_id" INTEGER, + "picture_book_name" TEXT, + "grade_tags" TEXT NOT NULL DEFAULT '[]', + "domain_tags" TEXT NOT NULL DEFAULT '[]', + "duration" INTEGER NOT NULL DEFAULT 25, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "version" TEXT NOT NULL DEFAULT '1.0', + "usage_count" INTEGER NOT NULL DEFAULT 0, + "teacher_count" INTEGER NOT NULL DEFAULT 0, + "avg_rating" REAL NOT NULL DEFAULT 0, + "created_by" INTEGER, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "published_at" DATETIME +); + +-- CreateTable +CREATE TABLE "course_resources" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "course_id" BIGINT NOT NULL, + "resource_type" TEXT NOT NULL, + "resource_name" TEXT NOT NULL, + "file_url" TEXT NOT NULL, + "file_size" BIGINT, + "mime_type" TEXT, + "metadata" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "course_resources_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_scripts" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "course_id" BIGINT NOT NULL, + "step_index" INTEGER NOT NULL, + "step_name" TEXT NOT NULL, + "step_type" TEXT NOT NULL, + "duration" INTEGER NOT NULL, + "objective" TEXT, + "teacher_script" TEXT, + "interaction_points" TEXT, + "resource_ids" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "course_scripts_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_script_pages" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "script_id" BIGINT NOT NULL, + "page_number" INTEGER NOT NULL, + "questions" TEXT, + "interaction_component" TEXT, + "teacher_notes" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "course_script_pages_script_id_fkey" FOREIGN KEY ("script_id") REFERENCES "course_scripts" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_activities" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "course_id" BIGINT NOT NULL, + "name" TEXT NOT NULL, + "domain" TEXT, + "domain_tag_id" INTEGER, + "activity_type" TEXT NOT NULL, + "duration" INTEGER, + "online_materials" TEXT, + "offlineMaterials" TEXT, + "activityGuide" TEXT, + "objectives" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "course_activities_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "lessons" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "tenant_id" BIGINT NOT NULL, + "teacher_id" BIGINT NOT NULL, + "class_id" BIGINT NOT NULL, + "course_id" BIGINT NOT NULL, + "planned_datetime" DATETIME, + "start_datetime" DATETIME, + "end_datetime" DATETIME, + "actual_duration" INTEGER, + "status" TEXT NOT NULL DEFAULT 'PLANNED', + "overall_rating" TEXT, + "participation_rating" TEXT, + "completion_note" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "lessons_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "lesson_feedbacks" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "lesson_id" BIGINT NOT NULL, + "teacher_id" BIGINT NOT NULL, + "design_quality" INTEGER, + "participation" INTEGER, + "goal_achievement" INTEGER, + "step_feedbacks" TEXT, + "pros" TEXT, + "suggestions" TEXT, + "activities_done" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "lesson_feedbacks_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "lesson_feedbacks_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "student_records" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "lesson_id" BIGINT NOT NULL, + "student_id" BIGINT NOT NULL, + "focus" INTEGER, + "participation" INTEGER, + "interest" INTEGER, + "understanding" INTEGER, + "domainAchievements" TEXT, + "notes" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "student_records_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "student_records_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "tags" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "level" INTEGER NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "parent_id" BIGINT, + "description" TEXT, + "metadata" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "tenant_courses" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "tenant_id" BIGINT NOT NULL, + "course_id" BIGINT NOT NULL, + "authorized" BOOLEAN NOT NULL DEFAULT true, + "authorized_at" DATETIME, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "tenant_courses_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tenant_courses_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "teachers_login_account_key" ON "teachers"("login_account"); + +-- CreateIndex +CREATE UNIQUE INDEX "course_scripts_course_id_step_index_key" ON "course_scripts"("course_id", "step_index"); + +-- CreateIndex +CREATE UNIQUE INDEX "course_script_pages_script_id_page_number_key" ON "course_script_pages"("script_id", "page_number"); + +-- CreateIndex +CREATE UNIQUE INDEX "lesson_feedbacks_lesson_id_teacher_id_key" ON "lesson_feedbacks"("lesson_id", "teacher_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "student_records_lesson_id_student_id_key" ON "student_records"("lesson_id", "student_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "tags_code_key" ON "tags"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_courses_tenant_id_course_id_key" ON "tenant_courses"("tenant_id", "course_id"); diff --git a/reading-platform-backend/prisma/migrations/20260210092744_init/migration.sql b/reading-platform-backend/prisma/migrations/20260210092744_init/migration.sql new file mode 100644 index 0000000..3a738ad --- /dev/null +++ b/reading-platform-backend/prisma/migrations/20260210092744_init/migration.sql @@ -0,0 +1,323 @@ +/* + Warnings: + + - The primary key for the `classes` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `classes` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `teacher_id` on the `classes` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `tenant_id` on the `classes` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `course_activities` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `course_id` on the `course_activities` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `id` on the `course_activities` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `course_resources` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `course_id` on the `course_resources` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `file_size` on the `course_resources` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `id` on the `course_resources` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `course_script_pages` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `course_script_pages` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `script_id` on the `course_script_pages` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `course_scripts` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `course_id` on the `course_scripts` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `id` on the `course_scripts` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `courses` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `courses` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `lesson_feedbacks` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `lesson_feedbacks` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `lesson_id` on the `lesson_feedbacks` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `teacher_id` on the `lesson_feedbacks` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `lessons` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `class_id` on the `lessons` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `course_id` on the `lessons` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `id` on the `lessons` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `teacher_id` on the `lessons` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `tenant_id` on the `lessons` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `student_records` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `student_records` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `lesson_id` on the `student_records` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `student_id` on the `student_records` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `students` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `class_id` on the `students` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `id` on the `students` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `tenant_id` on the `students` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `tags` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `tags` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `parent_id` on the `tags` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `teachers` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `teachers` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `tenant_id` on the `teachers` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `tenant_courses` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `course_id` on the `tenant_courses` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `id` on the `tenant_courses` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - You are about to alter the column `tenant_id` on the `tenant_courses` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - The primary key for the `tenants` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `tenants` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Int`. + - Made the column `picture_book_id` on table `courses` required. This step will fail if there are existing NULL values in that column. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_classes" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "grade" TEXT NOT NULL, + "teacher_id" INTEGER, + "student_count" INTEGER NOT NULL DEFAULT 0, + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "classes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "classes_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_classes" ("created_at", "grade", "id", "lesson_count", "name", "student_count", "teacher_id", "tenant_id", "updated_at") SELECT "created_at", "grade", "id", "lesson_count", "name", "student_count", "teacher_id", "tenant_id", "updated_at" FROM "classes"; +DROP TABLE "classes"; +ALTER TABLE "new_classes" RENAME TO "classes"; +CREATE TABLE "new_course_activities" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "course_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "domain" TEXT, + "domain_tag_id" INTEGER, + "activity_type" TEXT NOT NULL, + "duration" INTEGER, + "online_materials" TEXT, + "offlineMaterials" TEXT, + "activityGuide" TEXT, + "objectives" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "course_activities_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_course_activities" ("activityGuide", "activity_type", "course_id", "created_at", "domain", "domain_tag_id", "duration", "id", "name", "objectives", "offlineMaterials", "online_materials", "sort_order") SELECT "activityGuide", "activity_type", "course_id", "created_at", "domain", "domain_tag_id", "duration", "id", "name", "objectives", "offlineMaterials", "online_materials", "sort_order" FROM "course_activities"; +DROP TABLE "course_activities"; +ALTER TABLE "new_course_activities" RENAME TO "course_activities"; +CREATE TABLE "new_course_resources" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "course_id" INTEGER NOT NULL, + "resource_type" TEXT NOT NULL, + "resource_name" TEXT NOT NULL, + "file_url" TEXT NOT NULL, + "file_size" INTEGER, + "mime_type" TEXT, + "metadata" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "course_resources_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_course_resources" ("course_id", "created_at", "file_size", "file_url", "id", "metadata", "mime_type", "resource_name", "resource_type", "sort_order") SELECT "course_id", "created_at", "file_size", "file_url", "id", "metadata", "mime_type", "resource_name", "resource_type", "sort_order" FROM "course_resources"; +DROP TABLE "course_resources"; +ALTER TABLE "new_course_resources" RENAME TO "course_resources"; +CREATE TABLE "new_course_script_pages" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "script_id" INTEGER NOT NULL, + "page_number" INTEGER NOT NULL, + "questions" TEXT, + "interaction_component" TEXT, + "teacher_notes" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "course_script_pages_script_id_fkey" FOREIGN KEY ("script_id") REFERENCES "course_scripts" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_course_script_pages" ("created_at", "id", "interaction_component", "page_number", "questions", "script_id", "teacher_notes", "updated_at") SELECT "created_at", "id", "interaction_component", "page_number", "questions", "script_id", "teacher_notes", "updated_at" FROM "course_script_pages"; +DROP TABLE "course_script_pages"; +ALTER TABLE "new_course_script_pages" RENAME TO "course_script_pages"; +CREATE UNIQUE INDEX "course_script_pages_script_id_page_number_key" ON "course_script_pages"("script_id", "page_number"); +CREATE TABLE "new_course_scripts" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "course_id" INTEGER NOT NULL, + "step_index" INTEGER NOT NULL, + "step_name" TEXT NOT NULL, + "step_type" TEXT NOT NULL, + "duration" INTEGER NOT NULL, + "objective" TEXT, + "teacher_script" TEXT, + "interaction_points" TEXT, + "resource_ids" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "course_scripts_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_course_scripts" ("course_id", "created_at", "duration", "id", "interaction_points", "objective", "resource_ids", "sort_order", "step_index", "step_name", "step_type", "teacher_script", "updated_at") SELECT "course_id", "created_at", "duration", "id", "interaction_points", "objective", "resource_ids", "sort_order", "step_index", "step_name", "step_type", "teacher_script", "updated_at" FROM "course_scripts"; +DROP TABLE "course_scripts"; +ALTER TABLE "new_course_scripts" RENAME TO "course_scripts"; +CREATE UNIQUE INDEX "course_scripts_course_id_step_index_key" ON "course_scripts"("course_id", "step_index"); +CREATE TABLE "new_courses" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "description" TEXT, + "picture_book_id" INTEGER NOT NULL, + "picture_book_name" TEXT, + "grade_tags" TEXT NOT NULL DEFAULT '[]', + "domain_tags" TEXT NOT NULL DEFAULT '[]', + "duration" INTEGER NOT NULL DEFAULT 25, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "version" TEXT NOT NULL DEFAULT '1.0', + "usage_count" INTEGER NOT NULL DEFAULT 0, + "teacher_count" INTEGER NOT NULL DEFAULT 0, + "avg_rating" REAL NOT NULL DEFAULT 0, + "created_by" INTEGER, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "published_at" DATETIME +); +INSERT INTO "new_courses" ("avg_rating", "created_at", "created_by", "description", "domain_tags", "duration", "grade_tags", "id", "name", "picture_book_id", "picture_book_name", "published_at", "status", "teacher_count", "updated_at", "usage_count", "version") SELECT "avg_rating", "created_at", "created_by", "description", "domain_tags", "duration", "grade_tags", "id", "name", "picture_book_id", "picture_book_name", "published_at", "status", "teacher_count", "updated_at", "usage_count", "version" FROM "courses"; +DROP TABLE "courses"; +ALTER TABLE "new_courses" RENAME TO "courses"; +CREATE TABLE "new_lesson_feedbacks" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "lesson_id" INTEGER NOT NULL, + "teacher_id" INTEGER NOT NULL, + "design_quality" INTEGER, + "participation" INTEGER, + "goal_achievement" INTEGER, + "step_feedbacks" TEXT, + "pros" TEXT, + "suggestions" TEXT, + "activities_done" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "lesson_feedbacks_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "lesson_feedbacks_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_lesson_feedbacks" ("activities_done", "created_at", "design_quality", "goal_achievement", "id", "lesson_id", "participation", "pros", "step_feedbacks", "suggestions", "teacher_id", "updated_at") SELECT "activities_done", "created_at", "design_quality", "goal_achievement", "id", "lesson_id", "participation", "pros", "step_feedbacks", "suggestions", "teacher_id", "updated_at" FROM "lesson_feedbacks"; +DROP TABLE "lesson_feedbacks"; +ALTER TABLE "new_lesson_feedbacks" RENAME TO "lesson_feedbacks"; +CREATE UNIQUE INDEX "lesson_feedbacks_lesson_id_teacher_id_key" ON "lesson_feedbacks"("lesson_id", "teacher_id"); +CREATE TABLE "new_lessons" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "teacher_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "course_id" INTEGER NOT NULL, + "planned_datetime" DATETIME, + "start_datetime" DATETIME, + "end_datetime" DATETIME, + "actual_duration" INTEGER, + "status" TEXT NOT NULL DEFAULT 'PLANNED', + "overall_rating" TEXT, + "participation_rating" TEXT, + "completion_note" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "lessons_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_lessons" ("actual_duration", "class_id", "completion_note", "course_id", "created_at", "end_datetime", "id", "overall_rating", "participation_rating", "planned_datetime", "start_datetime", "status", "teacher_id", "tenant_id", "updated_at") SELECT "actual_duration", "class_id", "completion_note", "course_id", "created_at", "end_datetime", "id", "overall_rating", "participation_rating", "planned_datetime", "start_datetime", "status", "teacher_id", "tenant_id", "updated_at" FROM "lessons"; +DROP TABLE "lessons"; +ALTER TABLE "new_lessons" RENAME TO "lessons"; +CREATE TABLE "new_student_records" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "lesson_id" INTEGER NOT NULL, + "student_id" INTEGER NOT NULL, + "focus" INTEGER, + "participation" INTEGER, + "interest" INTEGER, + "understanding" INTEGER, + "domainAchievements" TEXT, + "notes" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "student_records_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "student_records_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_student_records" ("created_at", "domainAchievements", "focus", "id", "interest", "lesson_id", "notes", "participation", "student_id", "understanding", "updated_at") SELECT "created_at", "domainAchievements", "focus", "id", "interest", "lesson_id", "notes", "participation", "student_id", "understanding", "updated_at" FROM "student_records"; +DROP TABLE "student_records"; +ALTER TABLE "new_student_records" RENAME TO "student_records"; +CREATE UNIQUE INDEX "student_records_lesson_id_student_id_key" ON "student_records"("lesson_id", "student_id"); +CREATE TABLE "new_students" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "gender" TEXT, + "birth_date" DATETIME, + "parent_phone" TEXT, + "parent_name" TEXT, + "reading_count" INTEGER NOT NULL DEFAULT 0, + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "students_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "students_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_students" ("birth_date", "class_id", "created_at", "gender", "id", "lesson_count", "name", "parent_name", "parent_phone", "reading_count", "tenant_id", "updated_at") SELECT "birth_date", "class_id", "created_at", "gender", "id", "lesson_count", "name", "parent_name", "parent_phone", "reading_count", "tenant_id", "updated_at" FROM "students"; +DROP TABLE "students"; +ALTER TABLE "new_students" RENAME TO "students"; +CREATE TABLE "new_tags" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "level" INTEGER NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "parent_id" INTEGER, + "description" TEXT, + "metadata" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_tags" ("code", "created_at", "description", "id", "level", "metadata", "name", "parent_id", "sort_order") SELECT "code", "created_at", "description", "id", "level", "metadata", "name", "parent_id", "sort_order" FROM "tags"; +DROP TABLE "tags"; +ALTER TABLE "new_tags" RENAME TO "tags"; +CREATE UNIQUE INDEX "tags_code_key" ON "tags"("code"); +CREATE TABLE "new_teachers" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "email" TEXT, + "login_account" TEXT NOT NULL, + "password_hash" TEXT NOT NULL, + "class_ids" TEXT DEFAULT '[]', + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "feedback_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "last_login_at" DATETIME, + CONSTRAINT "teachers_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_teachers" ("class_ids", "created_at", "email", "feedback_count", "id", "last_login_at", "lesson_count", "login_account", "name", "password_hash", "phone", "status", "tenant_id", "updated_at") SELECT "class_ids", "created_at", "email", "feedback_count", "id", "last_login_at", "lesson_count", "login_account", "name", "password_hash", "phone", "status", "tenant_id", "updated_at" FROM "teachers"; +DROP TABLE "teachers"; +ALTER TABLE "new_teachers" RENAME TO "teachers"; +CREATE UNIQUE INDEX "teachers_login_account_key" ON "teachers"("login_account"); +CREATE TABLE "new_tenant_courses" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "course_id" INTEGER NOT NULL, + "authorized" BOOLEAN NOT NULL DEFAULT true, + "authorized_at" DATETIME, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "tenant_courses_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tenant_courses_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_tenant_courses" ("authorized", "authorized_at", "course_id", "created_at", "id", "tenant_id") SELECT "authorized", "authorized_at", "course_id", "created_at", "id", "tenant_id" FROM "tenant_courses"; +DROP TABLE "tenant_courses"; +ALTER TABLE "new_tenant_courses" RENAME TO "tenant_courses"; +CREATE UNIQUE INDEX "tenant_courses_tenant_id_course_id_key" ON "tenant_courses"("tenant_id", "course_id"); +CREATE TABLE "new_tenants" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "address" TEXT, + "contact_person" TEXT, + "contact_phone" TEXT, + "logo_url" TEXT, + "package_type" TEXT NOT NULL DEFAULT 'STANDARD', + "teacher_quota" INTEGER NOT NULL DEFAULT 20, + "student_quota" INTEGER NOT NULL DEFAULT 200, + "storage_quota" BIGINT NOT NULL DEFAULT 5368709120, + "start_date" TEXT NOT NULL, + "expire_date" TEXT NOT NULL, + "teacher_count" INTEGER NOT NULL DEFAULT 0, + "student_count" INTEGER NOT NULL DEFAULT 0, + "storage_used" BIGINT NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL +); +INSERT INTO "new_tenants" ("address", "contact_person", "contact_phone", "created_at", "expire_date", "id", "logo_url", "name", "package_type", "start_date", "status", "storage_quota", "storage_used", "student_count", "student_quota", "teacher_count", "teacher_quota", "updated_at") SELECT "address", "contact_person", "contact_phone", "created_at", "expire_date", "id", "logo_url", "name", "package_type", "start_date", "status", "storage_quota", "storage_used", "student_count", "student_quota", "teacher_count", "teacher_quota", "updated_at" FROM "tenants"; +DROP TABLE "tenants"; +ALTER TABLE "new_tenants" RENAME TO "tenants"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/reading-platform-backend/prisma/migrations/20260210093209_make_picture_book_nullable/migration.sql b/reading-platform-backend/prisma/migrations/20260210093209_make_picture_book_nullable/migration.sql new file mode 100644 index 0000000..7797270 --- /dev/null +++ b/reading-platform-backend/prisma/migrations/20260210093209_make_picture_book_nullable/migration.sql @@ -0,0 +1,27 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_courses" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "description" TEXT, + "picture_book_id" INTEGER, + "picture_book_name" TEXT, + "grade_tags" TEXT NOT NULL DEFAULT '[]', + "domain_tags" TEXT NOT NULL DEFAULT '[]', + "duration" INTEGER NOT NULL DEFAULT 25, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "version" TEXT NOT NULL DEFAULT '1.0', + "usage_count" INTEGER NOT NULL DEFAULT 0, + "teacher_count" INTEGER NOT NULL DEFAULT 0, + "avg_rating" REAL NOT NULL DEFAULT 0, + "created_by" INTEGER, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "published_at" DATETIME +); +INSERT INTO "new_courses" ("avg_rating", "created_at", "created_by", "description", "domain_tags", "duration", "grade_tags", "id", "name", "picture_book_id", "picture_book_name", "published_at", "status", "teacher_count", "updated_at", "usage_count", "version") SELECT "avg_rating", "created_at", "created_by", "description", "domain_tags", "duration", "grade_tags", "id", "name", "picture_book_id", "picture_book_name", "published_at", "status", "teacher_count", "updated_at", "usage_count", "version" FROM "courses"; +DROP TABLE "courses"; +ALTER TABLE "new_courses" RENAME TO "courses"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/reading-platform-backend/prisma/migrations/20260227180846_init/migration.sql b/reading-platform-backend/prisma/migrations/20260227180846_init/migration.sql new file mode 100644 index 0000000..73375fa --- /dev/null +++ b/reading-platform-backend/prisma/migrations/20260227180846_init/migration.sql @@ -0,0 +1,634 @@ +-- CreateTable +CREATE TABLE "tenants" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "login_account" TEXT, + "password_hash" TEXT, + "address" TEXT, + "contact_person" TEXT, + "contact_phone" TEXT, + "logo_url" TEXT, + "package_type" TEXT NOT NULL DEFAULT 'STANDARD', + "teacher_quota" INTEGER NOT NULL DEFAULT 20, + "student_quota" INTEGER NOT NULL DEFAULT 200, + "storage_quota" BIGINT NOT NULL DEFAULT 5368709120, + "start_date" TEXT NOT NULL, + "expire_date" TEXT NOT NULL, + "teacher_count" INTEGER NOT NULL DEFAULT 0, + "student_count" INTEGER NOT NULL DEFAULT 0, + "storage_used" BIGINT NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "teachers" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "email" TEXT, + "login_account" TEXT NOT NULL, + "password_hash" TEXT NOT NULL, + "class_ids" TEXT DEFAULT '[]', + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "feedback_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "last_login_at" DATETIME, + CONSTRAINT "teachers_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "classes" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "grade" TEXT NOT NULL, + "teacher_id" INTEGER, + "student_count" INTEGER NOT NULL DEFAULT 0, + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "classes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "classes_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "students" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "gender" TEXT, + "birth_date" DATETIME, + "parent_phone" TEXT, + "parent_name" TEXT, + "reading_count" INTEGER NOT NULL DEFAULT 0, + "lesson_count" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "students_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "students_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "courses" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "description" TEXT, + "picture_book_id" INTEGER, + "picture_book_name" TEXT, + "cover_image_path" TEXT, + "ebook_paths" TEXT, + "audio_paths" TEXT, + "video_paths" TEXT, + "other_resources" TEXT, + "ppt_path" TEXT, + "ppt_name" TEXT, + "poster_paths" TEXT, + "tools" TEXT, + "student_materials" TEXT, + "lesson_plan_data" TEXT, + "activities_data" TEXT, + "assessment_data" TEXT, + "grade_tags" TEXT NOT NULL DEFAULT '[]', + "domain_tags" TEXT NOT NULL DEFAULT '[]', + "duration" INTEGER NOT NULL DEFAULT 25, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "version" TEXT NOT NULL DEFAULT '1.0', + "submitted_at" DATETIME, + "submitted_by" INTEGER, + "reviewed_at" DATETIME, + "reviewed_by" INTEGER, + "review_comment" TEXT, + "review_checklist" TEXT, + "parent_id" INTEGER, + "isLatest" BOOLEAN NOT NULL DEFAULT true, + "usage_count" INTEGER NOT NULL DEFAULT 0, + "teacher_count" INTEGER NOT NULL DEFAULT 0, + "avg_rating" REAL NOT NULL DEFAULT 0, + "created_by" INTEGER, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "published_at" DATETIME +); + +-- CreateTable +CREATE TABLE "course_versions" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "course_id" INTEGER NOT NULL, + "version" TEXT NOT NULL, + "snapshotData" TEXT NOT NULL, + "changeLog" TEXT, + "published_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "published_by" INTEGER NOT NULL, + CONSTRAINT "course_versions_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_resources" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "course_id" INTEGER NOT NULL, + "resource_type" TEXT NOT NULL, + "resource_name" TEXT NOT NULL, + "file_url" TEXT NOT NULL, + "file_size" INTEGER, + "mime_type" TEXT, + "metadata" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "course_resources_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_scripts" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "course_id" INTEGER NOT NULL, + "step_index" INTEGER NOT NULL, + "step_name" TEXT NOT NULL, + "step_type" TEXT NOT NULL, + "duration" INTEGER NOT NULL, + "objective" TEXT, + "teacher_script" TEXT, + "interaction_points" TEXT, + "resource_ids" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "course_scripts_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_script_pages" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "script_id" INTEGER NOT NULL, + "page_number" INTEGER NOT NULL, + "questions" TEXT, + "interaction_component" TEXT, + "teacher_notes" TEXT, + "resource_ids" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "course_script_pages_script_id_fkey" FOREIGN KEY ("script_id") REFERENCES "course_scripts" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "course_activities" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "course_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "domain" TEXT, + "domain_tag_id" INTEGER, + "activity_type" TEXT NOT NULL, + "duration" INTEGER, + "online_materials" TEXT, + "offlineMaterials" TEXT, + "activityGuide" TEXT, + "objectives" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "course_activities_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "lessons" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "teacher_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "course_id" INTEGER NOT NULL, + "schedule_plan_id" INTEGER, + "planned_datetime" DATETIME, + "start_datetime" DATETIME, + "end_datetime" DATETIME, + "actual_duration" INTEGER, + "status" TEXT NOT NULL DEFAULT 'PLANNED', + "overall_rating" TEXT, + "participation_rating" TEXT, + "completion_note" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "lessons_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "lessons_schedule_plan_id_fkey" FOREIGN KEY ("schedule_plan_id") REFERENCES "schedule_plans" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "lesson_feedbacks" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "lesson_id" INTEGER NOT NULL, + "teacher_id" INTEGER NOT NULL, + "design_quality" INTEGER, + "participation" INTEGER, + "goal_achievement" INTEGER, + "step_feedbacks" TEXT, + "pros" TEXT, + "suggestions" TEXT, + "activities_done" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "lesson_feedbacks_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "lesson_feedbacks_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "student_records" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "lesson_id" INTEGER NOT NULL, + "student_id" INTEGER NOT NULL, + "focus" INTEGER, + "participation" INTEGER, + "interest" INTEGER, + "understanding" INTEGER, + "domainAchievements" TEXT, + "notes" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "student_records_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "student_records_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "tags" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "level" INTEGER NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "parent_id" INTEGER, + "description" TEXT, + "metadata" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "tenant_courses" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "course_id" INTEGER NOT NULL, + "authorized" BOOLEAN NOT NULL DEFAULT true, + "authorized_at" DATETIME, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "tenant_courses_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tenant_courses_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "growth_records" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "student_id" INTEGER NOT NULL, + "class_id" INTEGER, + "record_type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT, + "images" TEXT, + "record_date" DATETIME NOT NULL, + "created_by" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "growth_records_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "growth_records_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "growth_records_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "tasks" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "task_type" TEXT NOT NULL, + "target_type" TEXT NOT NULL, + "related_course_id" INTEGER, + "created_by" INTEGER NOT NULL, + "start_date" DATETIME NOT NULL, + "end_date" DATETIME NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PUBLISHED', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "tasks_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tasks_related_course_id_fkey" FOREIGN KEY ("related_course_id") REFERENCES "courses" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "task_targets" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "task_id" INTEGER NOT NULL, + "class_id" INTEGER, + "student_id" INTEGER, + CONSTRAINT "task_targets_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "task_completions" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "task_id" INTEGER NOT NULL, + "student_id" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "completed_at" DATETIME, + "feedback" TEXT, + "parent_feedback" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "task_completions_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "task_completions_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "task_templates" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "task_type" TEXT NOT NULL, + "related_course_id" INTEGER, + "default_duration" INTEGER NOT NULL DEFAULT 7, + "is_default" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "created_by" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "task_templates_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "task_templates_related_course_id_fkey" FOREIGN KEY ("related_course_id") REFERENCES "courses" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "resource_libraries" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "library_type" TEXT NOT NULL, + "description" TEXT, + "cover_image" TEXT, + "tenant_id" INTEGER, + "created_by" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PUBLISHED', + "sort_order" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "resource_items" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "library_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "file_type" TEXT NOT NULL, + "file_path" TEXT NOT NULL, + "file_size" INTEGER, + "tags" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "resource_items_library_id_fkey" FOREIGN KEY ("library_id") REFERENCES "resource_libraries" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "system_settings" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "school_name" TEXT, + "school_logo" TEXT, + "address" TEXT, + "notify_on_lesson" BOOLEAN NOT NULL DEFAULT true, + "notify_on_task" BOOLEAN NOT NULL DEFAULT true, + "notify_on_growth" BOOLEAN NOT NULL DEFAULT false, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "system_settings_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "parents" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "email" TEXT, + "login_account" TEXT NOT NULL, + "password_hash" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + "last_login_at" DATETIME, + CONSTRAINT "parents_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "parent_students" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "parent_id" INTEGER NOT NULL, + "student_id" INTEGER NOT NULL, + "relationship" TEXT NOT NULL, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "parent_students_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "parents" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "parent_students_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "notifications" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "recipient_type" TEXT NOT NULL, + "recipient_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "notification_type" TEXT NOT NULL, + "related_type" TEXT, + "related_id" INTEGER, + "is_read" BOOLEAN NOT NULL DEFAULT false, + "read_at" DATETIME, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "notifications_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "class_teachers" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "class_id" INTEGER NOT NULL, + "teacher_id" INTEGER NOT NULL, + "role" TEXT NOT NULL DEFAULT 'MAIN', + "isPrimary" BOOLEAN NOT NULL DEFAULT false, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "class_teachers_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "class_teachers_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "student_class_history" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "student_id" INTEGER NOT NULL, + "from_class_id" INTEGER, + "to_class_id" INTEGER NOT NULL, + "reason" TEXT, + "operated_by" INTEGER, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "student_class_history_student_id_fkey" FOREIGN KEY ("student_id") REFERENCES "students" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "student_class_history_from_class_id_fkey" FOREIGN KEY ("from_class_id") REFERENCES "classes" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "student_class_history_to_class_id_fkey" FOREIGN KEY ("to_class_id") REFERENCES "classes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "schedule_plans" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "class_id" INTEGER NOT NULL, + "course_id" INTEGER NOT NULL, + "teacher_id" INTEGER, + "scheduled_date" DATETIME, + "scheduled_time" TEXT, + "week_day" INTEGER, + "repeat_type" TEXT NOT NULL DEFAULT 'NONE', + "repeat_end_date" DATETIME, + "source" TEXT NOT NULL DEFAULT 'SCHOOL', + "created_by" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "note" TEXT, + "reminder_sent" BOOLEAN NOT NULL DEFAULT false, + "reminder_sent_at" DATETIME, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "schedule_plans_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "schedule_plans_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "schedule_plans_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "schedule_plans_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "schedule_templates" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "course_id" INTEGER NOT NULL, + "class_id" INTEGER, + "teacher_id" INTEGER, + "scheduled_time" TEXT, + "week_day" INTEGER, + "duration" INTEGER NOT NULL DEFAULT 25, + "is_default" BOOLEAN NOT NULL DEFAULT false, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME NOT NULL, + CONSTRAINT "schedule_templates_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "schedule_templates_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "schedule_templates_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classes" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "schedule_templates_teacher_id_fkey" FOREIGN KEY ("teacher_id") REFERENCES "teachers" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "operation_logs" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tenant_id" INTEGER, + "user_id" INTEGER NOT NULL, + "user_type" TEXT NOT NULL, + "action" TEXT NOT NULL, + "module" TEXT NOT NULL, + "description" TEXT NOT NULL, + "target_id" INTEGER, + "old_value" TEXT, + "new_value" TEXT, + "ip_address" TEXT, + "user_agent" TEXT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "operation_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "tenants_login_account_key" ON "tenants"("login_account"); + +-- CreateIndex +CREATE UNIQUE INDEX "teachers_login_account_key" ON "teachers"("login_account"); + +-- CreateIndex +CREATE INDEX "courses_status_idx" ON "courses"("status"); + +-- CreateIndex +CREATE INDEX "course_versions_course_id_idx" ON "course_versions"("course_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "course_scripts_course_id_step_index_key" ON "course_scripts"("course_id", "step_index"); + +-- CreateIndex +CREATE UNIQUE INDEX "course_script_pages_script_id_page_number_key" ON "course_script_pages"("script_id", "page_number"); + +-- CreateIndex +CREATE UNIQUE INDEX "lesson_feedbacks_lesson_id_teacher_id_key" ON "lesson_feedbacks"("lesson_id", "teacher_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "student_records_lesson_id_student_id_key" ON "student_records"("lesson_id", "student_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "tags_code_key" ON "tags"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_courses_tenant_id_course_id_key" ON "tenant_courses"("tenant_id", "course_id"); + +-- CreateIndex +CREATE INDEX "growth_records_tenant_id_student_id_idx" ON "growth_records"("tenant_id", "student_id"); + +-- CreateIndex +CREATE INDEX "growth_records_tenant_id_class_id_idx" ON "growth_records"("tenant_id", "class_id"); + +-- CreateIndex +CREATE INDEX "tasks_tenant_id_status_idx" ON "tasks"("tenant_id", "status"); + +-- CreateIndex +CREATE INDEX "task_targets_task_id_class_id_idx" ON "task_targets"("task_id", "class_id"); + +-- CreateIndex +CREATE INDEX "task_targets_task_id_student_id_idx" ON "task_targets"("task_id", "student_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "task_completions_task_id_student_id_key" ON "task_completions"("task_id", "student_id"); + +-- CreateIndex +CREATE INDEX "task_templates_tenant_id_status_idx" ON "task_templates"("tenant_id", "status"); + +-- CreateIndex +CREATE INDEX "resource_libraries_library_type_status_idx" ON "resource_libraries"("library_type", "status"); + +-- CreateIndex +CREATE INDEX "resource_items_library_id_idx" ON "resource_items"("library_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "system_settings_tenant_id_key" ON "system_settings"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "parents_login_account_key" ON "parents"("login_account"); + +-- CreateIndex +CREATE UNIQUE INDEX "parent_students_parent_id_student_id_key" ON "parent_students"("parent_id", "student_id"); + +-- CreateIndex +CREATE INDEX "notifications_tenant_id_recipient_type_recipient_id_idx" ON "notifications"("tenant_id", "recipient_type", "recipient_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "class_teachers_class_id_teacher_id_key" ON "class_teachers"("class_id", "teacher_id"); + +-- CreateIndex +CREATE INDEX "schedule_plans_tenant_id_class_id_idx" ON "schedule_plans"("tenant_id", "class_id"); + +-- CreateIndex +CREATE INDEX "schedule_plans_tenant_id_teacher_id_idx" ON "schedule_plans"("tenant_id", "teacher_id"); + +-- CreateIndex +CREATE INDEX "schedule_plans_tenant_id_scheduled_date_idx" ON "schedule_plans"("tenant_id", "scheduled_date"); + +-- CreateIndex +CREATE INDEX "schedule_templates_tenant_id_idx" ON "schedule_templates"("tenant_id"); + +-- CreateIndex +CREATE INDEX "schedule_templates_tenant_id_course_id_idx" ON "schedule_templates"("tenant_id", "course_id"); + +-- CreateIndex +CREATE INDEX "operation_logs_tenant_id_user_id_idx" ON "operation_logs"("tenant_id", "user_id"); + +-- CreateIndex +CREATE INDEX "operation_logs_tenant_id_created_at_idx" ON "operation_logs"("tenant_id", "created_at"); + +-- CreateIndex +CREATE INDEX "operation_logs_action_module_idx" ON "operation_logs"("action", "module"); diff --git a/reading-platform-backend/prisma/migrations/migration_lock.toml b/reading-platform-backend/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/reading-platform-backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/reading-platform-backend/prisma/schema-v2.prisma b/reading-platform-backend/prisma/schema-v2.prisma new file mode 100644 index 0000000..ad3c4af --- /dev/null +++ b/reading-platform-backend/prisma/schema-v2.prisma @@ -0,0 +1,1129 @@ +// prisma/schema-v2.prisma - 重构版数据模型 +// 创建时间:2026-02-27 +// 基于设计文档:21-数据模型重构设计.md +// +// 迁移说明: +// 1. 本文件为新版数据模型,与原 schema.prisma 共存 +// 2. 采用渐进式迁移策略,保留旧表以确保向后兼容 +// 3. 迁移完成后可将旧表标记为废弃 + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +// ==================== 主题字典(新增) ==================== + +model Theme { + id Int @id @default(autoincrement()) + + name String @unique + description String? + + sortOrder Int @default(0) @map("sort_order") + status String @default("ACTIVE") // ACTIVE, ARCHIVED + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + courses Course[] + + @@index([sortOrder]) + @@map("themes") +} + +// ==================== 课程套餐(新增) ==================== + +model CoursePackage { + id Int @id @default(autoincrement()) + + name String + description String? + + price Int @default(0) + discountPrice Int? @map("discount_price") + discountType String? @map("discount_type") + + gradeLevels String @default("[]") @map("grade_levels") + + status String @default("DRAFT") + + submittedAt DateTime? @map("submitted_at") + submittedBy Int? @map("submitted_by") + reviewedAt DateTime? @map("reviewed_at") + reviewedBy Int? @map("reviewed_by") + reviewComment String? @map("review_comment") + + courseCount Int @default(0) @map("course_count") + tenantCount Int @default(0) @map("tenant_count") + + createdBy Int? @map("created_by") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + publishedAt DateTime? @map("published_at") + + courses CoursePackageCourse[] + tenantPackages TenantPackage[] + + @@index([status]) + @@map("course_packages") +} + +// ==================== 套餐-课程包关联(新增) ==================== + +model CoursePackageCourse { + id Int @id @default(autoincrement()) + + packageId Int @map("package_id") + courseId Int @map("course_id") + + gradeLevel String @map("grade_level") + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + package CoursePackage @relation(fields: [packageId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@unique([packageId, courseId]) + @@index([packageId, gradeLevel]) + @@map("course_package_courses") +} + +// ==================== 租户-套餐授权(新增) ==================== + +model TenantPackage { + id Int @id @default(autoincrement()) + + tenantId Int @map("tenant_id") + packageId Int @map("package_id") + + startDate String @map("start_date") + endDate String @map("end_date") + + status String @default("ACTIVE") + + pricePaid Int @default(0) @map("price_paid") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + package CoursePackage @relation(fields: [packageId], references: [id], onDelete: Cascade) + + @@unique([tenantId, packageId]) + @@index([tenantId, status]) + @@map("tenant_packages") +} + +// ==================== 租户 ==================== + +model Tenant { + id Int @id @default(autoincrement()) + name String + loginAccount String? @unique @map("login_account") + passwordHash String? @map("password_hash") + address String? + contactPerson String? @map("contact_person") + contactPhone String? @map("contact_phone") + logoUrl String? @map("logo_url") + + packageType String @default("STANDARD") @map("package_type") + teacherQuota Int @default(20) @map("teacher_quota") + studentQuota Int @default(200) @map("student_quota") + storageQuota BigInt @default(5368709120) @map("storage_quota") + + startDate String @map("start_date") + expireDate String @map("expire_date") + + teacherCount Int @default(0) @map("teacher_count") + studentCount Int @default(0) @map("student_count") + storageUsed BigInt @default(0) @map("storage_used") + + status String @default("ACTIVE") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + teachers Teacher[] + students Student[] + classes Class[] + lessons Lesson[] + tenantCourses TenantCourse[] + growthRecords GrowthRecord[] + tasks Task[] + taskTemplates TaskTemplate[] + parents Parent[] + notifications Notification[] + settings SystemSettings? + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + operationLogs OperationLog[] + + // 新增关联 + packages TenantPackage[] + schoolCourses SchoolCourse[] + + @@map("tenants") +} + +// ==================== 教师 ==================== + +model Teacher { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + phone String + email String? + + loginAccount String @unique @map("login_account") + passwordHash String @map("password_hash") + classIds String? @default("[]") @map("class_ids") + + status String @default("ACTIVE") + + lessonCount Int @default(0) @map("lesson_count") + feedbackCount Int @default(0) @map("feedback_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + lastLoginAt DateTime? @map("last_login_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + classes Class[] + lessons Lesson[] + feedbacks LessonFeedback[] + classTeachers ClassTeacher[] + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + + @@map("teachers") +} + +// ==================== 班级 ==================== + +model Class { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + grade String + + teacherId Int? @map("teacher_id") + + studentCount Int @default(0) @map("student_count") + lessonCount Int @default(0) @map("lesson_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + teacher Teacher? @relation(fields: [teacherId], references: [id]) + students Student[] + lessons Lesson[] + growthRecords GrowthRecord[] + classTeachers ClassTeacher[] + studentHistory StudentClassHistory[] @relation("ToClass") + fromHistory StudentClassHistory[] @relation("FromClass") + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + + @@map("classes") +} + +// ==================== 学生 ==================== + +model Student { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + classId Int @map("class_id") + name String + gender String? + birthDate DateTime? @map("birth_date") + parentPhone String? @map("parent_phone") + parentName String? @map("parent_name") + + readingCount Int @default(0) @map("reading_count") + lessonCount Int @default(0) @map("lesson_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + class Class @relation(fields: [classId], references: [id], onDelete: Cascade) + records StudentRecord[] + growthRecords GrowthRecord[] + taskCompletions TaskCompletion[] + parents ParentStudent[] + classHistory StudentClassHistory[] + + @@map("students") +} + +// ==================== 课程包(重构) ==================== + +model Course { + id Int @id @default(autoincrement()) + + // 新增字段 + themeId Int? @map("theme_id") + coreContent String? @map("core_content") + + introSummary String? @map("intro_summary") + introHighlights String? @map("intro_highlights") + introGoals String? @map("intro_goals") + introSchedule String? @map("intro_schedule") + introKeyPoints String? @map("intro_key_points") + introMethods String? @map("intro_methods") + introEvaluation String? @map("intro_evaluation") + introNotes String? @map("intro_notes") + + scheduleRefData String? @map("schedule_ref_data") + + hasCollectiveLesson Boolean @default(false) @map("has_collective_lesson") + + // 保留字段 + name String + description String? + + pictureBookId Int? @map("picture_book_id") + pictureBookName String? @map("picture_book_name") + coverImagePath String? @map("cover_image_path") + + ebookPaths String? @map("ebook_paths") + audioPaths String? @map("audio_paths") + videoPaths String? @map("video_paths") + otherResources String? @map("other_resources") + + pptPath String? @map("ppt_path") + pptName String? @map("ppt_name") + posterPaths String? @map("poster_paths") + tools String? @map("tools") + studentMaterials String? @map("student_materials") + + lessonPlanData String? @map("lesson_plan_data") + activitiesData String? @map("activities_data") + assessmentData String? @map("assessment_data") + + gradeTags String @default("[]") @map("grade_tags") + domainTags String @default("[]") @map("domain_tags") + + duration Int @default(25) + + status String @default("DRAFT") + version String @default("1.0") + + submittedAt DateTime? @map("submitted_at") + submittedBy Int? @map("submitted_by") + reviewedAt DateTime? @map("reviewed_at") + reviewedBy Int? @map("reviewed_by") + reviewComment String? @map("review_comment") + reviewChecklist String? @map("review_checklist") + + parentId Int? @map("parent_id") + isLatest Boolean @default(true) + + usageCount Int @default(0) @map("usage_count") + teacherCount Int @default(0) @map("teacher_count") + avgRating Float @default(0) @map("avg_rating") + + createdBy Int? @map("created_by") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + publishedAt DateTime? @map("published_at") + + // 关联 + theme Theme? @relation(fields: [themeId], references: [id]) + + packageCourses CoursePackageCourse[] + courseLessons CourseLesson[] + schoolCourses SchoolCourse[] + + resources CourseResource[] + scripts CourseScript[] + activities CourseActivity[] + lessons Lesson[] + tenantCourses TenantCourse[] + versions CourseVersion[] + tasks Task[] + taskTemplates TaskTemplate[] + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + + @@index([themeId]) + @@index([status]) + @@map("courses") +} + +// ==================== 课程(新增) ==================== + +model CourseLesson { + id Int @id @default(autoincrement()) + + courseId Int @map("course_id") + + lessonType String @map("lesson_type") + name String + description String? + + duration Int @default(25) + + videoPath String? @map("video_path") + videoName String? @map("video_name") + pptPath String? @map("ppt_path") + pptName String? @map("ppt_name") + pdfPath String? @map("pdf_path") + pdfName String? @map("pdf_name") + + objectives String? + preparation String? + extension String? + reflection String? + + assessmentData String? @map("assessment_data") + + useTemplate Boolean @default(false) @map("use_template") + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + steps LessonStep[] + + @@unique([courseId, lessonType]) + @@index([courseId]) + @@map("course_lessons") +} + +// ==================== 教学环节(新增) ==================== + +model LessonStep { + id Int @id @default(autoincrement()) + + lessonId Int @map("lesson_id") + + name String + content String? + + duration Int @default(5) + + objective String? + + resourceIds String? @map("resource_ids") + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + lesson CourseLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + stepResources LessonStepResource[] + + @@index([lessonId]) + @@map("lesson_steps") +} + +// ==================== 环节资源关联(新增) ==================== + +model LessonStepResource { + id Int @id @default(autoincrement()) + + stepId Int @map("step_id") + resourceId Int @map("resource_id") + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + step LessonStep @relation(fields: [stepId], references: [id], onDelete: Cascade) + resource CourseResource @relation(fields: [resourceId], references: [id], onDelete: Cascade) + + @@unique([stepId, resourceId]) + @@map("lesson_step_resources") +} + +// ==================== 校本课程包(新增) ==================== + +model SchoolCourse { + id Int @id @default(autoincrement()) + + tenantId Int @map("tenant_id") + sourceCourseId Int @map("source_course_id") + + name String + description String? + + createdBy Int @map("created_by") + + changesSummary String? @map("changes_summary") + changesData String? @map("changes_data") + + usageCount Int @default(0) @map("usage_count") + + status String @default("ACTIVE") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + sourceCourse Course @relation(fields: [sourceCourseId], references: [id]) + lessons SchoolCourseLesson[] + reservations SchoolCourseReservation[] + + @@index([tenantId]) + @@index([sourceCourseId]) + @@map("school_courses") +} + +// ==================== 校本课程(新增) ==================== + +model SchoolCourseLesson { + id Int @id @default(autoincrement()) + + schoolCourseId Int @map("school_course_id") + sourceLessonId Int @map("source_lesson_id") + + lessonType String @map("lesson_type") + + objectives String? + preparation String? + extension String? + reflection String? + + changeNote String? @map("change_note") + + stepsData String? @map("steps_data") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + schoolCourse SchoolCourse @relation(fields: [schoolCourseId], references: [id], onDelete: Cascade) + + @@unique([schoolCourseId, lessonType]) + @@map("school_course_lessons") +} + +// ==================== 校本课程预约(新增) ==================== + +model SchoolCourseReservation { + id Int @id @default(autoincrement()) + + schoolCourseId Int @map("school_course_id") + + teacherId Int @map("teacher_id") + classId Int @map("class_id") + + scheduledDate String @map("scheduled_date") + scheduledTime String? @map("scheduled_time") + + status String @default("PENDING") + + note String? + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + schoolCourse SchoolCourse @relation(fields: [schoolCourseId], references: [id], onDelete: Cascade) + + @@index([schoolCourseId, scheduledDate]) + @@map("school_course_reservations") +} + +// ==================== 课程资源 ==================== + +model CourseResource { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + resourceType String @map("resource_type") + resourceName String @map("resource_name") + fileUrl String @map("file_url") + fileSize Int? @map("file_size") + mimeType String? @map("mime_type") + metadata String? + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + stepResources LessonStepResource[] + + @@map("course_resources") +} + +// ==================== 课程脚本(废弃但保留) ==================== + +model CourseScript { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + stepIndex Int @map("step_index") + stepName String @map("step_name") + stepType String @map("step_type") + + duration Int + objective String? + teacherScript String? @map("teacher_script") + interactionPoints String? @map("interaction_points") + + resourceIds String? @map("resource_ids") + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + pages CourseScriptPage[] + + @@unique([courseId, stepIndex]) + @@map("course_scripts") +} + +// ==================== 逐页配置(废弃但保留) ==================== + +model CourseScriptPage { + id Int @id @default(autoincrement()) + scriptId Int @map("script_id") + pageNumber Int @map("page_number") + questions String? + interactionComponent String? @map("interaction_component") + teacherNotes String? @map("teacher_notes") + resourceIds String? @map("resource_ids") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + script CourseScript @relation(fields: [scriptId], references: [id], onDelete: Cascade) + + @@unique([scriptId, pageNumber]) + @@map("course_script_pages") +} + +// ==================== 延伸活动(废弃但保留) ==================== + +model CourseActivity { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + name String + + domain String? + domainTagId Int? @map("domain_tag_id") + activityType String @map("activity_type") + duration Int? + + onlineMaterials String? @map("online_materials") + offlineMaterials String? + activityGuide String? + objectives String? + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@map("course_activities") +} + +// ==================== 课程版本历史 ==================== + +model CourseVersion { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + version String + snapshotData String + changeLog String? + publishedAt DateTime @default(now()) @map("published_at") + publishedBy Int @map("published_by") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@index([courseId]) + @@map("course_versions") +} + +// ==================== 授课记录 ==================== + +model Lesson { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + teacherId Int @map("teacher_id") + classId Int @map("class_id") + courseId Int @map("course_id") + schedulePlanId Int? @map("schedule_plan_id") + + plannedDatetime DateTime? @map("planned_datetime") + startDatetime DateTime? @map("start_datetime") + endDatetime DateTime? @map("end_datetime") + actualDuration Int? @map("actual_duration") + + status String @default("PLANNED") + + overallRating String? @map("overall_rating") + participationRating String? @map("participation_rating") + completionNote String? @map("completion_note") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + teacher Teacher @relation(fields: [teacherId], references: [id]) + class Class @relation(fields: [classId], references: [id]) + course Course @relation(fields: [courseId], references: [id]) + schedulePlan SchedulePlan? @relation(fields: [schedulePlanId], references: [id]) + feedbacks LessonFeedback[] + records StudentRecord[] + + @@map("lessons") +} + +// ==================== 课程反馈 ==================== + +model LessonFeedback { + id Int @id @default(autoincrement()) + lessonId Int @map("lesson_id") + teacherId Int @map("teacher_id") + + designQuality Int? @map("design_quality") + participation Int? + goalAchievement Int? @map("goal_achievement") + + stepFeedbacks String? @map("step_feedbacks") + + pros String? + suggestions String? + + activitiesDone String? @map("activities_done") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + teacher Teacher @relation(fields: [teacherId], references: [id]) + + @@unique([lessonId, teacherId]) + @@map("lesson_feedbacks") +} + +// ==================== 学生测评记录 ==================== + +model StudentRecord { + id Int @id @default(autoincrement()) + lessonId Int @map("lesson_id") + studentId Int @map("student_id") + + focus Int? + participation Int? + interest Int? + understanding Int? + + domainAchievements String? + + notes String? + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id]) + + @@unique([lessonId, studentId]) + @@map("student_records") +} + +// ==================== 标签体系 ==================== + +model Tag { + id Int @id @default(autoincrement()) + level Int + code String @unique + name String + parentId Int? @map("parent_id") + + description String? + metadata String? + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + @@map("tags") +} + +// ==================== 租户课程授权 ==================== + +model TenantCourse { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + courseId Int @map("course_id") + + authorized Boolean @default(true) + authorizedAt DateTime? @map("authorized_at") + + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@unique([tenantId, courseId]) + @@map("tenant_courses") +} + +// ==================== 成长档案 ==================== + +model GrowthRecord { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + studentId Int @map("student_id") + classId Int? @map("class_id") + recordType String @map("record_type") + title String + content String? + images String? @map("images") + recordDate DateTime @map("record_date") + createdBy Int @map("created_by") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) + class Class? @relation(fields: [classId], references: [id], onDelete: SetNull) + + @@index([tenantId, studentId]) + @@index([tenantId, classId]) + @@map("growth_records") +} + +// ==================== 阅读任务 ==================== + +model Task { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + title String + description String? + taskType String @map("task_type") + targetType String @map("target_type") + relatedCourseId Int? @map("related_course_id") + createdBy Int @map("created_by") + startDate DateTime @map("start_date") + endDate DateTime @map("end_date") + status String @default("PUBLISHED") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course? @relation(fields: [relatedCourseId], references: [id], onDelete: SetNull) + targets TaskTarget[] + completions TaskCompletion[] + + @@index([tenantId, status]) + @@map("tasks") +} + +model TaskTarget { + id Int @id @default(autoincrement()) + taskId Int @map("task_id") + classId Int? @map("class_id") + studentId Int? @map("student_id") + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + + @@index([taskId, classId]) + @@index([taskId, studentId]) + @@map("task_targets") +} + +model TaskCompletion { + id Int @id @default(autoincrement()) + taskId Int @map("task_id") + studentId Int @map("student_id") + status String @default("PENDING") + completedAt DateTime? @map("completed_at") + feedback String? + parentFeedback String? @map("parent_feedback") + createdAt DateTime @default(now()) + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) + + @@unique([taskId, studentId]) + @@map("task_completions") +} + +// ==================== 任务模板 ==================== + +model TaskTemplate { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + description String? + taskType String @map("task_type") + relatedCourseId Int? @map("related_course_id") + defaultDuration Int @default(7) @map("default_duration") + isDefault Boolean @default(false) @map("is_default") + status String @default("ACTIVE") + createdBy Int @map("created_by") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course? @relation(fields: [relatedCourseId], references: [id], onDelete: SetNull) + + @@index([tenantId, status]) + @@map("task_templates") +} + +// ==================== 资源库 ==================== + +model ResourceLibrary { + id Int @id @default(autoincrement()) + name String + libraryType String @map("library_type") + description String? + coverImage String? @map("cover_image") + tenantId Int? @map("tenant_id") + createdBy Int @map("created_by") + status String @default("PUBLISHED") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + items ResourceItem[] + + @@index([libraryType, status]) + @@map("resource_libraries") +} + +model ResourceItem { + id Int @id @default(autoincrement()) + libraryId Int @map("library_id") + title String + description String? + fileType String @map("file_type") + filePath String @map("file_path") + fileSize Int? @map("file_size") + tags String? + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) + + library ResourceLibrary @relation(fields: [libraryId], references: [id], onDelete: Cascade) + + @@index([libraryId]) + @@map("resource_items") +} + +// ==================== 系统设置 ==================== + +model SystemSettings { + id Int @id @default(autoincrement()) + tenantId Int @unique @map("tenant_id") + schoolName String? @map("school_name") + schoolLogo String? @map("school_logo") + address String? + notifyOnLesson Boolean @default(true) @map("notify_on_lesson") + notifyOnTask Boolean @default(true) @map("notify_on_task") + notifyOnGrowth Boolean @default(false) @map("notify_on_growth") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + + @@map("system_settings") +} + +// ==================== 家长 ==================== + +model Parent { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + phone String + email String? + loginAccount String @unique @map("login_account") + passwordHash String @map("password_hash") + status String @default("ACTIVE") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + lastLoginAt DateTime? @map("last_login_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + children ParentStudent[] + + @@map("parents") +} + +// ==================== 家长-学生关联 ==================== + +model ParentStudent { + id Int @id @default(autoincrement()) + parentId Int @map("parent_id") + studentId Int @map("student_id") + relationship String + createdAt DateTime @default(now()) @map("created_at") + + parent Parent @relation(fields: [parentId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) + + @@unique([parentId, studentId]) + @@map("parent_students") +} + +// ==================== 通知 ==================== + +model Notification { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + recipientType String @map("recipient_type") + recipientId Int @map("recipient_id") + title String + content String + notificationType String @map("notification_type") + relatedType String? @map("related_type") + relatedId Int? @map("related_id") + isRead Boolean @default(false) @map("is_read") + readAt DateTime? @map("read_at") + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + + @@index([tenantId, recipientType, recipientId]) + @@map("notifications") +} + +// ==================== 班级教师关联 ==================== + +model ClassTeacher { + id Int @id @default(autoincrement()) + classId Int @map("class_id") + teacherId Int @map("teacher_id") + role String @default("MAIN") + isPrimary Boolean @default(false) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + class Class @relation(fields: [classId], references: [id], onDelete: Cascade) + teacher Teacher @relation(fields: [teacherId], references: [id], onDelete: Cascade) + + @@unique([classId, teacherId]) + @@map("class_teachers") +} + +// ==================== 学生调班历史 ==================== + +model StudentClassHistory { + id Int @id @default(autoincrement()) + studentId Int @map("student_id") + fromClassId Int? @map("from_class_id") + toClassId Int @map("to_class_id") + reason String? + operatedBy Int? @map("operated_by") + createdAt DateTime @default(now()) @map("created_at") + + student Student @relation(fields: [studentId], references: [id]) + fromClass Class? @relation("FromClass", fields: [fromClassId], references: [id]) + toClass Class @relation("ToClass", fields: [toClassId], references: [id]) + + @@map("student_class_history") +} + +// ==================== 排课计划 ==================== + +model SchedulePlan { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + classId Int @map("class_id") + courseId Int @map("course_id") + teacherId Int? @map("teacher_id") + + scheduledDate DateTime? @map("scheduled_date") + scheduledTime String? @map("scheduled_time") + weekDay Int? @map("week_day") + + repeatType String @default("NONE") @map("repeat_type") + repeatEndDate DateTime? @map("repeat_end_date") + + source String @default("SCHOOL") + createdBy Int @map("created_by") + + status String @default("ACTIVE") + + note String? + + reminderSent Boolean @default(false) @map("reminder_sent") + reminderSentAt DateTime? @map("reminder_sent_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + class Class @relation(fields: [classId], references: [id]) + course Course @relation(fields: [courseId], references: [id]) + teacher Teacher? @relation(fields: [teacherId], references: [id]) + lessons Lesson[] + + @@index([tenantId, classId]) + @@index([tenantId, teacherId]) + @@index([tenantId, scheduledDate]) + @@map("schedule_plans") +} + +// ==================== 排课模板 ==================== + +model ScheduleTemplate { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + courseId Int @map("course_id") + classId Int? @map("class_id") + teacherId Int? @map("teacher_id") + + scheduledTime String? @map("scheduled_time") + weekDay Int? @map("week_day") + duration Int @default(25) + + isDefault Boolean @default(false) @map("is_default") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id]) + class Class? @relation(fields: [classId], references: [id]) + teacher Teacher? @relation(fields: [teacherId], references: [id]) + + @@index([tenantId]) + @@index([tenantId, courseId]) + @@map("schedule_templates") +} + +// ==================== 操作日志 ==================== + +model OperationLog { + id Int @id @default(autoincrement()) + tenantId Int? @map("tenant_id") + userId Int @map("user_id") + userType String @map("user_type") + action String + module String + description String + targetId Int? @map("target_id") + oldValue String? @map("old_value") + newValue String? @map("new_value") + ipAddress String? @map("ip_address") + userAgent String? @map("user_agent") + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull) + + @@index([tenantId, userId]) + @@index([tenantId, createdAt]) + @@index([action, module]) + @@map("operation_logs") +} diff --git a/reading-platform-backend/prisma/schema.prisma b/reading-platform-backend/prisma/schema.prisma new file mode 100644 index 0000000..28c04dc --- /dev/null +++ b/reading-platform-backend/prisma/schema.prisma @@ -0,0 +1,1073 @@ +// prisma/schema.prisma - SQLite版本(快速启动) + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +// ==================== 租户 ==================== + +model Tenant { + id Int @id @default(autoincrement()) + name String + loginAccount String? @unique @map("login_account") + passwordHash String? @map("password_hash") + address String? + contactPerson String? @map("contact_person") + contactPhone String? @map("contact_phone") + logoUrl String? @map("logo_url") + + packageType String @default("STANDARD") @map("package_type") + teacherQuota Int @default(20) @map("teacher_quota") + studentQuota Int @default(200) @map("student_quota") + storageQuota BigInt @default(5368709120) @map("storage_quota") + + startDate String @map("start_date") + expireDate String @map("expire_date") + + teacherCount Int @default(0) @map("teacher_count") + studentCount Int @default(0) @map("student_count") + storageUsed BigInt @default(0) @map("storage_used") + + status String @default("ACTIVE") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + teachers Teacher[] + students Student[] + classes Class[] + lessons Lesson[] + tenantCourses TenantCourse[] + growthRecords GrowthRecord[] + tasks Task[] + taskTemplates TaskTemplate[] + parents Parent[] + notifications Notification[] + settings SystemSettings? + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + operationLogs OperationLog[] + schoolCourses SchoolCourse[] + tenantPackages TenantPackage[] + + @@map("tenants") +} + +// ==================== 教师 ==================== + +model Teacher { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + phone String + email String? + + loginAccount String @unique @map("login_account") + passwordHash String @map("password_hash") + classIds String? @default("[]") @map("class_ids") // 保留,向后兼容(只读缓存) + + status String @default("ACTIVE") + + lessonCount Int @default(0) @map("lesson_count") + feedbackCount Int @default(0) @map("feedback_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + lastLoginAt DateTime? @map("last_login_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + classes Class[] + lessons Lesson[] + feedbacks LessonFeedback[] + classTeachers ClassTeacher[] // 新增:多对多关联 + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + + @@map("teachers") +} + +// ==================== 班级 ==================== + +model Class { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + grade String + + teacherId Int? @map("teacher_id") // 保留,向后兼容 + + studentCount Int @default(0) @map("student_count") + lessonCount Int @default(0) @map("lesson_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + teacher Teacher? @relation(fields: [teacherId], references: [id]) + students Student[] + lessons Lesson[] + growthRecords GrowthRecord[] + classTeachers ClassTeacher[] // 新增:多对多关联 + studentHistory StudentClassHistory[] @relation("ToClass") // 新增:调班历史 + fromHistory StudentClassHistory[] @relation("FromClass") // 新增:调班历史 + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + + @@map("classes") +} + +// ==================== 学生 ==================== + +model Student { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + classId Int @map("class_id") + name String + gender String? + birthDate DateTime? @map("birth_date") + parentPhone String? @map("parent_phone") + parentName String? @map("parent_name") + + readingCount Int @default(0) @map("reading_count") + lessonCount Int @default(0) @map("lesson_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + class Class @relation(fields: [classId], references: [id], onDelete: Cascade) + records StudentRecord[] + growthRecords GrowthRecord[] + taskCompletions TaskCompletion[] + parents ParentStudent[] + classHistory StudentClassHistory[] // 新增:调班历史 + + @@map("students") +} + +// ==================== 课程包 ==================== + +model Course { + id Int @id @default(autoincrement()) + name String + description String? + themeId Int? @map("theme_id") + + // 新增字段 - 课程包重构 + coreContent String? @map("core_content") + + // 课程介绍(8个字段) + introSummary String? @map("intro_summary") + introHighlights String? @map("intro_highlights") + introGoals String? @map("intro_goals") + introSchedule String? @map("intro_schedule") + introKeyPoints String? @map("intro_key_points") + introMethods String? @map("intro_methods") + introEvaluation String? @map("intro_evaluation") + introNotes String? @map("intro_notes") + + // 排课计划参考 + scheduleRefData String? @map("schedule_ref_data") + + // 环创建设 + environmentConstruction String? @map("environment_construction") + + // 是否有集体课 + hasCollectiveLesson Boolean @default(false) @map("has_collective_lesson") + + pictureBookId Int? @map("picture_book_id") + pictureBookName String? @map("picture_book_name") + + // 封面图片 - 存储文件路径(不再是 base64) + coverImagePath String? @map("cover_image_path") + + // 数字资源 - 存储多文件路径(JSON数组 [{path, name}]) + ebookPaths String? @map("ebook_paths") + + audioPaths String? @map("audio_paths") + + videoPaths String? @map("video_paths") + + otherResources String? @map("other_resources") // JSON数组存储多个资源 {path, name} + + // 教学材料 - 存储文件路径(不再是 base64) + pptPath String? @map("ppt_path") + pptName String? @map("ppt_name") + + posterPaths String? @map("poster_paths") // JSON数组存储多个挂图 {path, name} + + // 实体教具和学生材料 + tools String? @map("tools") // JSON数组存储教具列表 [{name, quantity}] + studentMaterials String? @map("student_materials") // 学生材料文本 + + // 课堂计划 + lessonPlanData String? @map("lesson_plan_data") // JSON存储课堂计划数据 + + // 延伸活动 + activitiesData String? @map("activities_data") // JSON存储延伸活动数据 + + // 测评工具 + assessmentData String? @map("assessment_data") // JSON存储测评工具数据 + + gradeTags String @default("[]") @map("grade_tags") + domainTags String @default("[]") @map("domain_tags") + + duration Int @default(25) + + status String @default("DRAFT") // DRAFT, PENDING, REJECTED, PUBLISHED, ARCHIVED + version String @default("1.0") + + // 审核相关字段 + submittedAt DateTime? @map("submitted_at") // 提交审核时间 + submittedBy Int? @map("submitted_by") // 提交人ID + reviewedAt DateTime? @map("reviewed_at") // 审核时间 + reviewedBy Int? @map("reviewed_by") // 审核人ID + reviewComment String? @map("review_comment") // 审核意见 + reviewChecklist String? @map("review_checklist") // 审核检查项结果 JSON + + // 版本相关字段 + parentId Int? @map("parent_id") // 父版本ID(迭代时指向原版本) + isLatest Boolean @default(true) // 是否最新版本 + + usageCount Int @default(0) @map("usage_count") + teacherCount Int @default(0) @map("teacher_count") + avgRating Float @default(0) @map("avg_rating") + + createdBy Int? @map("created_by") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + publishedAt DateTime? @map("published_at") + + resources CourseResource[] + scripts CourseScript[] + activities CourseActivity[] + lessons Lesson[] + tenantCourses TenantCourse[] + versions CourseVersion[] + tasks Task[] + taskTemplates TaskTemplate[] + schedulePlans SchedulePlan[] + scheduleTemplates ScheduleTemplate[] + + // V2 关联 + theme Theme? @relation(fields: [themeId], references: [id]) + packageCourses CoursePackageCourse[] + courseLessons CourseLesson[] + schoolCourses SchoolCourse[] + + @@index([status]) + @@map("courses") +} + +// ==================== 课程版本历史 ==================== + +model CourseVersion { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + version String // 版本号 + snapshotData String // JSON快照(完整课程内容) + changeLog String? // 变更说明 + publishedAt DateTime @default(now()) @map("published_at") + publishedBy Int @map("published_by") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@index([courseId]) + @@map("course_versions") +} + +// ==================== 课程资源 ==================== + +model CourseResource { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + resourceType String @map("resource_type") + resourceName String @map("resource_name") + fileUrl String @map("file_url") + fileSize Int? @map("file_size") + mimeType String? @map("mime_type") + metadata String? + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + stepResources LessonStepResource[] + + @@map("course_resources") +} + +// ==================== 课程脚本 ==================== + +model CourseScript { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + stepIndex Int @map("step_index") + stepName String @map("step_name") + stepType String @map("step_type") + + duration Int + objective String? + teacherScript String? @map("teacher_script") + interactionPoints String? @map("interaction_points") + + resourceIds String? @map("resource_ids") + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + pages CourseScriptPage[] + + @@unique([courseId, stepIndex]) + @@map("course_scripts") +} + +// ==================== 逐页配置 ==================== + +model CourseScriptPage { + id Int @id @default(autoincrement()) + scriptId Int @map("script_id") + pageNumber Int @map("page_number") + questions String? + interactionComponent String? @map("interaction_component") + teacherNotes String? @map("teacher_notes") + resourceIds String? @map("resource_ids") // 关联资源ID列表,JSON数组 + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + script CourseScript @relation(fields: [scriptId], references: [id], onDelete: Cascade) + + @@unique([scriptId, pageNumber]) + @@map("course_script_pages") +} + +// ==================== 延伸活动 ==================== + +model CourseActivity { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + name String + + domain String? + domainTagId Int? @map("domain_tag_id") + activityType String @map("activity_type") + duration Int? + + onlineMaterials String? @map("online_materials") + offlineMaterials String? + activityGuide String? + objectives String? + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@map("course_activities") +} + +// ==================== 授课记录 ==================== + +model Lesson { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + teacherId Int @map("teacher_id") + classId Int @map("class_id") + courseId Int @map("course_id") + schedulePlanId Int? @map("schedule_plan_id") + + plannedDatetime DateTime? @map("planned_datetime") + startDatetime DateTime? @map("start_datetime") + endDatetime DateTime? @map("end_datetime") + actualDuration Int? @map("actual_duration") + + status String @default("PLANNED") + + overallRating String? @map("overall_rating") + participationRating String? @map("participation_rating") + completionNote String? @map("completion_note") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + teacher Teacher @relation(fields: [teacherId], references: [id]) + class Class @relation(fields: [classId], references: [id]) + course Course @relation(fields: [courseId], references: [id]) + schedulePlan SchedulePlan? @relation(fields: [schedulePlanId], references: [id]) + feedbacks LessonFeedback[] + records StudentRecord[] + + @@map("lessons") +} + +// ==================== 课程反馈 ==================== + +model LessonFeedback { + id Int @id @default(autoincrement()) + lessonId Int @map("lesson_id") + teacherId Int @map("teacher_id") + + designQuality Int? @map("design_quality") + participation Int? + goalAchievement Int? @map("goal_achievement") + + stepFeedbacks String? @map("step_feedbacks") + + pros String? + suggestions String? + + activitiesDone String? @map("activities_done") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + teacher Teacher @relation(fields: [teacherId], references: [id]) + + @@unique([lessonId, teacherId]) + @@map("lesson_feedbacks") +} + +// ==================== 学生测评记录 ==================== + +model StudentRecord { + id Int @id @default(autoincrement()) + lessonId Int @map("lesson_id") + studentId Int @map("student_id") + + focus Int? + participation Int? + interest Int? + understanding Int? + + domainAchievements String? + + notes String? + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id]) + + @@unique([lessonId, studentId]) + @@map("student_records") +} + +// ==================== 标签体系 ==================== + +model Tag { + id Int @id @default(autoincrement()) + level Int + code String @unique + name String + parentId Int? @map("parent_id") + + description String? + metadata String? + + sortOrder Int @default(0) @map("sort_order") + + createdAt DateTime @default(now()) @map("created_at") + + @@map("tags") +} + +// ==================== 租户课程授权 ==================== + +model TenantCourse { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + courseId Int @map("course_id") + + authorized Boolean @default(true) + authorizedAt DateTime? @map("authorized_at") + + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + + @@unique([tenantId, courseId]) + @@map("tenant_courses") +} + +// ==================== 成长档案 ==================== + +model GrowthRecord { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + studentId Int @map("student_id") + classId Int? @map("class_id") + recordType String @map("record_type") // STUDENT, CLASS + title String + content String? + images String? @map("images") // JSON: ["path1", "path2"] + recordDate DateTime @map("record_date") + createdBy Int @map("created_by") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) + class Class? @relation(fields: [classId], references: [id], onDelete: SetNull) + + @@index([tenantId, studentId]) + @@index([tenantId, classId]) + @@map("growth_records") +} + +// ==================== 阅读任务 ==================== + +model Task { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + title String + description String? + taskType String @map("task_type") // READING, ACTIVITY, HOMEWORK + targetType String @map("target_type") // CLASS, STUDENT + relatedCourseId Int? @map("related_course_id") + createdBy Int @map("created_by") + startDate DateTime @map("start_date") + endDate DateTime @map("end_date") + status String @default("PUBLISHED") // DRAFT, PUBLISHED, ARCHIVED + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course? @relation(fields: [relatedCourseId], references: [id], onDelete: SetNull) + targets TaskTarget[] + completions TaskCompletion[] + + @@index([tenantId, status]) + @@map("tasks") +} + +model TaskTarget { + id Int @id @default(autoincrement()) + taskId Int @map("task_id") + classId Int? @map("class_id") + studentId Int? @map("student_id") + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + + @@index([taskId, classId]) + @@index([taskId, studentId]) + @@map("task_targets") +} + +model TaskCompletion { + id Int @id @default(autoincrement()) + taskId Int @map("task_id") + studentId Int @map("student_id") + status String @default("PENDING") // PENDING, IN_PROGRESS, COMPLETED + completedAt DateTime? @map("completed_at") + feedback String? + parentFeedback String? @map("parent_feedback") + createdAt DateTime @default(now()) + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) + + @@unique([taskId, studentId]) + @@map("task_completions") +} + +// ==================== 任务模板 ==================== + +model TaskTemplate { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + + // 模板基本信息 + name String + description String? + taskType String @map("task_type") // READING, ACTIVITY, HOMEWORK + + // 关联课程(可选) + relatedCourseId Int? @map("related_course_id") + + // 默认时间设置 + defaultDuration Int @default(7) @map("default_duration") // 默认任务天数 + + // 模板状态 + isDefault Boolean @default(false) @map("is_default") + status String @default("ACTIVE") // ACTIVE, ARCHIVED + + // 创建者 + createdBy Int @map("created_by") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course? @relation(fields: [relatedCourseId], references: [id], onDelete: SetNull) + + @@index([tenantId, status]) + @@map("task_templates") +} + +// ==================== 资源库 ==================== + +model ResourceLibrary { + id Int @id @default(autoincrement()) + name String + libraryType String @map("library_type") // PICTURE_BOOK, MATERIAL, TEMPLATE + description String? + coverImage String? @map("cover_image") + tenantId Int? @map("tenant_id") // NULL = 公共资源 + createdBy Int @map("created_by") + status String @default("PUBLISHED") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + items ResourceItem[] + + @@index([libraryType, status]) + @@map("resource_libraries") +} + +model ResourceItem { + id Int @id @default(autoincrement()) + libraryId Int @map("library_id") + title String + description String? + fileType String @map("file_type") // IMAGE, PDF, VIDEO, AUDIO, PPT, OTHER + filePath String @map("file_path") + fileSize Int? @map("file_size") + tags String? // JSON: ["tag1", "tag2"] + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) + + library ResourceLibrary @relation(fields: [libraryId], references: [id], onDelete: Cascade) + + @@index([libraryId]) + @@map("resource_items") +} + +// ==================== 系统设置 ==================== + +model SystemSettings { + id Int @id @default(autoincrement()) + tenantId Int @unique @map("tenant_id") + schoolName String? @map("school_name") + schoolLogo String? @map("school_logo") + address String? + notifyOnLesson Boolean @default(true) @map("notify_on_lesson") + notifyOnTask Boolean @default(true) @map("notify_on_task") + notifyOnGrowth Boolean @default(false) @map("notify_on_growth") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + @@map("system_settings") +} + +// ==================== 家长 ==================== + +model Parent { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + name String + phone String + email String? + loginAccount String @unique @map("login_account") + passwordHash String @map("password_hash") + status String @default("ACTIVE") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + lastLoginAt DateTime? @map("last_login_at") + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + children ParentStudent[] + @@map("parents") +} + +// ==================== 家长-学生关联 ==================== + +model ParentStudent { + id Int @id @default(autoincrement()) + parentId Int @map("parent_id") + studentId Int @map("student_id") + relationship String // FATHER, MOTHER, GRANDFATHER, GRANDMOTHER, OTHER + createdAt DateTime @default(now()) @map("created_at") + parent Parent @relation(fields: [parentId], references: [id], onDelete: Cascade) + student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) + @@unique([parentId, studentId]) + @@map("parent_students") +} + +// ==================== 通知 ==================== + +model Notification { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + recipientType String @map("recipient_type") // TEACHER, SCHOOL, PARENT + recipientId Int @map("recipient_id") + title String + content String + notificationType String @map("notification_type") // SYSTEM, TASK, LESSON, GROWTH + relatedType String? @map("related_type") + relatedId Int? @map("related_id") + isRead Boolean @default(false) @map("is_read") + readAt DateTime? @map("read_at") + createdAt DateTime @default(now()) @map("created_at") + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + @@index([tenantId, recipientType, recipientId]) + @@map("notifications") +} + +// ==================== 班级教师关联 ==================== + +model ClassTeacher { + id Int @id @default(autoincrement()) + classId Int @map("class_id") + teacherId Int @map("teacher_id") + role String @default("MAIN") // MAIN主班, ASSIST配班, CARE保育员 + isPrimary Boolean @default(false) // 是否班主任 + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + class Class @relation(fields: [classId], references: [id], onDelete: Cascade) + teacher Teacher @relation(fields: [teacherId], references: [id], onDelete: Cascade) + + @@unique([classId, teacherId]) + @@map("class_teachers") +} + +// ==================== 学生调班历史 ==================== + +model StudentClassHistory { + id Int @id @default(autoincrement()) + studentId Int @map("student_id") + fromClassId Int? @map("from_class_id") + toClassId Int @map("to_class_id") + reason String? + operatedBy Int? @map("operated_by") // 操作人(教师ID),可选 + createdAt DateTime @default(now()) @map("created_at") + + student Student @relation(fields: [studentId], references: [id]) + fromClass Class? @relation("FromClass", fields: [fromClassId], references: [id]) + toClass Class @relation("ToClass", fields: [toClassId], references: [id]) + + @@map("student_class_history") +} + +// ==================== 排课计划 ==================== + +model SchedulePlan { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + + // 关联信息 + classId Int @map("class_id") + courseId Int @map("course_id") + teacherId Int? @map("teacher_id") // 可选,未分配时为空 + + // 时间信息 + scheduledDate DateTime? @map("scheduled_date") // 具体日期 + scheduledTime String? @map("scheduled_time") // 时间段 "09:00-09:30" + weekDay Int? @map("week_day") // 周几 (0-6),用于重复排课 + + // 重复规则 + repeatType String @default("NONE") @map("repeat_type") // NONE, DAILY, WEEKLY + repeatEndDate DateTime? @map("repeat_end_date") + + // 排课来源 + source String @default("SCHOOL") // SCHOOL(学校排课), TEACHER(教师预约) + createdBy Int @map("created_by") // 创建人ID + + // 状态 + status String @default("ACTIVE") // ACTIVE, CANCELLED + + // 备注 + note String? + + // 提醒 + reminderSent Boolean @default(false) @map("reminder_sent") + reminderSentAt DateTime? @map("reminder_sent_at") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id]) + class Class @relation(fields: [classId], references: [id]) + course Course @relation(fields: [courseId], references: [id]) + teacher Teacher? @relation(fields: [teacherId], references: [id]) + lessons Lesson[] + + @@index([tenantId, classId]) + @@index([tenantId, teacherId]) + @@index([tenantId, scheduledDate]) + @@map("schedule_plans") +} + +// ==================== 排课模板 ==================== + +model ScheduleTemplate { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + + // 模板基本信息 + name String + courseId Int @map("course_id") + classId Int? @map("class_id") // 可选,特定班级 + teacherId Int? @map("teacher_id") // 可选,特定教师 + + // 时间配置 + scheduledTime String? @map("scheduled_time") // 时间段 "09:00-09:30" + weekDay Int? @map("week_day") // 周几 (0-6) + duration Int @default(25) // 课程时长(分钟) + + // 模板设置 + isDefault Boolean @default(false) @map("is_default") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id]) + class Class? @relation(fields: [classId], references: [id]) + teacher Teacher? @relation(fields: [teacherId], references: [id]) + + @@index([tenantId]) + @@index([tenantId, courseId]) + @@map("schedule_templates") +} + +// ==================== 操作日志 ==================== + +model OperationLog { + id Int @id @default(autoincrement()) + tenantId Int? @map("tenant_id") + userId Int @map("user_id") + userType String @map("user_type") // SCHOOL, TEACHER, PARENT, ADMIN + action String // CREATE, UPDATE, DELETE, LOGIN, etc. + module String // 教师管理, 学生管理, 排课管理, etc. + description String + targetId Int? @map("target_id") // 操作对象ID + oldValue String? @map("old_value") // JSON格式 + newValue String? @map("new_value") // JSON格式 + ipAddress String? @map("ip_address") + userAgent String? @map("user_agent") + createdAt DateTime @default(now()) @map("created_at") + + tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull) + + @@index([tenantId, userId]) + @@index([tenantId, createdAt]) + @@index([action, module]) + @@map("operation_logs") +} + +// ==================== 主题字典(V2新增) ==================== + +model Theme { + id Int @id @default(autoincrement()) + name String @unique + description String? + sortOrder Int @default(0) @map("sort_order") + status String @default("ACTIVE") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + courses Course[] + @@index([sortOrder]) + @@map("themes") +} + +// ==================== 课程套餐(V2新增) ==================== + +model CoursePackage { + id Int @id @default(autoincrement()) + name String + description String? + price Int @default(0) + discountPrice Int? @map("discount_price") + discountType String? @map("discount_type") + gradeLevels String @default("[]") @map("grade_levels") + status String @default("DRAFT") + submittedAt DateTime? @map("submitted_at") + submittedBy Int? @map("submitted_by") + reviewedAt DateTime? @map("reviewed_at") + reviewedBy Int? @map("reviewed_by") + reviewComment String? @map("review_comment") + courseCount Int @default(0) @map("course_count") + tenantCount Int @default(0) @map("tenant_count") + createdBy Int? @map("created_by") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + publishedAt DateTime? @map("published_at") + courses CoursePackageCourse[] + tenantPackages TenantPackage[] + @@index([status]) + @@map("course_packages") +} + +// ==================== 套餐-课程包关联(V2新增) ==================== + +model CoursePackageCourse { + id Int @id @default(autoincrement()) + packageId Int @map("package_id") + courseId Int @map("course_id") + gradeLevel String @map("grade_level") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + package CoursePackage @relation(fields: [packageId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + @@unique([packageId, courseId]) + @@index([packageId, gradeLevel]) + @@map("course_package_courses") +} + +// ==================== 租户-套餐授权(V2新增) ==================== + +model TenantPackage { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + packageId Int @map("package_id") + startDate String @map("start_date") + endDate String @map("end_date") + status String @default("ACTIVE") + pricePaid Int @default(0) @map("price_paid") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + package CoursePackage @relation(fields: [packageId], references: [id], onDelete: Cascade) + @@unique([tenantId, packageId]) + @@index([tenantId, status]) + @@map("tenant_packages") +} + +// ==================== 课程(V2新增) ==================== + +model CourseLesson { + id Int @id @default(autoincrement()) + courseId Int @map("course_id") + lessonType String @map("lesson_type") + name String + description String? + duration Int @default(25) + videoPath String? @map("video_path") + videoName String? @map("video_name") + pptPath String? @map("ppt_path") + pptName String? @map("ppt_name") + pdfPath String? @map("pdf_path") + pdfName String? @map("pdf_name") + objectives String? + preparation String? + extension String? + reflection String? + assessmentData String? @map("assessment_data") + useTemplate Boolean @default(false) @map("use_template") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + steps LessonStep[] + @@unique([courseId, lessonType]) + @@index([courseId]) + @@map("course_lessons") +} + +// ==================== 教学环节(V2新增) ==================== + +model LessonStep { + id Int @id @default(autoincrement()) + lessonId Int @map("lesson_id") + name String + content String? + duration Int @default(5) + objective String? + resourceIds String? @map("resource_ids") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + lesson CourseLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + stepResources LessonStepResource[] + @@index([lessonId]) + @@map("lesson_steps") +} + +// ==================== 环节资源关联(V2新增) ==================== + +model LessonStepResource { + id Int @id @default(autoincrement()) + stepId Int @map("step_id") + resourceId Int @map("resource_id") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + step LessonStep @relation(fields: [stepId], references: [id], onDelete: Cascade) + resource CourseResource @relation(fields: [resourceId], references: [id], onDelete: Cascade) + @@unique([stepId, resourceId]) + @@map("lesson_step_resources") +} + +// ==================== 校本课程包(V2新增) ==================== + +model SchoolCourse { + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") + sourceCourseId Int @map("source_course_id") + name String + description String? + createdBy Int @map("created_by") + changesSummary String? @map("changes_summary") + usageCount Int @default(0) @map("usage_count") + status String @default("ACTIVE") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + sourceCourse Course @relation(fields: [sourceCourseId], references: [id]) + lessons SchoolCourseLesson[] + reservations SchoolCourseReservation[] + @@index([tenantId]) + @@map("school_courses") +} + +// ==================== 校本课程(V2新增) ==================== + +model SchoolCourseLesson { + id Int @id @default(autoincrement()) + schoolCourseId Int @map("school_course_id") + sourceLessonId Int @map("source_lesson_id") + lessonType String @map("lesson_type") + objectives String? + preparation String? + extension String? + reflection String? + changeNote String? @map("change_note") + stepsData String? @map("steps_data") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + schoolCourse SchoolCourse @relation(fields: [schoolCourseId], references: [id], onDelete: Cascade) + @@index([schoolCourseId]) + @@map("school_course_lessons") +} + +// ==================== 校本课程预约(V2新增) ==================== + +model SchoolCourseReservation { + id Int @id @default(autoincrement()) + schoolCourseId Int @map("school_course_id") + teacherId Int @map("teacher_id") + classId Int @map("class_id") + scheduledDate String @map("scheduled_date") + scheduledTime String? @map("scheduled_time") + status String @default("PENDING") + note String? + createdAt DateTime @default(now()) @map("created_at") + schoolCourse SchoolCourse @relation(fields: [schoolCourseId], references: [id], onDelete: Cascade) + @@index([schoolCourseId, scheduledDate]) + @@map("school_course_reservations") +} \ No newline at end of file diff --git a/reading-platform-backend/prisma/seed.ts b/reading-platform-backend/prisma/seed.ts new file mode 100644 index 0000000..306db17 --- /dev/null +++ b/reading-platform-backend/prisma/seed.ts @@ -0,0 +1,431 @@ +import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcrypt'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('开始种子数据...'); + + // 1. 创建测试租户 + const tenant = await prisma.tenant.upsert({ + where: { id: 1 }, + update: {}, + create: { + name: '阳光幼儿园', + address: '北京市朝阳区xxx街道', + contactPerson: '张园长', + contactPhone: '13800138000', + packageType: 'STANDARD', + teacherQuota: 20, + studentQuota: 200, + storageQuota: BigInt(5368709120), // 5GB + startDate: '2024-01-01', + expireDate: '2025-12-31', + status: 'ACTIVE', + }, + }); + console.log('创建租户:', tenant.name); + + // 2. 创建教师账号 + const passwordHash = await bcrypt.hash('123456', 10); + const teacher = await prisma.teacher.upsert({ + where: { loginAccount: 'teacher1' }, + update: {}, + create: { + tenantId: tenant.id, + name: '李老师', + phone: '13900139000', + email: 'teacher1@example.com', + loginAccount: 'teacher1', + passwordHash: passwordHash, + status: 'ACTIVE', + }, + }); + console.log('创建教师:', teacher.name); + + // 3. 创建班级 + const class1 = await prisma.class.upsert({ + where: { id: 1 }, + update: {}, + create: { + tenantId: tenant.id, + name: '中一班', + grade: 'MIDDLE', + teacherId: teacher.id, + studentCount: 25, + }, + }); + console.log('创建班级:', class1.name); + + const class2 = await prisma.class.upsert({ + where: { id: 2 }, + update: {}, + create: { + tenantId: tenant.id, + name: '大一班', + grade: 'BIG', + teacherId: teacher.id, + studentCount: 30, + }, + }); + console.log('创建班级:', class2.name); + + // 4. 更新教师的班级关联 + await prisma.teacher.update({ + where: { id: teacher.id }, + data: { + classIds: JSON.stringify([class1.id, class2.id]), + }, + }); + + // 5. 创建示例学生 + const students = [ + { name: '小明', gender: 'MALE', classId: class1.id }, + { name: '小红', gender: 'FEMALE', classId: class1.id }, + { name: '小华', gender: 'MALE', classId: class1.id }, + { name: '小丽', gender: 'FEMALE', classId: class2.id }, + { name: '小强', gender: 'MALE', classId: class2.id }, + ]; + + for (const studentData of students) { + await prisma.student.upsert({ + where: { + id: students.indexOf(studentData) + 1, + }, + update: {}, + create: { + tenantId: tenant.id, + classId: studentData.classId, + name: studentData.name, + gender: studentData.gender, + }, + }); + } + console.log('创建学生:', students.length, '名'); + + // 6. 创建示例课程包 + const course = await prisma.course.upsert({ + where: { id: 1 }, + update: {}, + create: { + name: '好饿的毛毛虫', + description: '这是一本经典的绘本,讲述了一只毛毛虫从孵化到变成蝴蝶的故事。通过这个故事,孩子们可以学习到星期的概念、数字的认知,以及毛毛虫变蝴蝶的科学知识。', + pictureBookName: '好饿的毛毛虫', + gradeTags: JSON.stringify(['SMALL', 'MIDDLE']), + domainTags: JSON.stringify(['LANGUAGE', 'SCIENCE', 'MATH']), + duration: 30, + status: 'PUBLISHED', + version: '1.0', + coverImagePath: '/uploads/covers/caterpillar.jpg', + }, + }); + console.log('创建课程:', course.name); + + // 7. 创建课程脚本(6步教学流程) + const scripts = [ + { + stepIndex: 1, + stepName: '阅读导入', + stepType: 'INTRODUCTION', + duration: 5, + objective: '激发幼儿阅读兴趣,建立阅读期待', + teacherScript: '小朋友们,今天我们要认识一位新朋友——一只小小的毛毛虫。你们见过毛毛虫吗?它长什么样子呢?让我们一起来看看这只特别的毛毛虫的故事吧!', + interactionPoints: JSON.stringify([ + '展示毛毛虫图片或玩偶', + '引导幼儿分享见过的毛毛虫', + '预测故事内容', + ]), + }, + { + stepIndex: 2, + stepName: '绘本共读', + stepType: 'READING', + duration: 10, + objective: '理解故事内容,发展语言能力', + teacherScript: '(逐页讲述)从前,有一颗小小的蛋躺在叶子上...月光下,一条又小又饿的毛毛虫从蛋里爬了出来...', + interactionPoints: JSON.stringify([ + '提问预测', + '模仿毛毛虫吃东西的动作', + '一起数食物的数量', + ]), + }, + { + stepIndex: 3, + stepName: '理解讨论', + stepType: 'DISCUSSION', + duration: 5, + objective: '加深对故事的理解,发展思维能力', + teacherScript: '小朋友们,毛毛虫吃了哪些东西呢?为什么最后它肚子痛了?它最后变成了什么?', + interactionPoints: JSON.stringify([ + '回顾毛毛虫吃的食物', + '讨论健康饮食的重要性', + '讨论毛毛虫的成长变化', + ]), + }, + { + stepIndex: 4, + stepName: '互动游戏', + stepType: 'ACTIVITY', + duration: 5, + objective: '通过游戏巩固学习内容', + teacherScript: '现在我们来玩一个游戏,老师说出星期几,小朋友们来模仿毛毛虫吃了什么!', + interactionPoints: JSON.stringify([ + '星期与食物配对游戏', + '毛毛虫动作模仿', + '食物分类活动', + ]), + }, + { + stepIndex: 5, + stepName: '创意表达', + stepType: 'CREATIVE', + duration: 3, + objective: '发展创造力和表达能力', + teacherScript: '如果你是毛毛虫,你想吃什么?画一画你心目中的毛毛虫吧!', + interactionPoints: JSON.stringify([ + '自由绘画', + '分享作品', + '创意表达', + ]), + }, + { + stepIndex: 6, + stepName: '总结延伸', + stepType: 'SUMMARY', + duration: 2, + objective: '总结学习内容,激发延伸探索兴趣', + teacherScript: '今天我们认识了一只可爱的毛毛虫,它从一颗小蛋,变成毛毛虫,最后变成了漂亮的蝴蝶!回家后可以和爸爸妈妈一起找找看,还有哪些动物会变形呢?', + interactionPoints: JSON.stringify([ + '总结毛毛虫的成长过程', + '布置家庭延伸任务', + '预告下次活动', + ]), + }, + ]; + + for (const script of scripts) { + await prisma.courseScript.upsert({ + where: { + courseId_stepIndex: { + courseId: course.id, + stepIndex: script.stepIndex, + }, + }, + update: {}, + create: { + courseId: course.id, + ...script, + sortOrder: script.stepIndex, + }, + }); + } + console.log('创建课程脚本:', scripts.length, '个步骤'); + + // 8. 创建逐页配置(为绘本共读步骤添加) + const pages = [ + { pageNumber: 1, questions: '你们看到了什么?这是什么颜色的?', teacherNotes: '引导观察封面' }, + { pageNumber: 2, questions: '蛋在哪里?是谁的蛋呢?', teacherNotes: '引入故事悬念' }, + { pageNumber: 3, questions: '毛毛虫从蛋里出来了!它说了什么?', teacherNotes: '模仿毛毛虫的声音' }, + { pageNumber: 4, questions: '星期一,毛毛虫吃了什么?吃了几个?', teacherNotes: '学习星期和数字' }, + { pageNumber: 5, questions: '星期二,它又吃了什么?', teacherNotes: '继续学习星期' }, + ]; + + const readingScript = await prisma.courseScript.findFirst({ + where: { courseId: course.id, stepType: 'READING' }, + }); + + if (readingScript) { + for (const page of pages) { + await prisma.courseScriptPage.upsert({ + where: { + scriptId_pageNumber: { + scriptId: readingScript.id, + pageNumber: page.pageNumber, + }, + }, + update: {}, + create: { + scriptId: readingScript.id, + ...page, + }, + }); + } + console.log('创建逐页配置:', pages.length, '页'); + } + + // 9. 创建延伸活动 + const activities = [ + { + name: '毛毛虫手偶制作', + domain: 'ART', + activityType: 'HANDICRAFT', + duration: 20, + onlineMaterials: JSON.stringify(['毛毛虫模板PDF', '制作视频']), + offlineMaterials: '彩纸、剪刀、胶水、眼睛贴纸', + activityGuide: '1. 准备材料\n2. 按照模板剪裁\n3. 粘贴组装\n4. 添加装饰', + objectives: JSON.stringify(['锻炼手部精细动作', '培养创造力', '巩固毛毛虫认知']), + sortOrder: 1, + }, + { + name: '健康饮食分类', + domain: 'SCIENCE', + activityType: 'GAME', + duration: 15, + onlineMaterials: JSON.stringify(['食物卡片PPT']), + offlineMaterials: '食物图片卡片、分类筐', + activityGuide: '1. 展示各种食物图片\n2. 讨论健康与不健康食物\n3. 进行分类游戏', + objectives: JSON.stringify(['认识健康饮食', '学习分类', '培养健康饮食习惯']), + sortOrder: 2, + }, + { + name: '蝴蝶的生命周期', + domain: 'SCIENCE', + activityType: 'EXPLORATION', + duration: 25, + onlineMaterials: JSON.stringify(['蝴蝶生长视频', '生命周期图']), + offlineMaterials: '绘本、放大镜、观察记录本', + activityGuide: '1. 观看蝴蝶生长视频\n2. 讨论四个阶段\n3. 绘制生命周期图', + objectives: JSON.stringify(['了解变态发育', '培养科学探究精神', '学习观察记录']), + sortOrder: 3, + }, + ]; + + for (const activity of activities) { + await prisma.courseActivity.upsert({ + where: { id: activities.indexOf(activity) + 1 }, + update: {}, + create: { + courseId: course.id, + ...activity, + }, + }); + } + console.log('创建延伸活动:', activities.length, '个'); + + // 10. 为租户授权课程 + const tenantCourse = await prisma.tenantCourse.upsert({ + where: { + tenantId_courseId: { + tenantId: tenant.id, + courseId: course.id, + }, + }, + update: {}, + create: { + tenantId: tenant.id, + courseId: course.id, + authorized: true, + authorizedAt: new Date(), + }, + }); + console.log('授权课程给租户:', tenant.id, '->', course.id); + + // 11. 创建第二个示例课程 + const course2 = await prisma.course.upsert({ + where: { id: 2 }, + update: {}, + create: { + name: '猜猜我有多爱你', + description: '这是一本关于爱的温暖绘本,小兔子和大兔子用各种方式表达彼此的爱。通过这个故事,孩子们可以学习到表达爱的方式,感受亲情的温暖。', + pictureBookName: '猜猜我有多爱你', + gradeTags: JSON.stringify(['MIDDLE', 'BIG']), + domainTags: JSON.stringify(['LANGUAGE', 'SOCIAL']), + duration: 25, + status: 'PUBLISHED', + version: '1.0', + coverImagePath: '/uploads/covers/love.jpg', + }, + }); + console.log('创建课程:', course2.name); + + // 为第二个课程创建简化脚本 + const scripts2 = [ + { + stepIndex: 1, + stepName: '导入环节', + stepType: 'INTRODUCTION', + duration: 3, + objective: '引入爱的主题', + teacherScript: '小朋友们,你们爱爸爸妈妈吗?你们是怎么表达爱的呢?', + interactionPoints: JSON.stringify(['分享表达爱的方式']), + }, + { + stepIndex: 2, + stepName: '绘本共读', + stepType: 'READING', + duration: 10, + objective: '理解故事,感受爱的表达', + teacherScript: '小栗色兔子该上床睡觉了,可是他紧紧地抓住大栗色兔子的长耳朵不放...', + interactionPoints: JSON.stringify(['模仿动作', '感受爱的比较']), + }, + { + stepIndex: 3, + stepName: '情感讨论', + stepType: 'DISCUSSION', + duration: 5, + objective: '表达自己的感受', + teacherScript: '小兔子和大兔子谁的爱更多呢?你们觉得呢?', + interactionPoints: JSON.stringify(['讨论爱的深度', '分享感受']), + }, + { + stepIndex: 4, + stepName: '爱的表达', + stepType: 'ACTIVITY', + duration: 5, + objective: '学会表达爱', + teacherScript: '让我们也来学学小兔子,用手臂来量量我们有多爱爸爸妈妈!', + interactionPoints: JSON.stringify(['肢体表达', '语言表达']), + }, + ]; + + for (const script of scripts2) { + await prisma.courseScript.upsert({ + where: { + courseId_stepIndex: { + courseId: course2.id, + stepIndex: script.stepIndex, + }, + }, + update: {}, + create: { + courseId: course2.id, + ...script, + sortOrder: script.stepIndex, + }, + }); + } + + // 授权第二个课程 + await prisma.tenantCourse.upsert({ + where: { + tenantId_courseId: { + tenantId: tenant.id, + courseId: course2.id, + }, + }, + update: {}, + create: { + tenantId: tenant.id, + courseId: course2.id, + authorized: true, + authorizedAt: new Date(), + }, + }); + console.log('授权课程给租户:', tenant.id, '->', course2.id); + + console.log('\n种子数据创建完成!'); + console.log('===================='); + console.log('测试账号信息:'); + console.log('超管: admin / 123456'); + console.log('教师: teacher1 / 123456'); + console.log('===================='); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/reading-platform-backend/scripts/create-test-parent.ts b/reading-platform-backend/scripts/create-test-parent.ts new file mode 100644 index 0000000..0c68a12 --- /dev/null +++ b/reading-platform-backend/scripts/create-test-parent.ts @@ -0,0 +1,99 @@ +// 创建测试家长账号脚本 +// 运行方式: npx ts-node scripts/create-test-parent.ts + +import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcrypt'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('开始创建测试家长账号...'); + + // 获取第一个租户 + const tenant = await prisma.tenant.findFirst(); + + if (!tenant) { + console.log('未找到租户,请先创建学校'); + return; + } + + console.log(`使用租户: ${tenant.name} (ID: ${tenant.id})`); + + // 获取一些学生用于关联 + const students = await prisma.student.findMany({ + where: { tenantId: tenant.id }, + take: 5, + }); + + console.log(`找到 ${students.length} 个学生`); + + // 创建测试家长 + const hashedPassword = await bcrypt.hash('123456', 10); + + const parentData = [ + { + name: '张爸爸', + phone: '13800138001', + email: 'zhang@test.com', + loginAccount: 'parent1', + passwordHash: hashedPassword, + }, + { + name: '李妈妈', + phone: '13800138002', + email: 'li@test.com', + loginAccount: 'parent2', + passwordHash: hashedPassword, + }, + ]; + + for (const data of parentData) { + // 检查是否已存在 + const existing = await prisma.parent.findUnique({ + where: { loginAccount: data.loginAccount }, + }); + + if (existing) { + console.log(`家长 ${data.loginAccount} 已存在,跳过`); + continue; + } + + const parent = await prisma.parent.create({ + data: { + tenantId: tenant.id, + ...data, + status: 'ACTIVE', + }, + }); + + console.log(`创建家长: ${parent.name} (${parent.loginAccount})`); + + // 关联学生 + if (students.length > 0) { + const studentToLink = students[parentData.indexOf(data) % students.length]; + + await prisma.parentStudent.create({ + data: { + parentId: parent.id, + studentId: studentToLink.id, + relationship: data.name.includes('爸爸') ? 'FATHER' : 'MOTHER', + }, + }); + + console.log(` -> 关联学生: ${studentToLink.name}`); + } + } + + console.log('\n测试家长账号创建完成!'); + console.log('登录账号: parent1 / parent2'); + console.log('密码: 123456'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/reading-platform-backend/src/app.module.js b/reading-platform-backend/src/app.module.js new file mode 100644 index 0000000..be1d376 --- /dev/null +++ b/reading-platform-backend/src/app.module.js @@ -0,0 +1,92 @@ +"use strict"; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AppModule = void 0; +var common_1 = require("@nestjs/common"); +var config_1 = require("@nestjs/config"); +var throttler_1 = require("@nestjs/throttler"); +var prisma_module_1 = require("./database/prisma.module"); +var auth_module_1 = require("./modules/auth/auth.module"); +var course_module_1 = require("./modules/course/course.module"); +var tenant_module_1 = require("./modules/tenant/tenant.module"); +var common_module_1 = require("./modules/common/common.module"); +var AppModule = function () { + var _classDecorators = [(0, common_1.Module)({ + imports: [ + // 配置模块 + config_1.ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ".env.".concat(process.env.NODE_ENV || 'development'), + }), + // 限流模块 + throttler_1.ThrottlerModule.forRoot([ + { + ttl: 60000, // 60秒 + limit: 100, // 最多100个请求 + }, + ]), + // Prisma数据库模块 + prisma_module_1.PrismaModule, + // 业务模块 + auth_module_1.AuthModule, + course_module_1.CourseModule, + tenant_module_1.TenantModule, + common_module_1.CommonModule, + ], + })]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var AppModule = _classThis = /** @class */ (function () { + function AppModule_1() { + } + return AppModule_1; + }()); + __setFunctionName(_classThis, "AppModule"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + AppModule = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return AppModule = _classThis; +}(); +exports.AppModule = AppModule; diff --git a/reading-platform-backend/src/app.module.ts b/reading-platform-backend/src/app.module.ts new file mode 100644 index 0000000..31bf409 --- /dev/null +++ b/reading-platform-backend/src/app.module.ts @@ -0,0 +1,68 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { PrismaModule } from './database/prisma.module'; +import { AuthModule } from './modules/auth/auth.module'; +import { CourseModule } from './modules/course/course.module'; +import { TenantModule } from './modules/tenant/tenant.module'; +import { CommonModule } from './modules/common/common.module'; +import { FileUploadModule } from './modules/file-upload/file-upload.module'; +import { TeacherCourseModule } from './modules/teacher-course/teacher-course.module'; +import { LessonModule } from './modules/lesson/lesson.module'; +import { SchoolModule } from './modules/school/school.module'; +import { ResourceModule } from './modules/resource/resource.module'; +import { GrowthModule } from './modules/growth/growth.module'; +import { TaskModule } from './modules/task/task.module'; +import { ParentModule } from './modules/parent/parent.module'; +import { NotificationModule } from './modules/notification/notification.module'; +import { ExportModule } from './modules/export/export.module'; +import { AdminModule } from './modules/admin/admin.module'; +// V2 新增模块 +import { ThemeModule } from './modules/theme/theme.module'; +import { CoursePackageModule } from './modules/course-package/course-package.module'; +import { CourseLessonModule } from './modules/course-lesson/course-lesson.module'; +import { SchoolCourseModule } from './modules/school-course/school-course.module'; + +@Module({ + imports: [ + // 配置模块 + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: `.env.${process.env.NODE_ENV || 'development'}`, + }), + + // 限流模块 + ThrottlerModule.forRoot([ + { + ttl: 60000, // 60秒 + limit: 100, // 最多100个请求 + }, + ]), + + // Prisma数据库模块 + PrismaModule, + + // 业务模块 + AuthModule, + CourseModule, + TenantModule, + CommonModule, + FileUploadModule, + TeacherCourseModule, + LessonModule, + SchoolModule, + ResourceModule, + GrowthModule, + TaskModule, + ParentModule, + NotificationModule, + ExportModule, + AdminModule, + // V2 新增模块 + ThemeModule, + CoursePackageModule, + CourseLessonModule, + SchoolCourseModule, + ], +}) +export class AppModule {} diff --git a/reading-platform-backend/src/common/filters/http-exception.filter.ts b/reading-platform-backend/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..8161da5 --- /dev/null +++ b/reading-platform-backend/src/common/filters/http-exception.filter.ts @@ -0,0 +1,44 @@ +import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common'; +import { Request, Response } from 'express'; +import * as fs from 'fs'; +import * as path from 'path'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(HttpExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : { message: 'Internal server error', statusCode: 500 }; + + const errorLog = { + timestamp: new Date().toISOString(), + path: request.url, + method: request.method, + body: request.body, + status, + exception: exception instanceof Error ? exception.message : String(exception), + stack: exception instanceof Error ? exception.stack : undefined, + }; + + // Log to file + const logPath = path.join(process.cwd(), 'error.log'); + fs.appendFileSync(logPath, JSON.stringify(errorLog, null, 2) + '\n'); + + // Also log to console + this.logger.error('Exception caught', errorLog); + + response.status(status).json(message); + } +} diff --git a/reading-platform-backend/src/database/prisma.module.js b/reading-platform-backend/src/database/prisma.module.js new file mode 100644 index 0000000..bd2aad3 --- /dev/null +++ b/reading-platform-backend/src/database/prisma.module.js @@ -0,0 +1,67 @@ +"use strict"; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PrismaModule = void 0; +var common_1 = require("@nestjs/common"); +var prisma_service_1 = require("./prisma.service"); +var PrismaModule = function () { + var _classDecorators = [(0, common_1.Global)(), (0, common_1.Module)({ + providers: [prisma_service_1.PrismaService], + exports: [prisma_service_1.PrismaService], + })]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var PrismaModule = _classThis = /** @class */ (function () { + function PrismaModule_1() { + } + return PrismaModule_1; + }()); + __setFunctionName(_classThis, "PrismaModule"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + PrismaModule = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return PrismaModule = _classThis; +}(); +exports.PrismaModule = PrismaModule; diff --git a/reading-platform-backend/src/database/prisma.module.ts b/reading-platform-backend/src/database/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/reading-platform-backend/src/database/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/reading-platform-backend/src/database/prisma.service.js b/reading-platform-backend/src/database/prisma.service.js new file mode 100644 index 0000000..5d51ba6 --- /dev/null +++ b/reading-platform-backend/src/database/prisma.service.js @@ -0,0 +1,203 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PrismaService = void 0; +var common_1 = require("@nestjs/common"); +var client_1 = require("@prisma/client"); +var PrismaService = function () { + var _classDecorators = [(0, common_1.Injectable)()]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var _classSuper = client_1.PrismaClient; + var PrismaService = _classThis = /** @class */ (function (_super) { + __extends(PrismaService_1, _super); + function PrismaService_1() { + return _super !== null && _super.apply(this, arguments) || this; + } + PrismaService_1.prototype.onModuleInit = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.$connect()]; + case 1: + _a.sent(); + console.log('✅ Database connected successfully'); + return [2 /*return*/]; + } + }); + }); + }; + PrismaService_1.prototype.onModuleDestroy = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.$disconnect()]; + case 1: + _a.sent(); + console.log('👋 Database disconnected'); + return [2 /*return*/]; + } + }); + }); + }; + // 清理测试数据的辅助方法 + PrismaService_1.prototype.cleanDatabase = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (process.env.NODE_ENV === 'production') { + throw new Error('Cannot clean database in production'); + } + // 按照外键依赖顺序删除 + return [4 /*yield*/, this.studentRecord.deleteMany()]; + case 1: + // 按照外键依赖顺序删除 + _a.sent(); + return [4 /*yield*/, this.lessonFeedback.deleteMany()]; + case 2: + _a.sent(); + return [4 /*yield*/, this.lesson.deleteMany()]; + case 3: + _a.sent(); + return [4 /*yield*/, this.tenantCourse.deleteMany()]; + case 4: + _a.sent(); + return [4 /*yield*/, this.courseScriptPage.deleteMany()]; + case 5: + _a.sent(); + return [4 /*yield*/, this.courseScript.deleteMany()]; + case 6: + _a.sent(); + return [4 /*yield*/, this.courseActivity.deleteMany()]; + case 7: + _a.sent(); + return [4 /*yield*/, this.courseResource.deleteMany()]; + case 8: + _a.sent(); + return [4 /*yield*/, this.course.deleteMany()]; + case 9: + _a.sent(); + return [4 /*yield*/, this.student.deleteMany()]; + case 10: + _a.sent(); + return [4 /*yield*/, this.class.deleteMany()]; + case 11: + _a.sent(); + return [4 /*yield*/, this.teacher.deleteMany()]; + case 12: + _a.sent(); + return [4 /*yield*/, this.tenant.deleteMany()]; + case 13: + _a.sent(); + return [4 /*yield*/, this.tag.deleteMany()]; + case 14: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + return PrismaService_1; + }(_classSuper)); + __setFunctionName(_classThis, "PrismaService"); + (function () { + var _a; + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_a = _classSuper[Symbol.metadata]) !== null && _a !== void 0 ? _a : null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + PrismaService = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return PrismaService = _classThis; +}(); +exports.PrismaService = PrismaService; diff --git a/reading-platform-backend/src/database/prisma.service.ts b/reading-platform-backend/src/database/prisma.service.ts new file mode 100644 index 0000000..4977e3d --- /dev/null +++ b/reading-platform-backend/src/database/prisma.service.ts @@ -0,0 +1,38 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + async onModuleInit() { + await this.$connect(); + console.log('✅ Database connected successfully'); + } + + async onModuleDestroy() { + await this.$disconnect(); + console.log('👋 Database disconnected'); + } + + // 清理测试数据的辅助方法 + async cleanDatabase() { + if (process.env.NODE_ENV === 'production') { + throw new Error('Cannot clean database in production'); + } + + // 按照外键依赖顺序删除 + await this.studentRecord.deleteMany(); + await this.lessonFeedback.deleteMany(); + await this.lesson.deleteMany(); + await this.tenantCourse.deleteMany(); + await this.courseScriptPage.deleteMany(); + await this.courseScript.deleteMany(); + await this.courseActivity.deleteMany(); + await this.courseResource.deleteMany(); + await this.course.deleteMany(); + await this.student.deleteMany(); + await this.class.deleteMany(); + await this.teacher.deleteMany(); + await this.tenant.deleteMany(); + await this.tag.deleteMany(); + } +} diff --git a/reading-platform-backend/src/main.js b/reading-platform-backend/src/main.js new file mode 100644 index 0000000..31f581b --- /dev/null +++ b/reading-platform-backend/src/main.js @@ -0,0 +1,84 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var core_1 = require("@nestjs/core"); +var common_1 = require("@nestjs/common"); +var config_1 = require("@nestjs/config"); +var app_module_1 = require("./app.module"); +function bootstrap() { + return __awaiter(this, void 0, void 0, function () { + var app, configService, port; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, core_1.NestFactory.create(app_module_1.AppModule, { + logger: ['error', 'warn', 'log', 'debug', 'verbose'], + })]; + case 1: + app = _a.sent(); + configService = app.get(config_1.ConfigService); + // 全局验证管道 + app.useGlobalPipes(new common_1.ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { + enableImplicitConversion: true, + }, + })); + // 启用压缩 + // app.use(compression()); + // CORS + app.enableCors({ + origin: configService.get('FRONTEND_URL') || 'http://localhost:5173', + credentials: true, + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + allowedHeaders: 'Content-Type, Accept, Authorization', + }); + // API前缀 + app.setGlobalPrefix('api/v1'); + port = configService.get('PORT') || 3000; + return [4 /*yield*/, app.listen(port)]; + case 2: + _a.sent(); + console.log("\n \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n \u2551 \u2551\n \u2551 \uD83D\uDE80 \u5E7C\u513F\u9605\u8BFB\u6559\u5B66\u670D\u52A1\u5E73\u53F0\u540E\u7AEF\u542F\u52A8\u6210\u529F \u2551\n \u2551 \u2551\n \u2551 \uD83D\uDCCD Local: http://localhost:".concat(port, " \u2551\n \u2551 \uD83D\uDCCD API: http://localhost:").concat(port, "/api/v1 \u2551\n \u2551 \uD83D\uDCCD Prisma: npx prisma studio \u2551\n \u2551 \u2551\n \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n ")); + return [2 /*return*/]; + } + }); + }); +} +bootstrap(); diff --git a/reading-platform-backend/src/main.ts b/reading-platform-backend/src/main.ts new file mode 100644 index 0000000..3ca479a --- /dev/null +++ b/reading-platform-backend/src/main.ts @@ -0,0 +1,74 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { join } from 'path'; +import { AppModule } from './app.module'; +import * as compression from 'compression'; +import { HttpExceptionFilter } from './common/filters/http-exception.filter'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log', 'debug', 'verbose'], + }); + + // 增加请求体大小限制(支持上传大文件 base64) + // 1GB 文件编码后约 1.33GB,加上其他字段,设置为 1500mb + app.useBodyParser('json', { limit: '1500mb' }); + app.useBodyParser('urlencoded', { limit: '1500mb', extended: true }); + + const configService = app.get(ConfigService); + + // 全局验证管道 + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { + enableImplicitConversion: true, + }, + }), + ); + + // 全局异常过滤器 + app.useGlobalFilters(new HttpExceptionFilter()); + + // 启用压缩 + // app.use(compression()); + + // CORS + app.enableCors({ + origin: configService.get('FRONTEND_URL') || 'http://localhost:5173', + credentials: true, + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + allowedHeaders: 'Content-Type, Accept, Authorization', + }); + + // 配置静态文件服务(用于访问上传的文件) + // 使用绝对路径确保在编译后也能正确找到 uploads 目录 + const uploadsPath = join(__dirname, '..', '..', 'uploads'); + app.useStaticAssets(uploadsPath, { + prefix: '/uploads/', + }); + + // API前缀 + app.setGlobalPrefix('api/v1'); + + const port = configService.get('PORT') || 3000; + await app.listen(port); + + console.log(` + ╔═════════════════════════════════════════════════════╗ + ║ ║ + ║ 🚀 幼儿阅读教学服务平台后端启动成功 ║ + ║ ║ + ║ 📍 Local: http://localhost:${port} ║ + ║ 📍 API: http://localhost:${port}/api/v1 ║ + ║ 📍 Prisma: npx prisma studio ║ + ║ ║ + ╚═════════════════════════════════════════════════════╝ + `); +} + +bootstrap(); diff --git a/reading-platform-backend/src/modules/admin/admin-settings.controller.ts b/reading-platform-backend/src/modules/admin/admin-settings.controller.ts new file mode 100644 index 0000000..295edce --- /dev/null +++ b/reading-platform-backend/src/modules/admin/admin-settings.controller.ts @@ -0,0 +1,67 @@ +import { Controller, Get, Put, Body, UseGuards } from '@nestjs/common'; +import { AdminSettingsService } from './admin-settings.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('admin/settings') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class AdminSettingsController { + constructor(private readonly settingsService: AdminSettingsService) {} + + @Get() + async getAllSettings() { + return this.settingsService.getSettings(); + } + + @Put() + async updateSettings(@Body() data: Record) { + return this.settingsService.updateSettings(data); + } + + @Get('basic') + async getBasicSettings() { + return this.settingsService.getBasicSettings(); + } + + @Put('basic') + async updateBasicSettings(@Body() data: Record) { + return this.settingsService.updateSettings(data); + } + + @Get('security') + async getSecuritySettings() { + return this.settingsService.getSecuritySettings(); + } + + @Put('security') + async updateSecuritySettings(@Body() data: Record) { + return this.settingsService.updateSettings(data); + } + + @Get('notification') + async getNotificationSettings() { + return this.settingsService.getNotificationSettings(); + } + + @Put('notification') + async updateNotificationSettings(@Body() data: Record) { + return this.settingsService.updateSettings(data); + } + + @Get('storage') + async getStorageSettings() { + return this.settingsService.getStorageSettings(); + } + + @Put('storage') + async updateStorageSettings(@Body() data: Record) { + return this.settingsService.updateSettings(data); + } + + @Get('tenant-defaults') + async getTenantDefaults() { + return this.settingsService.getTenantDefaults(); + } +} diff --git a/reading-platform-backend/src/modules/admin/admin-settings.service.ts b/reading-platform-backend/src/modules/admin/admin-settings.service.ts new file mode 100644 index 0000000..e55eb83 --- /dev/null +++ b/reading-platform-backend/src/modules/admin/admin-settings.service.ts @@ -0,0 +1,107 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +// Admin settings stored in memory (could be moved to database later) +@Injectable() +export class AdminSettingsService { + private settings: Record = { + // Basic settings + systemName: '幼儿阅读教学服务平台', + systemDesc: '', + contactPhone: '', + contactEmail: '', + systemLogo: '', + + // Security settings + passwordStrength: 'medium', + maxLoginAttempts: 5, + tokenExpire: '7d', + forceHttps: false, + + // Notification settings + emailEnabled: true, + smtpHost: '', + smtpPort: 465, + smtpUser: '', + smtpPassword: '', + fromEmail: '', + smsEnabled: false, + + // Storage settings + storageType: 'local', + maxFileSize: 100, + allowedTypes: '.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.ppt,.pptx', + + // Tenant defaults + defaultTeacherQuota: 20, + defaultStudentQuota: 200, + enableAutoExpire: true, + notifyBeforeDays: 30, + }; + + constructor(private prisma: PrismaService) {} + + async getSettings() { + return { ...this.settings }; + } + + async getSetting(key: string) { + return this.settings[key]; + } + + async updateSettings(data: Record) { + // Update only provided keys + for (const key of Object.keys(data)) { + if (key in this.settings) { + this.settings[key] = data[key]; + } + } + return { ...this.settings }; + } + + async getBasicSettings() { + return { + systemName: this.settings.systemName, + systemDesc: this.settings.systemDesc, + contactPhone: this.settings.contactPhone, + contactEmail: this.settings.contactEmail, + systemLogo: this.settings.systemLogo, + }; + } + + async getSecuritySettings() { + return { + passwordStrength: this.settings.passwordStrength, + maxLoginAttempts: this.settings.maxLoginAttempts, + tokenExpire: this.settings.tokenExpire, + forceHttps: this.settings.forceHttps, + }; + } + + async getNotificationSettings() { + return { + emailEnabled: this.settings.emailEnabled, + smtpHost: this.settings.smtpHost, + smtpPort: this.settings.smtpPort, + fromEmail: this.settings.fromEmail, + smsEnabled: this.settings.smsEnabled, + }; + } + + async getStorageSettings() { + return { + type: this.settings.storageType, + maxFileSize: this.settings.maxFileSize, + allowedTypes: this.settings.allowedTypes, + }; + } + + async getTenantDefaults() { + return { + defaultTeacherQuota: this.settings.defaultTeacherQuota, + defaultStudentQuota: this.settings.defaultStudentQuota, + enableAutoExpire: this.settings.enableAutoExpire, + notifyBeforeDays: this.settings.notifyBeforeDays, + }; + } +} diff --git a/reading-platform-backend/src/modules/admin/admin-stats.controller.ts b/reading-platform-backend/src/modules/admin/admin-stats.controller.ts new file mode 100644 index 0000000..a98bf57 --- /dev/null +++ b/reading-platform-backend/src/modules/admin/admin-stats.controller.ts @@ -0,0 +1,40 @@ +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { AdminStatsService } from './admin-stats.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('admin/stats') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class AdminStatsController { + constructor(private readonly statsService: AdminStatsService) {} + + @Get() + async getStats() { + return this.statsService.getStats(); + } + + @Get('trend') + async getTrendData() { + return this.statsService.getTrendData(); + } + + @Get('tenants/active') + async getActiveTenants(@Query('limit') limit?: string) { + const limitNum = limit ? parseInt(limit, 10) : 5; + return this.statsService.getActiveTenants(limitNum); + } + + @Get('courses/popular') + async getPopularCourses(@Query('limit') limit?: string) { + const limitNum = limit ? parseInt(limit, 10) : 5; + return this.statsService.getPopularCourses(limitNum); + } + + @Get('activities') + async getRecentActivities(@Query('limit') limit?: string) { + const limitNum = limit ? parseInt(limit, 10) : 10; + return this.statsService.getRecentActivities(limitNum); + } +} diff --git a/reading-platform-backend/src/modules/admin/admin-stats.service.ts b/reading-platform-backend/src/modules/admin/admin-stats.service.ts new file mode 100644 index 0000000..d6dabef --- /dev/null +++ b/reading-platform-backend/src/modules/admin/admin-stats.service.ts @@ -0,0 +1,236 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class AdminStatsService { + constructor(private prisma: PrismaService) {} + + async getStats() { + const [ + tenantCount, + activeTenantCount, + courseCount, + publishedCourseCount, + studentCount, + teacherCount, + lessonCount, + monthlyLessons, + ] = await Promise.all([ + this.prisma.tenant.count(), + this.prisma.tenant.count({ where: { status: 'ACTIVE' } }), + this.prisma.course.count(), + this.prisma.course.count({ where: { status: 'PUBLISHED' } }), + this.prisma.student.count(), + this.prisma.teacher.count(), + this.prisma.lesson.count(), + this.getThisMonthLessonCount(), + ]); + + return { + tenantCount, + activeTenantCount, + courseCount, + publishedCourseCount, + studentCount, + teacherCount, + lessonCount, + monthlyLessons, + }; + } + + private async getThisMonthLessonCount() { + const now = new Date(); + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + return this.prisma.lesson.count({ + where: { + createdAt: { + gte: firstDayOfMonth, + }, + }, + }); + } + + async getTrendData() { + // Get last 6 months data + const months: Array<{ month: string; tenantCount: number; lessonCount: number; studentCount: number }> = []; + + for (let i = 5; i >= 0; i--) { + const date = new Date(); + date.setMonth(date.getMonth() - i); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const monthStr = `${year}-${String(month).padStart(2, '0')}`; + + const firstDay = new Date(year, month - 1, 1); + const lastDay = new Date(year, month, 0, 23, 59, 59); + + const [tenantCount, lessonCount, studentCount] = await Promise.all([ + this.prisma.tenant.count({ + where: { + createdAt: { + lte: lastDay, + }, + }, + }), + this.prisma.lesson.count({ + where: { + createdAt: { + gte: firstDay, + lte: lastDay, + }, + }, + }), + this.prisma.student.count({ + where: { + createdAt: { + lte: lastDay, + }, + }, + }), + ]); + + months.push({ + month: monthStr, + tenantCount, + lessonCount, + studentCount, + }); + } + + return months; + } + + async getActiveTenants(limit: number = 5) { + // Get tenants with most lessons in the last 30 days + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const tenants = await this.prisma.tenant.findMany({ + select: { + id: true, + name: true, + teacherCount: true, + studentCount: true, + _count: { + select: { + lessons: { + where: { + createdAt: { + gte: thirtyDaysAgo, + }, + }, + }, + }, + }, + }, + orderBy: { + lessons: { + _count: 'desc', + }, + }, + take: limit, + }); + + return tenants.map((t) => ({ + id: t.id, + name: t.name, + lessonCount: t._count.lessons, + teacherCount: t.teacherCount, + studentCount: t.studentCount, + })); + } + + async getPopularCourses(limit: number = 5) { + // Get courses with most usage + const courses = await this.prisma.course.findMany({ + select: { + id: true, + name: true, + usageCount: true, + teacherCount: true, + }, + where: { + status: 'PUBLISHED', + }, + orderBy: { + usageCount: 'desc', + }, + take: limit, + }); + + return courses; + } + + async getRecentActivities(limit: number = 10) { + const activities: Array<{ + id: number; + type: string; + title: string; + description?: string; + time: Date; + }> = []; + + // Get recent lessons + const recentLessons = await this.prisma.lesson.findMany({ + select: { + id: true, + createdAt: true, + tenant: { + select: { + name: true, + }, + }, + course: { + select: { + name: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + take: limit, + }); + + for (const lesson of recentLessons) { + activities.push({ + id: lesson.id, + type: 'lesson', + title: `${lesson.tenant.name} 完成了课程《${lesson.course.name}》`, + time: lesson.createdAt, + }); + } + + // Get recent tenants + const recentTenants = await this.prisma.tenant.findMany({ + select: { + id: true, + name: true, + createdAt: true, + }, + orderBy: { + createdAt: 'desc', + }, + take: limit, + }); + + for (const tenant of recentTenants) { + activities.push({ + id: tenant.id + 10000, + type: 'tenant', + title: `新租户注册: ${tenant.name}`, + time: tenant.createdAt, + }); + } + + // Sort by time and limit + return activities + .sort((a, b) => b.time.getTime() - a.time.getTime()) + .slice(0, limit) + .map((a) => ({ + ...a, + time: a.time.toISOString(), + })); + } +} diff --git a/reading-platform-backend/src/modules/admin/admin.module.ts b/reading-platform-backend/src/modules/admin/admin.module.ts new file mode 100644 index 0000000..2ef011a --- /dev/null +++ b/reading-platform-backend/src/modules/admin/admin.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { AdminSettingsController } from './admin-settings.controller'; +import { AdminSettingsService } from './admin-settings.service'; +import { AdminStatsController } from './admin-stats.controller'; +import { AdminStatsService } from './admin-stats.service'; +import { PrismaModule } from '../../database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [AdminSettingsController, AdminStatsController], + providers: [AdminSettingsService, AdminStatsService], + exports: [AdminSettingsService, AdminStatsService], +}) +export class AdminModule {} diff --git a/reading-platform-backend/src/modules/auth/auth.controller.js b/reading-platform-backend/src/modules/auth/auth.controller.js new file mode 100644 index 0000000..008181e --- /dev/null +++ b/reading-platform-backend/src/modules/auth/auth.controller.js @@ -0,0 +1,133 @@ +"use strict"; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AuthController = void 0; +var common_1 = require("@nestjs/common"); +var jwt_auth_guard_1 = require("../common/guards/jwt-auth.guard"); +var AuthController = function () { + var _classDecorators = [(0, common_1.Controller)('auth')]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var _instanceExtraInitializers = []; + var _login_decorators; + var _logout_decorators; + var _getProfile_decorators; + var AuthController = _classThis = /** @class */ (function () { + function AuthController_1(authService) { + this.authService = (__runInitializers(this, _instanceExtraInitializers), authService); + } + AuthController_1.prototype.login = function (loginDto) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.authService.login(loginDto)]; + }); + }); + }; + AuthController_1.prototype.logout = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + // JWT是无状态的,logout主要在前端删除token + return [2 /*return*/, { message: 'Logged out successfully' }]; + }); + }); + }; + AuthController_1.prototype.getProfile = function (req) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.authService.getProfile(req.user.userId, req.user.role)]; + }); + }); + }; + return AuthController_1; + }()); + __setFunctionName(_classThis, "AuthController"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + _login_decorators = [(0, common_1.Post)('login')]; + _logout_decorators = [(0, common_1.Post)('logout'), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard)]; + _getProfile_decorators = [(0, common_1.Get)('profile'), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard)]; + __esDecorate(_classThis, null, _login_decorators, { kind: "method", name: "login", static: false, private: false, access: { has: function (obj) { return "login" in obj; }, get: function (obj) { return obj.login; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(_classThis, null, _logout_decorators, { kind: "method", name: "logout", static: false, private: false, access: { has: function (obj) { return "logout" in obj; }, get: function (obj) { return obj.logout; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(_classThis, null, _getProfile_decorators, { kind: "method", name: "getProfile", static: false, private: false, access: { has: function (obj) { return "getProfile" in obj; }, get: function (obj) { return obj.getProfile; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + AuthController = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return AuthController = _classThis; +}(); +exports.AuthController = AuthController; diff --git a/reading-platform-backend/src/modules/auth/auth.controller.ts b/reading-platform-backend/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..a973555 --- /dev/null +++ b/reading-platform-backend/src/modules/auth/auth.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Post, Body, Get, UseGuards, Request } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { LoginDto } from './dto/login.dto'; + +@Controller('auth') +export class AuthController { + constructor(private authService: AuthService) {} + + @Post('login') + async login(@Body() loginDto: LoginDto) { + return this.authService.login(loginDto); + } + + @Post('logout') + @UseGuards(JwtAuthGuard) + async logout() { + // JWT是无状态的,logout主要在前端删除token + return { message: 'Logged out successfully' }; + } + + @Get('profile') + @UseGuards(JwtAuthGuard) + async getProfile(@Request() req) { + return this.authService.getProfile(req.user.userId, req.user.role); + } +} diff --git a/reading-platform-backend/src/modules/auth/auth.module.js b/reading-platform-backend/src/modules/auth/auth.module.js new file mode 100644 index 0000000..f5f574a --- /dev/null +++ b/reading-platform-backend/src/modules/auth/auth.module.js @@ -0,0 +1,128 @@ +"use strict"; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AuthModule = void 0; +var common_1 = require("@nestjs/common"); +var jwt_1 = require("@nestjs/jwt"); +var passport_1 = require("@nestjs/passport"); +var config_1 = require("@nestjs/config"); +var auth_service_1 = require("./auth.service"); +var auth_controller_1 = require("./auth.controller"); +var jwt_strategy_1 = require("./strategies/jwt.strategy"); +var prisma_module_1 = require("../../database/prisma.module"); +var AuthModule = function () { + var _classDecorators = [(0, common_1.Module)({ + imports: [ + passport_1.PassportModule, + jwt_1.JwtModule.registerAsync({ + imports: [config_1.ConfigModule], + useFactory: function (configService) { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, ({ + secret: configService.get('JWT_SECRET') || 'your-secret-key', + signOptions: { + expiresIn: configService.get('JWT_EXPIRES_IN') || '7d', + }, + })]; + }); + }); }, + inject: [config_1.ConfigService], + }), + prisma_module_1.PrismaModule, + ], + controllers: [auth_controller_1.AuthController], + providers: [auth_service_1.AuthService, jwt_strategy_1.JwtStrategy], + exports: [auth_service_1.AuthService], + })]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var AuthModule = _classThis = /** @class */ (function () { + function AuthModule_1() { + } + return AuthModule_1; + }()); + __setFunctionName(_classThis, "AuthModule"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + AuthModule = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return AuthModule = _classThis; +}(); +exports.AuthModule = AuthModule; diff --git a/reading-platform-backend/src/modules/auth/auth.module.ts b/reading-platform-backend/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..652a013 --- /dev/null +++ b/reading-platform-backend/src/modules/auth/auth.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { PrismaModule } from '../../database/prisma.module'; + +@Module({ + imports: [ + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET') || 'your-secret-key', + signOptions: { + expiresIn: configService.get('JWT_EXPIRES_IN') || '7d', + }, + }), + inject: [ConfigService], + }), + PrismaModule, + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/reading-platform-backend/src/modules/auth/auth.service.js b/reading-platform-backend/src/modules/auth/auth.service.js new file mode 100644 index 0000000..d2107a1 --- /dev/null +++ b/reading-platform-backend/src/modules/auth/auth.service.js @@ -0,0 +1,288 @@ +"use strict"; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AuthService = void 0; +var common_1 = require("@nestjs/common"); +var bcrypt = require("bcrypt"); +var AuthService = function () { + var _classDecorators = [(0, common_1.Injectable)()]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var AuthService = _classThis = /** @class */ (function () { + function AuthService_1(prisma, jwtService) { + this.prisma = prisma; + this.jwtService = jwtService; + } + AuthService_1.prototype.validateUser = function (account, password) { + return __awaiter(this, void 0, void 0, function () { + var user, isPasswordValid; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.prisma.teacher.findUnique({ + where: { loginAccount: account }, + })]; + case 1: + user = _a.sent(); + if (!user) { + throw new common_1.UnauthorizedException('账号或密码错误'); + } + return [4 /*yield*/, bcrypt.compare(password, user.passwordHash)]; + case 2: + isPasswordValid = _a.sent(); + if (!isPasswordValid) { + throw new common_1.UnauthorizedException('账号或密码错误'); + } + if (user.status !== 'ACTIVE') { + throw new common_1.UnauthorizedException('账号已被停用'); + } + return [2 /*return*/, user]; + } + }); + }); + }; + AuthService_1.prototype.login = function (dto) { + return __awaiter(this, void 0, void 0, function () { + var user, tenant, teacher, isPasswordValid, payload, token; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!(dto.role === 'admin')) return [3 /*break*/, 1]; + // 超管账号(硬编码或从配置读取) + if (dto.account === 'admin' && dto.password === '123456') { + user = { + id: 1, + name: '超级管理员', + role: 'admin', + }; + } + else { + throw new common_1.UnauthorizedException('账号或密码错误'); + } + return [3 /*break*/, 7]; + case 1: + if (!(dto.role === 'school')) return [3 /*break*/, 3]; + return [4 /*yield*/, this.prisma.tenant.findFirst({ + where: { name: dto.account }, + })]; + case 2: + tenant = _a.sent(); + if (!tenant) { + throw new common_1.UnauthorizedException('账号或密码错误'); + } + // 验证密码(这里简化处理) + if (dto.password !== '123456') { + throw new common_1.UnauthorizedException('账号或密码错误'); + } + user = { + id: tenant.id, + name: tenant.name, + role: 'school', + tenantId: tenant.id, + tenantName: tenant.name, + }; + return [3 /*break*/, 7]; + case 3: + if (!(dto.role === 'teacher')) return [3 /*break*/, 6]; + return [4 /*yield*/, this.prisma.teacher.findUnique({ + where: { loginAccount: dto.account }, + })]; + case 4: + teacher = _a.sent(); + if (!teacher) { + throw new common_1.UnauthorizedException('账号或密码错误'); + } + return [4 /*yield*/, bcrypt.compare(dto.password, teacher.passwordHash)]; + case 5: + isPasswordValid = _a.sent(); + if (!isPasswordValid) { + throw new common_1.UnauthorizedException('账号或密码错误'); + } + if (teacher.status !== 'ACTIVE') { + throw new common_1.UnauthorizedException('账号已被停用'); + } + user = { + id: teacher.id, + name: teacher.name, + role: 'teacher', + tenantId: teacher.tenantId, + }; + return [3 /*break*/, 7]; + case 6: throw new common_1.UnauthorizedException('无效的角色'); + case 7: + payload = { + sub: user.id, + role: user.role, + tenantId: user.tenantId, + }; + token = this.jwtService.sign(payload); + if (!(dto.role === 'teacher')) return [3 /*break*/, 9]; + return [4 /*yield*/, this.prisma.teacher.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + })]; + case 8: + _a.sent(); + _a.label = 9; + case 9: return [2 /*return*/, { + token: token, + user: { + id: user.id, + name: user.name, + role: user.role, + tenantId: user.tenantId, + tenantName: user.tenantName, + }, + }]; + } + }); + }); + }; + AuthService_1.prototype.getProfile = function (userId, role) { + return __awaiter(this, void 0, void 0, function () { + var tenant, teacher; + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!(role === 'admin')) return [3 /*break*/, 1]; + return [2 /*return*/, { + id: 1, + name: '超级管理员', + role: 'admin', + }]; + case 1: + if (!(role === 'school')) return [3 /*break*/, 3]; + return [4 /*yield*/, this.prisma.tenant.findUnique({ + where: { id: userId }, + })]; + case 2: + tenant = _b.sent(); + if (!tenant) { + throw new common_1.UnauthorizedException('用户不存在'); + } + return [2 /*return*/, { + id: tenant.id, + name: tenant.name, + role: 'school', + tenantId: tenant.id, + tenantName: tenant.name, + }]; + case 3: + if (!(role === 'teacher')) return [3 /*break*/, 5]; + return [4 /*yield*/, this.prisma.teacher.findUnique({ + where: { id: userId }, + include: { + tenant: { + select: { + id: true, + name: true, + }, + }, + }, + })]; + case 4: + teacher = _b.sent(); + if (!teacher) { + throw new common_1.UnauthorizedException('用户不存在'); + } + return [2 /*return*/, { + id: teacher.id, + name: teacher.name, + role: 'teacher', + tenantId: teacher.tenantId, + tenantName: (_a = teacher.tenant) === null || _a === void 0 ? void 0 : _a.name, + email: teacher.email, + phone: teacher.phone, + }]; + case 5: throw new common_1.UnauthorizedException('无效的角色'); + } + }); + }); + }; + return AuthService_1; + }()); + __setFunctionName(_classThis, "AuthService"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + AuthService = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return AuthService = _classThis; +}(); +exports.AuthService = AuthService; diff --git a/reading-platform-backend/src/modules/auth/auth.service.ts b/reading-platform-backend/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..1853d56 --- /dev/null +++ b/reading-platform-backend/src/modules/auth/auth.service.ts @@ -0,0 +1,298 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { PrismaService } from '../../database/prisma.service'; +import * as bcrypt from 'bcrypt'; + +export interface LoginDto { + account: string; + password: string; + role: string; +} + +export interface JwtPayload { + sub: number; + role: string; + tenantId?: number; +} + +@Injectable() +export class AuthService { + constructor( + private prisma: PrismaService, + private jwtService: JwtService, + ) {} + + async validateUser(account: string, password: string) { + // 根据账号查找用户(这里简化处理,实际应根据role查找不同表) + const user = await this.prisma.teacher.findUnique({ + where: { loginAccount: account }, + }); + + if (!user) { + throw new UnauthorizedException('账号或密码错误'); + } + + const isPasswordValid = await bcrypt.compare(password, user.passwordHash); + + if (!isPasswordValid) { + throw new UnauthorizedException('账号或密码错误'); + } + + if (user.status !== 'ACTIVE') { + throw new UnauthorizedException('账号已被停用'); + } + + return user; + } + + async login(dto: LoginDto) { + // 根据角色查找不同的用户表 + let user: any; + + if (dto.role === 'admin') { + // 超管账号(硬编码或从配置读取) + if (dto.account === 'admin' && dto.password === 'admin123') { + user = { + id: 1, + name: '超级管理员', + role: 'admin', + }; + } else { + throw new UnauthorizedException('账号或密码错误'); + } + } else if (dto.role === 'school') { + // 学校管理员(从租户表查找) + const tenant = await this.prisma.tenant.findUnique({ + where: { loginAccount: dto.account }, + }); + + if (!tenant) { + throw new UnauthorizedException('账号或密码错误'); + } + + // 验证密码 + if (!tenant.passwordHash) { + throw new UnauthorizedException('账号未设置密码'); + } + + const isPasswordValid = await bcrypt.compare(dto.password, tenant.passwordHash); + + if (!isPasswordValid) { + throw new UnauthorizedException('账号或密码错误'); + } + + if (tenant.status !== 'ACTIVE') { + throw new UnauthorizedException('账号已被停用'); + } + + user = { + id: tenant.id, + name: tenant.name, + role: 'school', + tenantId: tenant.id, + tenantName: tenant.name, + }; + } else if (dto.role === 'teacher') { + // 教师 + const teacher = await this.prisma.teacher.findUnique({ + where: { loginAccount: dto.account }, + include: { + tenant: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + if (!teacher) { + throw new UnauthorizedException('账号或密码错误'); + } + + const isPasswordValid = await bcrypt.compare(dto.password, teacher.passwordHash); + + if (!isPasswordValid) { + throw new UnauthorizedException('账号或密码错误'); + } + + if (teacher.status !== 'ACTIVE') { + throw new UnauthorizedException('账号已被停用'); + } + + user = { + id: teacher.id, + name: teacher.name, + role: 'teacher', + tenantId: teacher.tenantId, + tenantName: teacher.tenant?.name, + }; + } else if (dto.role === 'parent') { + // 家长 + const parent = await this.prisma.parent.findUnique({ + where: { loginAccount: dto.account }, + include: { + tenant: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + if (!parent) { + throw new UnauthorizedException('账号或密码错误'); + } + + const isPasswordValid = await bcrypt.compare(dto.password, parent.passwordHash); + + if (!isPasswordValid) { + throw new UnauthorizedException('账号或密码错误'); + } + + if (parent.status !== 'ACTIVE') { + throw new UnauthorizedException('账号已被停用'); + } + + user = { + id: parent.id, + name: parent.name, + role: 'parent', + tenantId: parent.tenantId, + tenantName: parent.tenant?.name, + }; + + // 更新最后登录时间 + await this.prisma.parent.update({ + where: { id: parent.id }, + data: { lastLoginAt: new Date() }, + }); + } else { + throw new UnauthorizedException('无效的角色'); + } + + // 生成JWT token + const payload: JwtPayload = { + sub: user.id, + role: user.role, + tenantId: user.tenantId, + }; + + const token = this.jwtService.sign(payload); + + // 更新最后登录时间 + if (dto.role === 'teacher') { + await this.prisma.teacher.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + }); + } + + return { + token, + user: { + id: user.id, + name: user.name, + role: user.role, + tenantId: user.tenantId, + tenantName: user.tenantName, + }, + }; + } + + async getProfile(userId: number, role: string) { + if (role === 'admin') { + return { + id: 1, + name: '超级管理员', + role: 'admin', + }; + } else if (role === 'school') { + const tenant = await this.prisma.tenant.findUnique({ + where: { id: userId }, + }); + + if (!tenant) { + throw new UnauthorizedException('用户不存在'); + } + + return { + id: tenant.id, + name: tenant.name, + role: 'school', + tenantId: tenant.id, + tenantName: tenant.name, + }; + } else if (role === 'teacher') { + const teacher = await this.prisma.teacher.findUnique({ + where: { id: userId }, + include: { + tenant: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + if (!teacher) { + throw new UnauthorizedException('用户不存在'); + } + + return { + id: teacher.id, + name: teacher.name, + role: 'teacher', + tenantId: teacher.tenantId, + tenantName: teacher.tenant?.name, + email: teacher.email, + phone: teacher.phone, + }; + } else if (role === 'parent') { + const parent = await this.prisma.parent.findUnique({ + where: { id: userId }, + include: { + tenant: { + select: { + id: true, + name: true, + }, + }, + children: { + include: { + student: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }); + + if (!parent) { + throw new UnauthorizedException('用户不存在'); + } + + return { + id: parent.id, + name: parent.name, + role: 'parent', + tenantId: parent.tenantId, + tenantName: parent.tenant?.name, + email: parent.email, + phone: parent.phone, + children: parent.children.map((c) => ({ + id: c.student.id, + name: c.student.name, + relationship: c.relationship, + })), + }; + } + + throw new UnauthorizedException('无效的角色'); + } +} diff --git a/reading-platform-backend/src/modules/auth/dto/login.dto.js b/reading-platform-backend/src/modules/auth/dto/login.dto.js new file mode 100644 index 0000000..977df81 --- /dev/null +++ b/reading-platform-backend/src/modules/auth/dto/login.dto.js @@ -0,0 +1,71 @@ +"use strict"; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LoginDto = void 0; +var class_validator_1 = require("class-validator"); +var LoginDto = function () { + var _a; + var _account_decorators; + var _account_initializers = []; + var _account_extraInitializers = []; + var _password_decorators; + var _password_initializers = []; + var _password_extraInitializers = []; + var _role_decorators; + var _role_initializers = []; + var _role_extraInitializers = []; + return _a = /** @class */ (function () { + function LoginDto() { + this.account = __runInitializers(this, _account_initializers, void 0); + this.password = (__runInitializers(this, _account_extraInitializers), __runInitializers(this, _password_initializers, void 0)); + this.role = (__runInitializers(this, _password_extraInitializers), __runInitializers(this, _role_initializers, void 0)); + __runInitializers(this, _role_extraInitializers); + } + return LoginDto; + }()), + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + _account_decorators = [(0, class_validator_1.IsString)(), (0, class_validator_1.IsNotEmpty)()]; + _password_decorators = [(0, class_validator_1.IsString)(), (0, class_validator_1.IsNotEmpty)()]; + _role_decorators = [(0, class_validator_1.IsString)(), (0, class_validator_1.IsIn)(['admin', 'school', 'teacher']), (0, class_validator_1.IsNotEmpty)()]; + __esDecorate(null, null, _account_decorators, { kind: "field", name: "account", static: false, private: false, access: { has: function (obj) { return "account" in obj; }, get: function (obj) { return obj.account; }, set: function (obj, value) { obj.account = value; } }, metadata: _metadata }, _account_initializers, _account_extraInitializers); + __esDecorate(null, null, _password_decorators, { kind: "field", name: "password", static: false, private: false, access: { has: function (obj) { return "password" in obj; }, get: function (obj) { return obj.password; }, set: function (obj, value) { obj.password = value; } }, metadata: _metadata }, _password_initializers, _password_extraInitializers); + __esDecorate(null, null, _role_decorators, { kind: "field", name: "role", static: false, private: false, access: { has: function (obj) { return "role" in obj; }, get: function (obj) { return obj.role; }, set: function (obj, value) { obj.role = value; } }, metadata: _metadata }, _role_initializers, _role_extraInitializers); + if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + })(), + _a; +}(); +exports.LoginDto = LoginDto; diff --git a/reading-platform-backend/src/modules/auth/dto/login.dto.ts b/reading-platform-backend/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..71085dd --- /dev/null +++ b/reading-platform-backend/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,16 @@ +import { IsString, IsIn, IsNotEmpty } from 'class-validator'; + +export class LoginDto { + @IsString() + @IsNotEmpty() + account: string; + + @IsString() + @IsNotEmpty() + password: string; + + @IsString() + @IsIn(['admin', 'school', 'teacher', 'parent']) + @IsNotEmpty() + role: string; +} diff --git a/reading-platform-backend/src/modules/auth/strategies/jwt.strategy.js b/reading-platform-backend/src/modules/auth/strategies/jwt.strategy.js new file mode 100644 index 0000000..a79e469 --- /dev/null +++ b/reading-platform-backend/src/modules/auth/strategies/jwt.strategy.js @@ -0,0 +1,140 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.JwtStrategy = void 0; +var common_1 = require("@nestjs/common"); +var passport_1 = require("@nestjs/passport"); +var passport_jwt_1 = require("passport-jwt"); +var JwtStrategy = function () { + var _classDecorators = [(0, common_1.Injectable)()]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var _classSuper = (0, passport_1.PassportStrategy)(passport_jwt_1.Strategy); + var JwtStrategy = _classThis = /** @class */ (function (_super) { + __extends(JwtStrategy_1, _super); + function JwtStrategy_1(configService) { + var _this = _super.call(this, { + jwtFromRequest: passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET') || 'your-secret-key', + }) || this; + _this.configService = configService; + return _this; + } + JwtStrategy_1.prototype.validate = function (payload) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + if (!payload.sub || !payload.role) { + throw new common_1.UnauthorizedException(); + } + return [2 /*return*/, { + userId: payload.sub, + role: payload.role, + tenantId: payload.tenantId, + }]; + }); + }); + }; + return JwtStrategy_1; + }(_classSuper)); + __setFunctionName(_classThis, "JwtStrategy"); + (function () { + var _a; + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_a = _classSuper[Symbol.metadata]) !== null && _a !== void 0 ? _a : null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + JwtStrategy = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return JwtStrategy = _classThis; +}(); +exports.JwtStrategy = JwtStrategy; diff --git a/reading-platform-backend/src/modules/auth/strategies/jwt.strategy.ts b/reading-platform-backend/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..a790d52 --- /dev/null +++ b/reading-platform-backend/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,33 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; + +export interface JwtPayload { + sub: number; + role: string; + tenantId?: number; +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET') || 'your-secret-key', + }); + } + + async validate(payload: JwtPayload) { + if (!payload.sub || !payload.role) { + throw new UnauthorizedException(); + } + + return { + userId: payload.sub, + role: payload.role, + tenantId: payload.tenantId, + }; + } +} diff --git a/reading-platform-backend/src/modules/common/common.module.ts b/reading-platform-backend/src/modules/common/common.module.ts new file mode 100644 index 0000000..1f0dcdf --- /dev/null +++ b/reading-platform-backend/src/modules/common/common.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { RolesGuard } from './guards/roles.guard'; +import { LogInterceptor } from './interceptors/log.interceptor'; +import { OperationLogService } from './operation-log.service'; +import { SchoolOperationLogController, AdminOperationLogController } from './operation-log.controller'; + +@Module({ + controllers: [SchoolOperationLogController, AdminOperationLogController], + providers: [JwtAuthGuard, RolesGuard, LogInterceptor, OperationLogService], + exports: [JwtAuthGuard, RolesGuard, LogInterceptor, OperationLogService], +}) +export class CommonModule {} diff --git a/reading-platform-backend/src/modules/common/decorators/log-operation.decorator.ts b/reading-platform-backend/src/modules/common/decorators/log-operation.decorator.ts new file mode 100644 index 0000000..9dbca28 --- /dev/null +++ b/reading-platform-backend/src/modules/common/decorators/log-operation.decorator.ts @@ -0,0 +1,17 @@ +import { SetMetadata } from '@nestjs/common'; + +export const LOG_OPERATION_KEY = 'log_operation'; + +export interface LogOperationOptions { + action: string; // 操作类型: CREATE, UPDATE, DELETE, LOGIN, etc. + module: string; // 模块名称: 教师管理, 学生管理, etc. + description: string; // 操作描述 +} + +/** + * 操作日志装饰器 + * 用于标记需要记录操作日志的方法 + */ +export const LogOperation = (options: LogOperationOptions) => { + return SetMetadata(LOG_OPERATION_KEY, options); +}; diff --git a/reading-platform-backend/src/modules/common/decorators/roles.decorator.ts b/reading-platform-backend/src/modules/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..e038e16 --- /dev/null +++ b/reading-platform-backend/src/modules/common/decorators/roles.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/reading-platform-backend/src/modules/common/guards/jwt-auth.guard.ts b/reading-platform-backend/src/modules/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..0bfe520 --- /dev/null +++ b/reading-platform-backend/src/modules/common/guards/jwt-auth.guard.ts @@ -0,0 +1,24 @@ +import { Injectable, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + // 可以通过装饰器设置是否需要认证 + const requireAuth = this.reflector.getAllAndOverride('requireAuth', [ + context.getHandler(), + context.getClass(), + ]); + + if (requireAuth === false) { + return true; + } + + return super.canActivate(context); + } +} diff --git a/reading-platform-backend/src/modules/common/guards/roles.guard.ts b/reading-platform-backend/src/modules/common/guards/roles.guard.ts new file mode 100644 index 0000000..658d99c --- /dev/null +++ b/reading-platform-backend/src/modules/common/guards/roles.guard.ts @@ -0,0 +1,25 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + // 如果没有设置角色要求,则允许访问 + if (!requiredRoles) { + return true; + } + + const { user } = context.switchToHttp().getRequest(); + + // 检查用户角色是否在允许的角色列表中 + return requiredRoles.some((role) => user?.role === role); + } +} diff --git a/reading-platform-backend/src/modules/common/interceptors/log.interceptor.ts b/reading-platform-backend/src/modules/common/interceptors/log.interceptor.ts new file mode 100644 index 0000000..00a4470 --- /dev/null +++ b/reading-platform-backend/src/modules/common/interceptors/log.interceptor.ts @@ -0,0 +1,135 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Logger, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Observable, tap } from 'rxjs'; +import { PrismaService } from '../../../database/prisma.service'; +import { LOG_OPERATION_KEY, LogOperationOptions } from '../decorators/log-operation.decorator'; + +@Injectable() +export class LogInterceptor implements NestInterceptor { + private readonly logger = new Logger(LogInterceptor.name); + + constructor( + private reflector: Reflector, + private prisma: PrismaService, + ) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const logOptions = this.reflector.getAllAndOverride( + LOG_OPERATION_KEY, + [context.getHandler(), context.getClass()], + ); + + if (!logOptions) { + return next.handle(); + } + + const request = context.switchToHttp().getRequest(); + const user = request.user; + const startTime = Date.now(); + + // 获取请求体(用于记录变更前后数据) + const body = request.body; + const params = request.params; + + return next.handle().pipe( + tap({ + next: (response) => { + this.saveLog({ + user, + logOptions, + body, + params, + response, + ipAddress: this.getIpAddress(request), + userAgent: request.headers['user-agent'], + duration: Date.now() - startTime, + }); + }, + error: (error) => { + // 错误情况下也记录日志 + this.saveLog({ + user, + logOptions, + body, + params, + response: { error: error.message }, + ipAddress: this.getIpAddress(request), + userAgent: request.headers['user-agent'], + duration: Date.now() - startTime, + isError: true, + }); + }, + }), + ); + } + + private async saveLog(data: { + user: any; + logOptions: LogOperationOptions; + body: any; + params: any; + response: any; + ipAddress: string; + userAgent: string; + duration: number; + isError?: boolean; + }) { + try { + const { user, logOptions, body, params, response, ipAddress, userAgent } = data; + + // 获取目标ID + let targetId: number | undefined; + if (params?.id) { + targetId = parseInt(params.id, 10); + } else if (response?.id) { + targetId = response.id; + } else if (body?.id) { + targetId = body.id; + } + + // 构建描述 + let description = logOptions.description; + if (data.isError) { + description = `[失败] ${description}`; + } + + await this.prisma.operationLog.create({ + data: { + tenantId: user?.tenantId || null, + userId: user?.userId || 0, + userType: user?.role || 'UNKNOWN', + action: logOptions.action, + module: logOptions.module, + description, + targetId, + oldValue: body ? JSON.stringify(body) : null, + newValue: response ? JSON.stringify(response) : null, + ipAddress, + userAgent, + }, + }); + + this.logger.debug( + `Operation logged: ${logOptions.module} - ${logOptions.action} (${data.duration}ms)` + ); + } catch (error) { + this.logger.error('Failed to save operation log:', error); + } + } + + private getIpAddress(request: any): string { + return ( + request.headers['x-forwarded-for']?.split(',')[0]?.trim() || + request.headers['x-real-ip'] || + request.connection?.remoteAddress || + request.socket?.remoteAddress || + 'unknown' + ); + } +} diff --git a/reading-platform-backend/src/modules/common/operation-log.controller.ts b/reading-platform-backend/src/modules/common/operation-log.controller.ts new file mode 100644 index 0000000..4025c46 --- /dev/null +++ b/reading-platform-backend/src/modules/common/operation-log.controller.ts @@ -0,0 +1,56 @@ +import { + Controller, + Get, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { OperationLogService } from './operation-log.service'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { RolesGuard } from './guards/roles.guard'; +import { Roles } from './decorators/roles.decorator'; + +@Controller('school') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class SchoolOperationLogController { + constructor(private readonly logService: OperationLogService) {} + + @Get('operation-logs') + getLogs(@Request() req: any, @Query() query: any) { + return this.logService.getLogs(req.user.tenantId, query); + } + + @Get('operation-logs/stats') + getStats(@Request() req: any, @Query() query: any) { + return this.logService.getModuleStats(req.user.tenantId, query.startDate, query.endDate); + } + + @Get('operation-logs/:id') + getLogById(@Request() req: any, @Param('id') id: string) { + return this.logService.getLogById(req.user.tenantId, +id); + } +} + +@Controller('admin') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class AdminOperationLogController { + constructor(private readonly logService: OperationLogService) {} + + @Get('operation-logs') + getLogs(@Query() query: any) { + return this.logService.getLogs(null, query); + } + + @Get('operation-logs/stats') + getStats(@Query() query: any) { + return this.logService.getModuleStats(null, query.startDate, query.endDate); + } + + @Get('operation-logs/:id') + getLogById(@Param('id') id: string) { + return this.logService.getLogById(null, +id); + } +} diff --git a/reading-platform-backend/src/modules/common/operation-log.service.ts b/reading-platform-backend/src/modules/common/operation-log.service.ts new file mode 100644 index 0000000..395cb2c --- /dev/null +++ b/reading-platform-backend/src/modules/common/operation-log.service.ts @@ -0,0 +1,171 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class OperationLogService { + private readonly logger = new Logger(OperationLogService.name); + + constructor(private prisma: PrismaService) {} + + /** + * 获取操作日志列表 + */ + async getLogs(tenantId: number | null, query: { + page?: number; + pageSize?: number; + userId?: number; + userType?: string; + action?: string; + module?: string; + startDate?: string; + endDate?: string; + }) { + const { + page = 1, + pageSize = 20, + userId, + userType, + action, + module, + startDate, + endDate, + } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = {}; + + if (tenantId !== null) { + where.tenantId = tenantId; + } + + if (userId) { + where.userId = userId; + } + + if (userType) { + where.userType = userType; + } + + if (action) { + where.action = action; + } + + if (module) { + where.module = module; + } + + if (startDate || endDate) { + where.createdAt = {}; + if (startDate) { + where.createdAt.gte = new Date(startDate); + } + if (endDate) { + where.createdAt.lte = new Date(endDate); + } + } + + const [items, total] = await Promise.all([ + this.prisma.operationLog.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.operationLog.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + /** + * 获取日志详情 + */ + async getLogById(tenantId: number | null, id: number) { + const where: any = { id }; + + if (tenantId !== null) { + where.tenantId = tenantId; + } + + const log = await this.prisma.operationLog.findFirst({ + where, + }); + + if (!log) { + return null; + } + + // 解析 JSON 字段 + return { + ...log, + oldValue: log.oldValue ? this.safeParseJson(log.oldValue) : null, + newValue: log.newValue ? this.safeParseJson(log.newValue) : null, + }; + } + + /** + * 获取模块统计 + */ + async getModuleStats(tenantId: number | null, startDate?: string, endDate?: string) { + const where: any = {}; + + if (tenantId !== null) { + where.tenantId = tenantId; + } + + if (startDate || endDate) { + where.createdAt = {}; + if (startDate) { + where.createdAt.gte = new Date(startDate); + } + if (endDate) { + where.createdAt.lte = new Date(endDate); + } + } + + const logs = await this.prisma.operationLog.findMany({ + where, + select: { + module: true, + action: true, + }, + }); + + // 统计每个模块的操作数 + const moduleStats = new Map(); + const actionStats = new Map(); + + logs.forEach((log) => { + moduleStats.set(log.module, (moduleStats.get(log.module) || 0) + 1); + actionStats.set(log.action, (actionStats.get(log.action) || 0) + 1); + }); + + return { + modules: Array.from(moduleStats.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count), + actions: Array.from(actionStats.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count), + total: logs.length, + }; + } + + /** + * 安全解析 JSON + */ + private safeParseJson(str: string): any { + try { + return JSON.parse(str); + } catch { + return str; + } + } +} diff --git a/reading-platform-backend/src/modules/course-lesson/course-lesson.controller.ts b/reading-platform-backend/src/modules/course-lesson/course-lesson.controller.ts new file mode 100644 index 0000000..5145884 --- /dev/null +++ b/reading-platform-backend/src/modules/course-lesson/course-lesson.controller.ts @@ -0,0 +1,198 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + ParseIntPipe, + UseGuards, + Request, +} from '@nestjs/common'; +import { CourseLessonService } from './course-lesson.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +// 超管端课程控制器 +@Controller('admin/courses/:courseId/lessons') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class CourseLessonController { + constructor(private readonly lessonService: CourseLessonService) {} + + @Get() + async findAll(@Param('courseId', ParseIntPipe) courseId: number) { + return this.lessonService.findAll(courseId); + } + + @Get(':id') + async findOne( + @Param('courseId', ParseIntPipe) courseId: number, + @Param('id', ParseIntPipe) id: number, + ) { + return this.lessonService.findOne(id); + } + + @Get('type/:lessonType') + async findByType( + @Param('courseId', ParseIntPipe) courseId: number, + @Param('lessonType') lessonType: string, + ) { + return this.lessonService.findByType(courseId, lessonType); + } + + @Post() + async create( + @Param('courseId', ParseIntPipe) courseId: number, + @Body() + body: { + lessonType: string; + name: string; + description?: string; + duration?: number; + videoPath?: string; + videoName?: string; + pptPath?: string; + pptName?: string; + pdfPath?: string; + pdfName?: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + assessmentData?: string; + useTemplate?: boolean; + }, + ) { + return this.lessonService.create(courseId, body); + } + + @Put(':id') + async update( + @Param('id', ParseIntPipe) id: number, + @Body() + body: { + name?: string; + description?: string; + duration?: number; + videoPath?: string; + videoName?: string; + pptPath?: string; + pptName?: string; + pdfPath?: string; + pdfName?: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + assessmentData?: string; + useTemplate?: boolean; + }, + ) { + return this.lessonService.update(id, body); + } + + @Delete(':id') + async remove(@Param('id', ParseIntPipe) id: number) { + return this.lessonService.delete(id); + } + + @Put('reorder') + async reorder( + @Param('courseId', ParseIntPipe) courseId: number, + @Body() body: { lessonIds: number[] }, + ) { + return this.lessonService.reorder(courseId, body.lessonIds); + } + + // ==================== 教学环节管理 ==================== + + @Get(':lessonId/steps') + async findSteps(@Param('lessonId', ParseIntPipe) lessonId: number) { + return this.lessonService.findSteps(lessonId); + } + + @Post(':lessonId/steps') + async createStep( + @Param('lessonId', ParseIntPipe) lessonId: number, + @Body() + body: { + name: string; + content?: string; + duration?: number; + objective?: string; + resourceIds?: number[]; + }, + ) { + return this.lessonService.createStep(lessonId, body); + } + + @Put('steps/:stepId') + async updateStep( + @Param('stepId', ParseIntPipe) stepId: number, + @Body() + body: { + name?: string; + content?: string; + duration?: number; + objective?: string; + resourceIds?: number[]; + }, + ) { + return this.lessonService.updateStep(stepId, body); + } + + @Delete('steps/:stepId') + async removeStep(@Param('stepId', ParseIntPipe) stepId: number) { + return this.lessonService.deleteStep(stepId); + } + + @Put(':lessonId/steps/reorder') + async reorderSteps( + @Param('lessonId', ParseIntPipe) lessonId: number, + @Body() body: { stepIds: number[] }, + ) { + return this.lessonService.reorderSteps(lessonId, body.stepIds); + } +} + +// 教师端课程控制器 +@Controller('teacher/courses/:courseId/lessons') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class TeacherCourseLessonController { + constructor(private readonly lessonService: CourseLessonService) {} + + @Get() + async findAll( + @Param('courseId', ParseIntPipe) courseId: number, + @Request() req: any, + ) { + return this.lessonService.findCourseLessonsForTeacher(courseId, req.user.tenantId); + } + + @Get(':id') + async findOne( + @Param('courseId', ParseIntPipe) courseId: number, + @Param('id', ParseIntPipe) id: number, + @Request() req: any, + ) { + // 先验证权限 + await this.lessonService.findCourseLessonsForTeacher(courseId, req.user.tenantId); + return this.lessonService.findOne(id); + } + + @Get('type/:lessonType') + async findByType( + @Param('courseId', ParseIntPipe) courseId: number, + @Param('lessonType') lessonType: string, + @Request() req: any, + ) { + // 先验证权限 + await this.lessonService.findCourseLessonsForTeacher(courseId, req.user.tenantId); + return this.lessonService.findByType(courseId, lessonType); + } +} diff --git a/reading-platform-backend/src/modules/course-lesson/course-lesson.module.ts b/reading-platform-backend/src/modules/course-lesson/course-lesson.module.ts new file mode 100644 index 0000000..994abf5 --- /dev/null +++ b/reading-platform-backend/src/modules/course-lesson/course-lesson.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CourseLessonController, TeacherCourseLessonController } from './course-lesson.controller'; +import { CourseLessonService } from './course-lesson.service'; + +@Module({ + controllers: [CourseLessonController, TeacherCourseLessonController], + providers: [CourseLessonService], + exports: [CourseLessonService], +}) +export class CourseLessonModule {} diff --git a/reading-platform-backend/src/modules/course-lesson/course-lesson.service.ts b/reading-platform-backend/src/modules/course-lesson/course-lesson.service.ts new file mode 100644 index 0000000..52b32d4 --- /dev/null +++ b/reading-platform-backend/src/modules/course-lesson/course-lesson.service.ts @@ -0,0 +1,311 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class CourseLessonService { + constructor(private prisma: PrismaService) {} + + // ==================== 课程管理 ==================== + + async findAll(courseId: number) { + return this.prisma.courseLesson.findMany({ + where: { courseId }, + include: { + steps: { + orderBy: { sortOrder: 'asc' }, + }, + _count: { + select: { steps: true }, + }, + }, + orderBy: { sortOrder: 'asc' }, + }); + } + + async findOne(id: number) { + return this.prisma.courseLesson.findUnique({ + where: { id }, + include: { + steps: { + orderBy: { sortOrder: 'asc' }, + }, + course: { + select: { + id: true, + name: true, + coverImagePath: true, + }, + }, + }, + }); + } + + async findByType(courseId: number, lessonType: string) { + return this.prisma.courseLesson.findUnique({ + where: { + courseId_lessonType: { courseId, lessonType }, + }, + include: { + steps: { + orderBy: { sortOrder: 'asc' }, + }, + }, + }); + } + + async create( + courseId: number, + data: { + lessonType: string; + name: string; + description?: string; + duration?: number; + videoPath?: string; + videoName?: string; + pptPath?: string; + pptName?: string; + pdfPath?: string; + pdfName?: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + assessmentData?: string; + useTemplate?: boolean; + }, + ) { + // 检查是否已存在相同类型的课程 + const existing = await this.prisma.courseLesson.findUnique({ + where: { + courseId_lessonType: { courseId, lessonType: data.lessonType }, + }, + }); + + if (existing) { + throw new Error(`该课程包已存在 ${data.lessonType} 类型的课程`); + } + + // 获取最大排序号 + const maxSortOrder = await this.prisma.courseLesson.aggregate({ + where: { courseId }, + _max: { sortOrder: true }, + }); + + const lesson = await this.prisma.courseLesson.create({ + data: { + courseId, + ...data, + sortOrder: (maxSortOrder._max.sortOrder || 0) + 1, + }, + }); + + return lesson; + } + + async update( + id: number, + data: { + name?: string; + description?: string; + duration?: number; + videoPath?: string; + videoName?: string; + pptPath?: string; + pptName?: string; + pdfPath?: string; + pdfName?: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + assessmentData?: string; + useTemplate?: boolean; + }, + ) { + return this.prisma.courseLesson.update({ + where: { id }, + data, + }); + } + + async delete(id: number) { + const lesson = await this.prisma.courseLesson.findUnique({ + where: { id }, + }); + + if (!lesson) { + throw new Error('课程不存在'); + } + + await this.prisma.courseLesson.delete({ + where: { id }, + }); + + return { success: true }; + } + + async reorder(courseId: number, lessonIds: number[]) { + const updates = lessonIds.map((id, index) => + this.prisma.courseLesson.update({ + where: { id }, + data: { sortOrder: index + 1 }, + }), + ); + + return Promise.all(updates); + } + + // ==================== 教学环节管理 ==================== + + async findSteps(lessonId: number) { + return this.prisma.lessonStep.findMany({ + where: { lessonId }, + include: { + stepResources: { + include: { + resource: true, + }, + }, + }, + orderBy: { sortOrder: 'asc' }, + }); + } + + async createStep( + lessonId: number, + data: { + name: string; + content?: string; + duration?: number; + objective?: string; + resourceIds?: number[]; + }, + ) { + // 获取最大排序号 + const maxSortOrder = await this.prisma.lessonStep.aggregate({ + where: { lessonId }, + _max: { sortOrder: true }, + }); + + const step = await this.prisma.lessonStep.create({ + data: { + lessonId, + name: data.name, + content: data.content, + duration: data.duration || 5, + objective: data.objective, + resourceIds: data.resourceIds ? JSON.stringify(data.resourceIds) : null, + sortOrder: (maxSortOrder._max.sortOrder || 0) + 1, + }, + }); + + // 创建环节资源关联 + if (data.resourceIds && data.resourceIds.length > 0) { + await this.prisma.lessonStepResource.createMany({ + data: data.resourceIds.map((resourceId, index) => ({ + stepId: step.id, + resourceId, + sortOrder: index, + })), + }); + } + + return this.prisma.lessonStep.findUnique({ + where: { id: step.id }, + include: { + stepResources: { + include: { resource: true }, + }, + }, + }); + } + + async updateStep( + stepId: number, + data: { + name?: string; + content?: string; + duration?: number; + objective?: string; + resourceIds?: number[]; + }, + ) { + const updateData: any = { ...data }; + if (data.resourceIds !== undefined) { + updateData.resourceIds = JSON.stringify(data.resourceIds); + + // 删除旧的资源关联 + await this.prisma.lessonStepResource.deleteMany({ + where: { stepId }, + }); + + // 创建新的资源关联 + if (data.resourceIds.length > 0) { + await this.prisma.lessonStepResource.createMany({ + data: data.resourceIds.map((resourceId, index) => ({ + stepId, + resourceId, + sortOrder: index, + })), + }); + } + } + + return this.prisma.lessonStep.update({ + where: { id: stepId }, + data: updateData, + include: { + stepResources: { + include: { resource: true }, + }, + }, + }); + } + + async deleteStep(stepId: number) { + return this.prisma.lessonStep.delete({ + where: { id: stepId }, + }); + } + + async reorderSteps(lessonId: number, stepIds: number[]) { + const updates = stepIds.map((id, index) => + this.prisma.lessonStep.update({ + where: { id }, + data: { sortOrder: index + 1 }, + }), + ); + + return Promise.all(updates); + } + + // ==================== 教师端查询 ==================== + + async findCourseLessonsForTeacher(courseId: number, tenantId: number) { + // 检查租户是否有权限访问该课程 + const tenantCourse = await this.prisma.tenantCourse.findFirst({ + where: { tenantId, courseId, authorized: true }, + }); + + if (!tenantCourse) { + // 检查是否通过套餐授权 + const tenantPackage = await this.prisma.tenantPackage.findFirst({ + where: { + tenantId, + status: 'ACTIVE', + package: { + courses: { + some: { courseId }, + }, + }, + }, + }); + + if (!tenantPackage) { + throw new Error('无权访问该课程'); + } + } + + return this.findAll(courseId); + } +} diff --git a/reading-platform-backend/src/modules/course-package/course-package.controller.ts b/reading-platform-backend/src/modules/course-package/course-package.controller.ts new file mode 100644 index 0000000..c06a0f0 --- /dev/null +++ b/reading-platform-backend/src/modules/course-package/course-package.controller.ts @@ -0,0 +1,164 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + ParseIntPipe, + UseGuards, + Request, +} from '@nestjs/common'; +import { CoursePackageService } from './course-package.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('admin/packages') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class CoursePackageController { + constructor(private readonly packageService: CoursePackageService) {} + + // ==================== 套餐管理 ==================== + + @Get() + async findAll( + @Query('status') status?: string, + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + return this.packageService.findAllPackages({ + status, + page: page ? parseInt(page, 10) : 1, + pageSize: pageSize ? parseInt(pageSize, 10) : 20, + }); + } + + @Get(':id') + async findOne(@Param('id', ParseIntPipe) id: number) { + return this.packageService.findOnePackage(id); + } + + @Post() + async create( + @Body() + body: { + name: string; + description?: string; + price: number; + discountPrice?: number; + discountType?: string; + gradeLevels: string[]; + }, + ) { + return this.packageService.createPackage(body); + } + + @Put(':id') + async update( + @Param('id', ParseIntPipe) id: number, + @Body() + body: { + name?: string; + description?: string; + price?: number; + discountPrice?: number; + discountType?: string; + gradeLevels?: string[]; + }, + ) { + return this.packageService.updatePackage(id, body); + } + + @Delete(':id') + async remove(@Param('id', ParseIntPipe) id: number) { + return this.packageService.deletePackage(id); + } + + // ==================== 套餐课程管理 ==================== + + @Put(':id/courses') + async setCourses( + @Param('id', ParseIntPipe) id: number, + @Body() body: { courses: { courseId: number; gradeLevel: string; sortOrder?: number }[] }, + ) { + return this.packageService.setPackageCourses(id, body.courses); + } + + @Post(':id/courses') + async addCourse( + @Param('id', ParseIntPipe) id: number, + @Body() body: { courseId: number; gradeLevel: string; sortOrder?: number }, + ) { + return this.packageService.addCourseToPackage( + id, + body.courseId, + body.gradeLevel, + body.sortOrder, + ); + } + + @Delete(':id/courses/:courseId') + async removeCourse( + @Param('id', ParseIntPipe) id: number, + @Param('courseId', ParseIntPipe) courseId: number, + ) { + return this.packageService.removeCourseFromPackage(id, courseId); + } + + // ==================== 套餐状态管理 ==================== + + @Post(':id/submit') + async submit(@Param('id', ParseIntPipe) id: number, @Request() req: any) { + return this.packageService.submitPackage(id, req.user.id); + } + + @Post(':id/review') + async review( + @Param('id', ParseIntPipe) id: number, + @Body() body: { approved: boolean; comment?: string }, + @Request() req: any, + ) { + return this.packageService.reviewPackage(id, req.user.id, body.approved, body.comment); + } + + @Post(':id/publish') + async publish(@Param('id', ParseIntPipe) id: number) { + return this.packageService.publishPackage(id); + } + + @Post(':id/offline') + async offline(@Param('id', ParseIntPipe) id: number) { + return this.packageService.offlinePackage(id); + } +} + +// 学校端套餐控制器 +@Controller('school/packages') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class SchoolPackageController { + constructor(private readonly packageService: CoursePackageService) {} + + @Get() + async findTenantPackages(@Request() req: any) { + return this.packageService.findTenantPackages(req.user.tenantId); + } + + @Post(':id/renew') + async renewPackage( + @Param('id', ParseIntPipe) id: number, + @Body() body: { endDate: string; pricePaid?: number }, + @Request() req: any, + ) { + return this.packageService.renewTenantPackage( + req.user.tenantId, + id, + body.endDate, + body.pricePaid, + ); + } +} diff --git a/reading-platform-backend/src/modules/course-package/course-package.module.ts b/reading-platform-backend/src/modules/course-package/course-package.module.ts new file mode 100644 index 0000000..eee2056 --- /dev/null +++ b/reading-platform-backend/src/modules/course-package/course-package.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CoursePackageController, SchoolPackageController } from './course-package.controller'; +import { CoursePackageService } from './course-package.service'; + +@Module({ + controllers: [CoursePackageController, SchoolPackageController], + providers: [CoursePackageService], + exports: [CoursePackageService], +}) +export class CoursePackageModule {} diff --git a/reading-platform-backend/src/modules/course-package/course-package.service.ts b/reading-platform-backend/src/modules/course-package/course-package.service.ts new file mode 100644 index 0000000..a6385b0 --- /dev/null +++ b/reading-platform-backend/src/modules/course-package/course-package.service.ts @@ -0,0 +1,372 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class CoursePackageService { + constructor(private prisma: PrismaService) {} + + // ==================== 套餐管理 ==================== + + async findAllPackages(params: { + status?: string; + page?: number; + pageSize?: number; + }) { + const { status, page = 1, pageSize = 20 } = params; + const skip = (page - 1) * pageSize; + + const where: any = {}; + if (status) { + where.status = status; + } + + const [items, total] = await Promise.all([ + this.prisma.coursePackage.findMany({ + where, + include: { + _count: { + select: { courses: true, tenantPackages: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }), + this.prisma.coursePackage.count({ where }), + ]); + + return { + items: items.map((pkg) => ({ + ...pkg, + courseCount: pkg._count.courses, + tenantCount: pkg._count.tenantPackages, + })), + total, + page, + pageSize, + }; + } + + async findOnePackage(id: number) { + const pkg = await this.prisma.coursePackage.findUnique({ + where: { id }, + include: { + courses: { + include: { + course: { + select: { + id: true, + name: true, + coverImagePath: true, + duration: true, + gradeTags: true, + }, + }, + }, + orderBy: { sortOrder: 'asc' }, + }, + }, + }); + + if (!pkg) { + throw new Error('套餐不存在'); + } + + return pkg; + } + + async createPackage(data: { + name: string; + description?: string; + price: number; + discountPrice?: number; + discountType?: string; + gradeLevels: string[]; + }) { + return this.prisma.coursePackage.create({ + data: { + name: data.name, + description: data.description, + price: data.price, + discountPrice: data.discountPrice, + discountType: data.discountType, + gradeLevels: JSON.stringify(data.gradeLevels), + status: 'DRAFT', + }, + }); + } + + async updatePackage( + id: number, + data: { + name?: string; + description?: string; + price?: number; + discountPrice?: number; + discountType?: string; + gradeLevels?: string[]; + }, + ) { + const updateData: any = { ...data }; + if (data.gradeLevels) { + updateData.gradeLevels = JSON.stringify(data.gradeLevels); + } + + return this.prisma.coursePackage.update({ + where: { id }, + data: updateData, + }); + } + + async deletePackage(id: number) { + // 检查是否有租户正在使用 + const tenantCount = await this.prisma.tenantPackage.count({ + where: { packageId: id, status: 'ACTIVE' }, + }); + + if (tenantCount > 0) { + throw new Error(`有 ${tenantCount} 个租户正在使用该套餐,无法删除`); + } + + return this.prisma.coursePackage.delete({ + where: { id }, + }); + } + + // ==================== 套餐课程管理 ==================== + + async setPackageCourses( + packageId: number, + courses: { courseId: number; gradeLevel: string; sortOrder?: number }[], + ) { + // 删除现有关联 + await this.prisma.coursePackageCourse.deleteMany({ + where: { packageId }, + }); + + // 创建新关联 + if (courses.length > 0) { + await this.prisma.coursePackageCourse.createMany({ + data: courses.map((c, index) => ({ + packageId, + courseId: c.courseId, + gradeLevel: c.gradeLevel, + sortOrder: c.sortOrder ?? index, + })), + }); + } + + // 更新套餐课程数 + await this.prisma.coursePackage.update({ + where: { id: packageId }, + data: { courseCount: courses.length }, + }); + + return this.findOnePackage(packageId); + } + + async addCourseToPackage( + packageId: number, + courseId: number, + gradeLevel: string, + sortOrder?: number, + ) { + // 检查是否已存在 + const existing = await this.prisma.coursePackageCourse.findUnique({ + where: { + packageId_courseId: { packageId, courseId }, + }, + }); + + if (existing) { + throw new Error('该课程已在套餐中'); + } + + await this.prisma.coursePackageCourse.create({ + data: { + packageId, + courseId, + gradeLevel, + sortOrder: sortOrder ?? 0, + }, + }); + + // 更新套餐课程数 + const count = await this.prisma.coursePackageCourse.count({ + where: { packageId }, + }); + await this.prisma.coursePackage.update({ + where: { id: packageId }, + data: { courseCount: count }, + }); + + return this.findOnePackage(packageId); + } + + async removeCourseFromPackage(packageId: number, courseId: number) { + await this.prisma.coursePackageCourse.delete({ + where: { + packageId_courseId: { packageId, courseId }, + }, + }); + + // 更新套餐课程数 + const count = await this.prisma.coursePackageCourse.count({ + where: { packageId }, + }); + await this.prisma.coursePackage.update({ + where: { id: packageId }, + data: { courseCount: count }, + }); + + return this.findOnePackage(packageId); + } + + // ==================== 套餐状态管理 ==================== + + async submitPackage(id: number, userId: number) { + const pkg = await this.prisma.coursePackage.findUnique({ + where: { id }, + include: { _count: { select: { courses: true } } }, + }); + + if (!pkg) { + throw new Error('套餐不存在'); + } + + if (pkg._count.courses === 0) { + throw new Error('套餐必须包含至少一个课程包'); + } + + return this.prisma.coursePackage.update({ + where: { id }, + data: { + status: 'PENDING_REVIEW', + submittedAt: new Date(), + submittedBy: userId, + }, + }); + } + + async reviewPackage( + id: number, + userId: number, + approved: boolean, + comment?: string, + ) { + const pkg = await this.prisma.coursePackage.findUnique({ + where: { id }, + }); + + if (!pkg) { + throw new Error('套餐不存在'); + } + + if (pkg.status !== 'PENDING_REVIEW') { + throw new Error('只有待审核状态的套餐可以审核'); + } + + return this.prisma.coursePackage.update({ + where: { id }, + data: { + status: approved ? 'APPROVED' : 'REJECTED', + reviewedAt: new Date(), + reviewedBy: userId, + reviewComment: comment, + }, + }); + } + + async publishPackage(id: number) { + const pkg = await this.prisma.coursePackage.findUnique({ + where: { id }, + }); + + if (!pkg) { + throw new Error('套餐不存在'); + } + + if (pkg.status !== 'APPROVED') { + throw new Error('只有已审核通过的套餐可以发布'); + } + + return this.prisma.coursePackage.update({ + where: { id }, + data: { + status: 'PUBLISHED', + publishedAt: new Date(), + }, + }); + } + + async offlinePackage(id: number) { + return this.prisma.coursePackage.update({ + where: { id }, + data: { + status: 'OFFLINE', + }, + }); + } + + // ==================== 学校端查询 ==================== + + async findTenantPackages(tenantId: number) { + return this.prisma.tenantPackage.findMany({ + where: { + tenantId, + status: 'ACTIVE', + }, + include: { + package: { + include: { + courses: { + include: { + course: { + select: { + id: true, + name: true, + coverImagePath: true, + }, + }, + }, + }, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + async renewTenantPackage( + tenantId: number, + packageId: number, + endDate: string, + pricePaid?: number, + ) { + const existing = await this.prisma.tenantPackage.findFirst({ + where: { tenantId, packageId }, + }); + + if (existing) { + return this.prisma.tenantPackage.update({ + where: { id: existing.id }, + data: { + endDate, + status: 'ACTIVE', + pricePaid: pricePaid ?? existing.pricePaid, + }, + }); + } + + return this.prisma.tenantPackage.create({ + data: { + tenantId, + packageId, + startDate: new Date().toISOString().split('T')[0], + endDate, + status: 'ACTIVE', + pricePaid: pricePaid ?? 0, + }, + }); + } +} diff --git a/reading-platform-backend/src/modules/course/course-validation.service.ts b/reading-platform-backend/src/modules/course/course-validation.service.ts new file mode 100644 index 0000000..57cd36f --- /dev/null +++ b/reading-platform-backend/src/modules/course/course-validation.service.ts @@ -0,0 +1,270 @@ +import { Injectable, Logger } from '@nestjs/common'; + +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; + warnings: ValidationWarning[]; +} + +export interface ValidationError { + field: string; + message: string; + code: string; +} + +export interface ValidationWarning { + field: string; + message: string; + code: string; +} + +export interface CourseValidationData { + name?: string; + description?: string; + coverImagePath?: string; + gradeTags?: string; + domainTags?: string; + duration?: number; + ebookPaths?: string; + audioPaths?: string; + videoPaths?: string; + otherResources?: string; + scripts?: any[]; + lessonPlanData?: string; // JSON字符串,包含 phases 和 scriptPages +} + +/** + * 课程发布前验证服务 + * 验证课程内容的完整性和合规性 + */ +@Injectable() +export class CourseValidationService { + private readonly logger = new Logger(CourseValidationService.name); + + /** + * 验证课程是否可以提交审核 + */ + async validateForSubmit(course: CourseValidationData): Promise { + const errors: ValidationError[] = []; + const warnings: ValidationWarning[] = []; + + // 1. 验证基本信息 + this.validateBasicInfo(course, errors); + + // 2. 验证封面图片 + this.validateCover(course, errors); + + // 3. 验证年级标签 + this.validateGradeTags(course, errors); + + // 4. 验证课程时长 + this.validateDuration(course, errors); + + // 5. 验证数字资源 + this.validateResources(course, warnings); + + // 6. 验证教学流程 + this.validateScripts(course, errors); + + const result: ValidationResult = { + valid: errors.length === 0, + errors, + warnings, + }; + + this.logger.log(`Validation result: valid=${result.valid}, errors=${errors.length}, warnings=${warnings.length}`); + + return result; + } + + /** + * 验证基本信息 + */ + private validateBasicInfo(course: CourseValidationData, errors: ValidationError[]): void { + // 课程名称 + if (!course.name || course.name.trim().length === 0) { + errors.push({ + field: 'name', + message: '请输入课程名称', + code: 'NAME_REQUIRED', + }); + } else if (course.name.length < 2) { + errors.push({ + field: 'name', + message: '课程名称至少需要2个字符', + code: 'NAME_TOO_SHORT', + }); + } else if (course.name.length > 50) { + errors.push({ + field: 'name', + message: '课程名称不能超过50个字符', + code: 'NAME_TOO_LONG', + }); + } + } + + /** + * 验证封面图片 + */ + private validateCover(course: CourseValidationData, errors: ValidationError[]): void { + if (!course.coverImagePath) { + errors.push({ + field: 'coverImagePath', + message: '请上传课程封面', + code: 'COVER_REQUIRED', + }); + } + } + + /** + * 验证年级标签 + */ + private validateGradeTags(course: CourseValidationData, errors: ValidationError[]): void { + if (!course.gradeTags) { + errors.push({ + field: 'gradeTags', + message: '请选择适用年级', + code: 'GRADE_REQUIRED', + }); + return; + } + + try { + const grades = JSON.parse(course.gradeTags); + if (!Array.isArray(grades) || grades.length === 0) { + errors.push({ + field: 'gradeTags', + message: '请至少选择一个适用年级', + code: 'GRADE_EMPTY', + }); + } + } catch { + errors.push({ + field: 'gradeTags', + message: '年级标签格式错误', + code: 'GRADE_FORMAT_ERROR', + }); + } + } + + /** + * 验证课程时长 + */ + private validateDuration(course: CourseValidationData, errors: ValidationError[]): void { + if (course.duration === undefined || course.duration === null) { + errors.push({ + field: 'duration', + message: '请设置课程时长', + code: 'DURATION_REQUIRED', + }); + return; + } + + if (course.duration < 5) { + errors.push({ + field: 'duration', + message: '课程时长不能少于5分钟', + code: 'DURATION_TOO_SHORT', + }); + } else if (course.duration > 60) { + errors.push({ + field: 'duration', + message: '课程时长不能超过60分钟', + code: 'DURATION_TOO_LONG', + }); + } + } + + /** + * 验证数字资源 + */ + private validateResources(course: CourseValidationData, warnings: ValidationWarning[]): void { + const hasEbook = this.hasValidJsonArray(course.ebookPaths); + const hasAudio = this.hasValidJsonArray(course.audioPaths); + const hasVideo = this.hasValidJsonArray(course.videoPaths); + const hasOther = this.hasValidJsonArray(course.otherResources); + + if (!hasEbook && !hasAudio && !hasVideo && !hasOther) { + warnings.push({ + field: 'resources', + message: '建议上传至少1个数字资源(电子绘本、音频或视频)', + code: 'RESOURCES_SUGGESTED', + }); + } + } + + /** + * 验证教学流程 + */ + private validateScripts(course: CourseValidationData, errors: ValidationError[]): void { + // 优先检查 lessonPlanData 中的 phases(新数据结构) + if (course.lessonPlanData) { + try { + const lessonPlan = JSON.parse(course.lessonPlanData); + if (!lessonPlan.phases || !Array.isArray(lessonPlan.phases) || lessonPlan.phases.length === 0) { + errors.push({ + field: 'lessonPlanData', + message: '请至少配置一个教学环节', + code: 'SCRIPTS_REQUIRED', + }); + } + return; // 使用新数据结构验证完成 + } catch { + errors.push({ + field: 'lessonPlanData', + message: '教学计划数据格式错误', + code: 'LESSON_PLAN_FORMAT_ERROR', + }); + return; + } + } + + // 兼容旧的 scripts 字段 + if (course.scripts !== undefined) { + if (!Array.isArray(course.scripts) || course.scripts.length === 0) { + errors.push({ + field: 'scripts', + message: '请至少配置一个教学环节', + code: 'SCRIPTS_REQUIRED', + }); + } + } + } + + /** + * 检查是否是有效的JSON数组 + */ + private hasValidJsonArray(jsonStr: string | undefined | null): boolean { + if (!jsonStr) return false; + + try { + const arr = JSON.parse(jsonStr); + return Array.isArray(arr) && arr.length > 0; + } catch { + return false; + } + } + + /** + * 快速检查课程是否可以提交(只返回布尔值) + */ + async canSubmit(course: CourseValidationData): Promise { + const result = await this.validateForSubmit(course); + return result.valid; + } + + /** + * 获取验证摘要(用于显示) + */ + getValidationSummary(result: ValidationResult): string { + if (result.valid && result.warnings.length === 0) { + return '课程内容完整,可以提交审核'; + } + + if (result.valid && result.warnings.length > 0) { + return `课程可以提交,但有 ${result.warnings.length} 条建议`; + } + + return `课程有 ${result.errors.length} 个问题需要修复`; + } +} diff --git a/reading-platform-backend/src/modules/course/course.controller.ts b/reading-platform-backend/src/modules/course/course.controller.ts new file mode 100644 index 0000000..56b5cb8 --- /dev/null +++ b/reading-platform-backend/src/modules/course/course.controller.ts @@ -0,0 +1,161 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { CourseService } from './course.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('courses') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class CourseController { + constructor(private readonly courseService: CourseService) {} + + @Get() + findAll(@Query() query: any) { + return this.courseService.findAll(query); + } + + @Get('review') + getReviewList(@Query() query: any) { + return this.courseService.getReviewList(query); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.courseService.findOne(+id); + } + + @Get(':id/stats') + getStats(@Param('id') id: string) { + return this.courseService.getStats(+id); + } + + @Get(':id/validate') + validate(@Param('id') id: string) { + return this.courseService.validate(+id); + } + + @Get(':id/versions') + getVersionHistory(@Param('id') id: string) { + return this.courseService.getVersionHistory(+id); + } + + @Post() + create(@Body() createCourseDto: any, @Request() req: any) { + return this.courseService.create({ + ...createCourseDto, + createdBy: req.user?.userId, + }); + } + + @Put(':id') + update(@Param('id') id: string, @Body() updateCourseDto: any) { + return this.courseService.update(+id, updateCourseDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.courseService.remove(+id); + } + + /** + * 提交审核 + * POST /api/v1/courses/:id/submit + */ + @Post(':id/submit') + submit(@Param('id') id: string, @Body() body: { copyrightConfirmed: boolean }, @Request() req: any) { + const userId = req.user?.userId || 0; + return this.courseService.submit(+id, userId, body.copyrightConfirmed); + } + + /** + * 撤销审核申请 + * POST /api/v1/courses/:id/withdraw + */ + @Post(':id/withdraw') + withdraw(@Param('id') id: string, @Request() req: any) { + const userId = req.user?.userId || 0; + return this.courseService.withdraw(+id, userId); + } + + /** + * 审核通过 + * POST /api/v1/courses/:id/approve + */ + @Post(':id/approve') + approve( + @Param('id') id: string, + @Body() body: { checklist?: any; comment?: string }, + @Request() req: any, + ) { + const reviewerId = req.user?.userId || 0; + return this.courseService.approve(+id, reviewerId, body); + } + + /** + * 审核驳回 + * POST /api/v1/courses/:id/reject + */ + @Post(':id/reject') + reject( + @Param('id') id: string, + @Body() body: { checklist?: any; comment: string }, + @Request() req: any, + ) { + const reviewerId = req.user?.userId || 0; + return this.courseService.reject(+id, reviewerId, body); + } + + /** + * 直接发布(超级管理员专用) + * POST /api/v1/courses/:id/direct-publish + */ + @Post(':id/direct-publish') + @Roles('admin') + directPublish( + @Param('id') id: string, + @Body() body: { skipValidation?: boolean }, + @Request() req: any, + ) { + const userId = req.user?.userId || 0; + return this.courseService.directPublish(+id, userId, body.skipValidation); + } + + /** + * 发布课程(兼容旧API) + * POST /api/v1/courses/:id/publish + */ + @Post(':id/publish') + publish(@Param('id') id: string) { + return this.courseService.publish(+id); + } + + /** + * 下架课程 + * POST /api/v1/courses/:id/unpublish + */ + @Post(':id/unpublish') + unpublish(@Param('id') id: string) { + return this.courseService.unpublish(+id); + } + + /** + * 重新发布已下架的课程 + * POST /api/v1/courses/:id/republish + */ + @Post(':id/republish') + republish(@Param('id') id: string) { + return this.courseService.republish(+id); + } +} diff --git a/reading-platform-backend/src/modules/course/course.module.ts b/reading-platform-backend/src/modules/course/course.module.ts new file mode 100644 index 0000000..b1803a6 --- /dev/null +++ b/reading-platform-backend/src/modules/course/course.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { CourseController } from './course.controller'; +import { CourseService } from './course.service'; +import { CourseValidationService } from './course-validation.service'; +import { PrismaModule } from '../../database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [CourseController], + providers: [CourseService, CourseValidationService], + exports: [CourseService, CourseValidationService], +}) +export class CourseModule {} diff --git a/reading-platform-backend/src/modules/course/course.service.ts b/reading-platform-backend/src/modules/course/course.service.ts new file mode 100644 index 0000000..64f738b --- /dev/null +++ b/reading-platform-backend/src/modules/course/course.service.ts @@ -0,0 +1,1033 @@ +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { CourseValidationService, ValidationResult } from './course-validation.service'; + +@Injectable() +export class CourseService { + private readonly logger = new Logger(CourseService.name); + + constructor( + private prisma: PrismaService, + private validationService: CourseValidationService, + ) {} + + async findAll(query: any) { + const { page = 1, pageSize = 10, grade, status, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = {}; + + // 筛选条件 + if (status) { + where.status = status; + } + + if (keyword) { + where.name = { contains: keyword }; + } + + // 年级筛选 - SQLite使用字符串包含匹配 + if (grade) { + // 搜索英文值(数据库存储的是英文) + // 支持大小写不敏感搜索 + const gradeUpper = grade.toUpperCase(); + where.OR = [ + { gradeTags: { contains: gradeUpper } }, // 大写格式: SMALL, MIDDLE, BIG + { gradeTags: { contains: grade.toLowerCase() } }, // 小写格式: small, middle, big + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.course.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + name: true, + pictureBookName: true, + gradeTags: true, + status: true, + version: true, + usageCount: true, + teacherCount: true, + avgRating: true, + createdAt: true, + updatedAt: true, + submittedAt: true, + reviewedAt: true, + reviewComment: true, + themeId: true, + coreContent: true, + coverImagePath: true, + domainTags: true, + theme: { + select: { id: true, name: true }, + }, + }, + }), + this.prisma.course.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findOne(id: number) { + const course = await this.prisma.course.findUnique({ + where: { id }, + include: { + theme: { + select: { id: true, name: true }, + }, + resources: { + orderBy: { sortOrder: 'asc' }, + }, + scripts: { + orderBy: { sortOrder: 'asc' }, + include: { + pages: { + orderBy: { pageNumber: 'asc' }, + }, + }, + }, + activities: { + orderBy: { sortOrder: 'asc' }, + }, + courseLessons: { + orderBy: { sortOrder: 'asc' }, + include: { + steps: { + orderBy: { sortOrder: 'asc' }, + }, + }, + }, + }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + return course; + } + + async create(createCourseDto: any) { + try { + this.logger.log(`Creating course with data: ${JSON.stringify(createCourseDto)}`); + const result = await this.prisma.course.create({ + data: createCourseDto, + }); + this.logger.log(`Course created successfully with ID: ${result.id}`); + return result; + } catch (error) { + this.logger.error(`Error creating course: ${error.message}`, error.stack); + throw error; + } + } + + async update(id: number, updateCourseDto: any) { + // 需要明确设置为 null 的字段列表(与 schema.prisma 保持一致) + const fieldsToClear = [ + 'coverImagePath', + 'ebookPaths', + 'audioPaths', + 'videoPaths', + 'otherResources', + 'pptPath', + 'pptName', + 'posterPaths', + 'tools', + 'studentMaterials', + 'pictureBookName', + 'lessonPlanData', + 'activitiesData', + 'assessmentData', + // 新增课程介绍字段 + 'introSummary', + 'introHighlights', + 'introGoals', + 'introSchedule', + 'introKeyPoints', + 'introMethods', + 'introEvaluation', + 'introNotes', + 'coreContent', + 'scheduleRefData', + 'environmentConstruction', + ]; + + const cleanedData: any = {}; + + for (const [key, value] of Object.entries(updateCourseDto)) { + // 对于可以清除的字段,如果是 null 或空字符串,设置为 null + if (fieldsToClear.includes(key) && (value === null || value === '')) { + cleanedData[key] = null; + } + // 对于其他字段,如果有值则添加 + else if (value !== undefined) { + cleanedData[key] = value; + } + } + + this.logger.log(`Updating course ${id} with data: ${JSON.stringify(Object.keys(cleanedData))}`); + + // 使用事务更新课程和关联表数据 + return this.prisma.$transaction(async (tx) => { + // 更新课程主表 + const updatedCourse = await tx.course.update({ + where: { id }, + data: cleanedData, + }); + + // 同步 lessonPlanData 到 course_scripts 关联表 + if (updateCourseDto.lessonPlanData !== undefined) { + await this.syncLessonPlanToScripts(tx, id, updateCourseDto.lessonPlanData); + } + + // 同步 activitiesData 到 course_activities 关联表 + if (updateCourseDto.activitiesData !== undefined) { + await this.syncActivitiesToTable(tx, id, updateCourseDto.activitiesData); + } + + return updatedCourse; + }); + } + + /** + * 将 lessonPlanData 同步到 course_scripts 关联表 + * 支持新格式(pages在每个phase内部)和旧格式(scriptPages在顶层) + */ + private async syncLessonPlanToScripts(tx: any, courseId: number, lessonPlanData: string | null) { + // 先删除旧的 scripts 和 pages + await tx.courseScriptPage.deleteMany({ + where: { script: { courseId } }, + }); + await tx.courseScript.deleteMany({ + where: { courseId }, + }); + + if (!lessonPlanData) { + this.logger.log(`Course ${courseId}: lessonPlanData is null, cleared scripts`); + return; + } + + try { + const lessonPlan = JSON.parse(lessonPlanData); + const phases = lessonPlan.phases || []; + // 兼容旧格式:顶层的 scriptPages + const topLevelScriptPages = lessonPlan.scriptPages || []; + + // 调试日志 + this.logger.log(`=== 同步课程 ${courseId} 的教学脚本 ===`); + this.logger.log(`phases 数量: ${phases.length}`); + this.logger.log(`顶层 scriptPages 数量: ${topLevelScriptPages.length}`); + + for (let i = 0; i < phases.length; i++) { + const phase = phases[i]; + this.logger.log(`Phase ${i}: name=${phase.name}, pages=${phase.pages?.length || 0}, enablePageScript=${phase.enablePageScript}`); + + // 创建 script 记录 + const script = await tx.courseScript.create({ + data: { + courseId, + stepIndex: i + 1, + stepName: phase.name || `步骤${i + 1}`, + stepType: phase.type || 'CUSTOM', + duration: phase.duration || 5, + objective: phase.objective || null, + teacherScript: phase.content || null, + interactionPoints: null, + resourceIds: phase.resourceIds ? JSON.stringify(phase.resourceIds) : null, + sortOrder: i, + }, + }); + + // 优先使用 phase 内部的 pages(新格式) + // 如果没有,则兼容旧格式:第一个 phase 使用顶层的 scriptPages + let pagesToCreate = phase.pages || []; + if (pagesToCreate.length === 0 && topLevelScriptPages.length > 0 && i === 0) { + pagesToCreate = topLevelScriptPages; + } + + // 创建逐页配置 + if (pagesToCreate.length > 0) { + this.logger.log(`为 Phase ${i} 创建 ${pagesToCreate.length} 页逐页脚本`); + for (const page of pagesToCreate) { + await tx.courseScriptPage.create({ + data: { + scriptId: script.id, + pageNumber: page.pageNumber, + questions: page.teacherScript || null, + interactionComponent: page.actions ? JSON.stringify(page.actions) : null, + teacherNotes: page.notes || null, + resourceIds: page.resourceIds ? JSON.stringify(page.resourceIds) : null, + }, + }); + } + } + } + + this.logger.log(`Course ${courseId}: synced ${phases.length} scripts from lessonPlanData`); + } catch (error) { + this.logger.error(`Failed to sync lessonPlanData for course ${courseId}: ${error.message}`); + } + } + + /** + * 将 activitiesData 同步到 course_activities 关联表 + */ + private async syncActivitiesToTable(tx: any, courseId: number, activitiesData: string | null) { + // 先删除旧的活动 + await tx.courseActivity.deleteMany({ + where: { courseId }, + }); + + if (!activitiesData) { + this.logger.log(`Course ${courseId}: activitiesData is null, cleared activities`); + return; + } + + try { + const activities = JSON.parse(activitiesData); + + for (let i = 0; i < activities.length; i++) { + const activity = activities[i]; + + await tx.courseActivity.create({ + data: { + courseId, + name: activity.name || `活动${i + 1}`, + domain: activity.domain || null, // 使用单独的 domain 字段,不再错误地使用 type + domainTagId: null, + activityType: this.mapActivityType(activity.type), + duration: activity.duration || 15, + onlineMaterials: activity.content ? JSON.stringify({ content: activity.content }) : null, + offlineMaterials: activity.materials || null, + activityGuide: null, + objectives: null, + sortOrder: i, + }, + }); + } + + this.logger.log(`Course ${courseId}: synced ${activities.length} activities from activitiesData`); + } catch (error) { + this.logger.error(`Failed to sync activitiesData for course ${courseId}: ${error.message}`); + } + } + + /** + * 映射活动类型 + */ + private mapActivityType(type: string | undefined): string { + const typeMap: Record = { + 'family': 'FAMILY', + 'art': 'ART', + 'game': 'GAME', + 'outdoor': 'OUTDOOR', + 'other': 'OTHER', + 'handicraft': 'HANDICRAFT', + 'music': 'MUSIC', + 'exploration': 'EXPLORATION', + 'sports': 'SPORTS', + // 中文兼容 + '家庭延伸': 'FAMILY', + '美工活动': 'ART', + '游戏活动': 'GAME', + '户外活动': 'OUTDOOR', + '其他': 'OTHER', + '手工活动': 'HANDICRAFT', + '音乐活动': 'MUSIC', + '探索活动': 'EXPLORATION', + '运动活动': 'SPORTS', + '亲子活动': 'FAMILY', + }; + return typeMap[type || ''] || 'OTHER'; + } + + async remove(id: number) { + // Check if course has usage records + const usageCount = await this.prisma.lesson.count({ + where: { courseId: id }, + }); + + if (usageCount > 0) { + throw new BadRequestException(`该课程包已被使用${usageCount}次,无法删除`); + } + + // Delete related records in order + // Delete course lessons (if any) + await this.prisma.courseLesson.deleteMany({ + where: { courseId: id }, + }); + + // Delete tenant course authorizations (if any) + await this.prisma.tenantCourse.deleteMany({ + where: { courseId: id }, + }); + + // Delete course resources (if any) + await this.prisma.courseResource.deleteMany({ + where: { courseId: id }, + }); + + // Delete course scripts (if any) + await this.prisma.courseScript.deleteMany({ + where: { courseId: id }, + }); + + // Delete course activities (if any) + await this.prisma.courseActivity.deleteMany({ + where: { courseId: id }, + }); + + // Delete course versions (if any) + await this.prisma.courseVersion.deleteMany({ + where: { courseId: id }, + }); + + // Delete schedule plans (if any) + await this.prisma.schedulePlan.deleteMany({ + where: { courseId: id }, + }); + + // Delete schedule templates (if any) + await this.prisma.scheduleTemplate.deleteMany({ + where: { courseId: id }, + }); + + // Delete tasks (if any) + await this.prisma.task.deleteMany({ + where: { relatedCourseId: id }, + }); + + // Delete task templates (if any) + await this.prisma.taskTemplate.deleteMany({ + where: { relatedCourseId: id }, + }); + + // Delete package course relations (if any) + await this.prisma.coursePackageCourse.deleteMany({ + where: { courseId: id }, + }); + + // Delete school courses (if any) + await this.prisma.schoolCourse.deleteMany({ + where: { sourceCourseId: id }, + }); + + // Finally delete the course + return this.prisma.course.delete({ + where: { id }, + }); + } + + /** + * 验证课程完整性 + */ + async validate(id: number): Promise { + const course = await this.findOne(id); + return this.validationService.validateForSubmit(course); + } + + /** + * 提交审核 + */ + async submit(id: number, userId: number, copyrightConfirmed: boolean) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + // 检查当前状态是否可以提交 + if (course.status !== 'DRAFT' && course.status !== 'REJECTED') { + throw new BadRequestException(`课程状态为 ${course.status},无法提交审核`); + } + + // 验证课程完整性 + const validationResult = await this.validationService.validateForSubmit(course); + if (!validationResult.valid) { + throw new BadRequestException({ + message: '课程内容不完整,请检查以下问题', + errors: validationResult.errors, + warnings: validationResult.warnings, + }); + } + + // 版权确认 + if (!copyrightConfirmed) { + throw new BadRequestException('请确认版权合规'); + } + + // 更新课程状态 + const updatedCourse = await this.prisma.course.update({ + where: { id }, + data: { + status: 'PENDING', + submittedAt: new Date(), + submittedBy: userId, + }, + }); + + this.logger.log(`Course ${id} submitted for review by user ${userId}`); + + return { + ...updatedCourse, + validationSummary: this.validationService.getValidationSummary(validationResult), + }; + } + + /** + * 撤销审核申请 + */ + async withdraw(id: number, userId: number) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + if (course.status !== 'PENDING') { + throw new BadRequestException(`课程状态为 ${course.status},无法撤销`); + } + + const updatedCourse = await this.prisma.course.update({ + where: { id }, + data: { + status: 'DRAFT', + submittedAt: null, + submittedBy: null, + }, + }); + + this.logger.log(`Course ${id} review withdrawn by user ${userId}`); + + return updatedCourse; + } + + /** + * 审核通过并发布 + */ + async approve(id: number, reviewerId: number, reviewData: { checklist?: any; comment?: string }) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + if (course.status !== 'PENDING') { + throw new BadRequestException(`课程状态为 ${course.status},无法审核`); + } + + // 禁止自审 + if (course.submittedBy === reviewerId) { + throw new BadRequestException('不能审核自己提交的课程'); + } + + // 使用事务更新课程状态并创建版本快照 + const result = await this.prisma.$transaction(async (tx) => { + // 更新课程状态 + const updatedCourse = await tx.course.update({ + where: { id }, + data: { + status: 'PUBLISHED', + reviewedAt: new Date(), + reviewedBy: reviewerId, + reviewComment: reviewData.comment || null, + reviewChecklist: reviewData.checklist ? JSON.stringify(reviewData.checklist) : null, + publishedAt: new Date(), + }, + }); + + // 创建版本快照 + await tx.courseVersion.create({ + data: { + courseId: id, + version: course.version, + snapshotData: JSON.stringify(course), + changeLog: reviewData.comment || '审核通过发布', + publishedBy: reviewerId, + }, + }); + + return updatedCourse; + }); + + // 授权给所有活跃租户 + const activeTenants = await this.prisma.tenant.findMany({ + where: { status: 'ACTIVE' }, + select: { id: true }, + }); + + this.logger.log(`Publishing course ${id} to ${activeTenants.length} active tenants`); + + for (const tenant of activeTenants) { + await this.prisma.tenantCourse.upsert({ + where: { + tenantId_courseId: { + tenantId: tenant.id, + courseId: id, + }, + }, + update: { + authorized: true, + authorizedAt: new Date(), + }, + create: { + tenantId: tenant.id, + courseId: id, + authorized: true, + authorizedAt: new Date(), + }, + }); + } + + this.logger.log(`Course ${id} approved and published by reviewer ${reviewerId}`); + + return { + ...result, + authorizedTenantCount: activeTenants.length, + }; + } + + /** + * 审核驳回 + */ + async reject(id: number, reviewerId: number, reviewData: { checklist?: any; comment: string }) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + if (course.status !== 'PENDING') { + throw new BadRequestException(`课程状态为 ${course.status},无法审核`); + } + + // 禁止自审 + if (course.submittedBy === reviewerId) { + throw new BadRequestException('不能审核自己提交的课程'); + } + + if (!reviewData.comment || reviewData.comment.trim().length === 0) { + throw new BadRequestException('请填写驳回原因'); + } + + const updatedCourse = await this.prisma.course.update({ + where: { id }, + data: { + status: 'REJECTED', + reviewedAt: new Date(), + reviewedBy: reviewerId, + reviewComment: reviewData.comment, + reviewChecklist: reviewData.checklist ? JSON.stringify(reviewData.checklist) : null, + }, + }); + + this.logger.log(`Course ${id} rejected by reviewer ${reviewerId}: ${reviewData.comment}`); + + return updatedCourse; + } + + /** + * 直接发布(超级管理员专用) + */ + async directPublish(id: number, userId: number, skipValidation: boolean = false) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + // 检查课程状态 + if (course.status === 'PUBLISHED') { + throw new BadRequestException('课程已发布'); + } + + // 验证课程完整性(即使跳过也要记录) + const validationResult = await this.validationService.validateForSubmit(course); + + if (!skipValidation && !validationResult.valid) { + throw new BadRequestException({ + message: '课程内容不完整,请检查以下问题', + errors: validationResult.errors, + warnings: validationResult.warnings, + }); + } + + // 使用事务更新课程状态并创建版本快照 + const result = await this.prisma.$transaction(async (tx) => { + const updatedCourse = await tx.course.update({ + where: { id }, + data: { + status: 'PUBLISHED', + publishedAt: new Date(), + reviewedAt: new Date(), + reviewedBy: userId, + reviewComment: '超级管理员直接发布', + }, + }); + + // 创建版本快照 + await tx.courseVersion.create({ + data: { + courseId: id, + version: course.version, + snapshotData: JSON.stringify(course), + changeLog: '超级管理员直接发布', + publishedBy: userId, + }, + }); + + return updatedCourse; + }); + + // 授权给所有活跃租户 + const activeTenants = await this.prisma.tenant.findMany({ + where: { status: 'ACTIVE' }, + select: { id: true }, + }); + + for (const tenant of activeTenants) { + await this.prisma.tenantCourse.upsert({ + where: { + tenantId_courseId: { + tenantId: tenant.id, + courseId: id, + }, + }, + update: { + authorized: true, + authorizedAt: new Date(), + }, + create: { + tenantId: tenant.id, + courseId: id, + authorized: true, + authorizedAt: new Date(), + }, + }); + } + + this.logger.log(`Course ${id} directly published by super admin ${userId}`); + + return { + ...result, + authorizedTenantCount: activeTenants.length, + validationSkipped: skipValidation && !validationResult.valid, + validationWarnings: validationResult.warnings, + }; + } + + /** + * 发布课程(兼容旧API) + */ + async publish(id: number) { + // 旧的publish方法改为调用directPublish + return this.directPublish(id, 0, false); + } + + /** + * 下架课程 + */ + async unpublish(id: number) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + if (course.status !== 'PUBLISHED') { + throw new BadRequestException(`课程状态为 ${course.status},无法下架`); + } + + const updatedCourse = await this.prisma.course.update({ + where: { id }, + data: { + status: 'ARCHIVED', + }, + }); + + // 取消所有租户的授权 + await this.prisma.tenantCourse.updateMany({ + where: { courseId: id }, + data: { + authorized: false, + }, + }); + + this.logger.log(`Course ${id} unpublished`); + + return updatedCourse; + } + + /** + * 重新发布已下架的课程 + */ + async republish(id: number) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + if (course.status !== 'ARCHIVED') { + throw new BadRequestException(`课程状态为 ${course.status},无法重新发布`); + } + + const updatedCourse = await this.prisma.course.update({ + where: { id }, + data: { + status: 'PUBLISHED', + }, + }); + + // 重新授权给所有活跃租户 + const activeTenants = await this.prisma.tenant.findMany({ + where: { status: 'ACTIVE' }, + select: { id: true }, + }); + + for (const tenant of activeTenants) { + await this.prisma.tenantCourse.upsert({ + where: { + tenantId_courseId: { + tenantId: tenant.id, + courseId: id, + }, + }, + update: { + authorized: true, + authorizedAt: new Date(), + }, + create: { + tenantId: tenant.id, + courseId: id, + authorized: true, + authorizedAt: new Date(), + }, + }); + } + + this.logger.log(`Course ${id} republished`); + + return { + ...updatedCourse, + authorizedTenantCount: activeTenants.length, + }; + } + + async getStats(id: number) { + const course = await this.prisma.course.findUnique({ + where: { id }, + select: { + id: true, + name: true, + usageCount: true, + teacherCount: true, + avgRating: true, + }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + const lessons = await this.prisma.lesson.findMany({ + where: { courseId: id }, + include: { + teacher: { + select: { id: true, name: true }, + }, + class: { + select: { id: true, name: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + take: 10, + }); + + const feedbacks = await this.prisma.lessonFeedback.findMany({ + where: { + lesson: { + courseId: id, + }, + }, + }); + + const calculateAverage = (field: string) => { + const validFeedbacks = feedbacks.filter((f: any) => f[field] != null); + if (validFeedbacks.length === 0) return 0; + const sum = validFeedbacks.reduce((acc: number, f: any) => acc + f[field], 0); + return sum / validFeedbacks.length; + }; + + const studentRecords = await this.prisma.studentRecord.findMany({ + where: { + lesson: { + courseId: id, + }, + }, + }); + + const calculateStudentAvg = (field: string) => { + const validRecords = studentRecords.filter((r: any) => r[field] != null); + if (validRecords.length === 0) return 0; + const sum = validRecords.reduce((acc: number, r: any) => acc + r[field], 0); + return sum / validRecords.length; + }; + + const now = new Date(); + const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const recentLessons = await this.prisma.lesson.findMany({ + where: { + courseId: id, + createdAt: { gte: weekAgo }, + }, + select: { + createdAt: true, + }, + }); + + const lessonTrend = []; + for (let i = 6; i >= 0; i--) { + const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000); + const dateStr = date.toLocaleDateString('zh-CN', { weekday: 'short' }); + const count = recentLessons.filter((lesson: any) => { + const lessonDate = new Date(lesson.createdAt); + return lessonDate.toDateString() === date.toDateString(); + }).length; + lessonTrend.push({ date: dateStr, count }); + } + + const uniqueStudentIds = new Set(); + lessons.forEach((lesson: any) => { + uniqueStudentIds.add(lesson.classId); + }); + + return { + courseName: course.name, + totalLessons: course.usageCount || lessons.length, + totalTeachers: course.teacherCount || new Set(lessons.map((l: any) => l.teacherId)).size, + totalStudents: uniqueStudentIds.size, + avgRating: course.avgRating || 0, + lessonTrend, + feedbackDistribution: { + designQuality: calculateAverage('designQuality'), + participation: calculateAverage('participation'), + goalAchievement: calculateAverage('goalAchievement'), + totalFeedbacks: feedbacks.length, + }, + recentLessons: lessons.map((lesson: any) => ({ + ...lesson, + date: lesson.createdAt, + })), + studentPerformance: { + avgFocus: calculateStudentAvg('focus'), + avgParticipation: calculateStudentAvg('participation'), + avgInterest: calculateStudentAvg('interest'), + avgUnderstanding: calculateStudentAvg('understanding'), + }, + }; + } + + /** + * 获取审核列表 + */ + async getReviewList(query: any) { + const { page = 1, pageSize = 10, status, submittedBy } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + status: { in: ['PENDING', 'REJECTED'] }, + }; + + if (status) { + where.status = status; + } + + if (submittedBy) { + where.submittedBy = +submittedBy; + } + + const [items, total] = await Promise.all([ + this.prisma.course.findMany({ + where, + skip, + take, + orderBy: { submittedAt: 'desc' }, + select: { + id: true, + name: true, + status: true, + submittedAt: true, + submittedBy: true, + reviewedAt: true, + reviewedBy: true, + reviewComment: true, + coverImagePath: true, + gradeTags: true, + }, + }), + this.prisma.course.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + /** + * 获取版本历史 + */ + async getVersionHistory(id: number) { + const course = await this.prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + throw new NotFoundException(`Course #${id} not found`); + } + + const versions = await this.prisma.courseVersion.findMany({ + where: { courseId: id }, + orderBy: { publishedAt: 'desc' }, + }); + + return versions.map((v) => ({ + id: v.id, + version: v.version, + changeLog: v.changeLog, + publishedAt: v.publishedAt, + publishedBy: v.publishedBy, + })); + } +} diff --git a/reading-platform-backend/src/modules/export/export.controller.ts b/reading-platform-backend/src/modules/export/export.controller.ts new file mode 100644 index 0000000..1b42e6c --- /dev/null +++ b/reading-platform-backend/src/modules/export/export.controller.ts @@ -0,0 +1,88 @@ +import { + Controller, + Get, + Param, + Query, + UseGuards, + Request, + Res, +} from '@nestjs/common'; +import { Response } from 'express'; +import { ExportService } from './export.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('school/export') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class ExportController { + constructor(private readonly exportService: ExportService) {} + + @Get('teachers') + async exportTeachers(@Request() req: any, @Res() res: Response) { + const buffer = await this.exportService.exportTeachers(req.user.tenantId); + + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + res.setHeader( + 'Content-Disposition', + `attachment; filename=teachers_${Date.now()}.xlsx`, + ); + res.send(buffer); + } + + @Get('students') + async exportStudents(@Request() req: any, @Res() res: Response) { + const buffer = await this.exportService.exportStudents(req.user.tenantId); + + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + res.setHeader( + 'Content-Disposition', + `attachment; filename=students_${Date.now()}.xlsx`, + ); + res.send(buffer); + } + + @Get('lessons') + async exportLessons(@Request() req: any, @Res() res: Response) { + const buffer = await this.exportService.exportLessons(req.user.tenantId); + + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + res.setHeader( + 'Content-Disposition', + `attachment; filename=lessons_${Date.now()}.xlsx`, + ); + res.send(buffer); + } + + @Get('growth-records') + async exportGrowthRecords( + @Request() req: any, + @Query('studentId') studentId: string, + @Res() res: Response, + ) { + const buffer = await this.exportService.exportGrowthRecords( + req.user.tenantId, + studentId ? +studentId : undefined, + ); + + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + res.setHeader( + 'Content-Disposition', + `attachment; filename=growth_records_${Date.now()}.xlsx`, + ); + res.send(buffer); + } +} diff --git a/reading-platform-backend/src/modules/export/export.module.ts b/reading-platform-backend/src/modules/export/export.module.ts new file mode 100644 index 0000000..6b1e87d --- /dev/null +++ b/reading-platform-backend/src/modules/export/export.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ExportController } from './export.controller'; +import { ExportService } from './export.service'; + +@Module({ + controllers: [ExportController], + providers: [ExportService], + exports: [ExportService], +}) +export class ExportModule {} diff --git a/reading-platform-backend/src/modules/export/export.service.ts b/reading-platform-backend/src/modules/export/export.service.ts new file mode 100644 index 0000000..d9b927e --- /dev/null +++ b/reading-platform-backend/src/modules/export/export.service.ts @@ -0,0 +1,266 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import * as ExcelJS from 'exceljs'; + +@Injectable() +export class ExportService { + private readonly logger = new Logger(ExportService.name); + + constructor(private prisma: PrismaService) {} + + // ==================== 导出教师列表 ==================== + + async exportTeachers(tenantId: number): Promise { + const teachers = await this.prisma.teacher.findMany({ + where: { tenantId }, + include: { + classes: { + select: { name: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('教师列表'); + + // 设置表头 + worksheet.columns = [ + { header: 'ID', key: 'id', width: 8 }, + { header: '姓名', key: 'name', width: 15 }, + { header: '手机号', key: 'phone', width: 15 }, + { header: '邮箱', key: 'email', width: 25 }, + { header: '登录账号', key: 'loginAccount', width: 15 }, + { header: '负责班级', key: 'classes', width: 20 }, + { header: '授课次数', key: 'lessonCount', width: 10 }, + { header: '状态', key: 'status', width: 10 }, + { header: '创建时间', key: 'createdAt', width: 20 }, + ]; + + // 设置表头样式 + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' }, + }; + + // 添加数据 + teachers.forEach((teacher) => { + worksheet.addRow({ + id: teacher.id, + name: teacher.name, + phone: teacher.phone, + email: teacher.email || '-', + loginAccount: teacher.loginAccount, + classes: teacher.classes.map((c) => c.name).join('、') || '-', + lessonCount: teacher.lessonCount, + status: teacher.status === 'ACTIVE' ? '正常' : '停用', + createdAt: teacher.createdAt.toLocaleDateString('zh-CN'), + }); + }); + + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); + } + + // ==================== 导出学生列表 ==================== + + async exportStudents(tenantId: number): Promise { + const students = await this.prisma.student.findMany({ + where: { tenantId }, + include: { + class: { + select: { name: true }, + }, + }, + orderBy: [{ classId: 'asc' }, { createdAt: 'asc' }], + }); + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('学生列表'); + + // 设置表头 + worksheet.columns = [ + { header: 'ID', key: 'id', width: 8 }, + { header: '姓名', key: 'name', width: 15 }, + { header: '性别', key: 'gender', width: 8 }, + { header: '班级', key: 'className', width: 15 }, + { header: '生日', key: 'birthDate', width: 15 }, + { header: '家长姓名', key: 'parentName', width: 15 }, + { header: '家长电话', key: 'parentPhone', width: 15 }, + { header: '阅读次数', key: 'readingCount', width: 10 }, + { header: '创建时间', key: 'createdAt', width: 20 }, + ]; + + // 设置表头样式 + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' }, + }; + + // 添加数据 + students.forEach((student) => { + worksheet.addRow({ + id: student.id, + name: student.name, + gender: student.gender || '-', + className: student.class?.name || '-', + birthDate: student.birthDate + ? new Date(student.birthDate).toLocaleDateString('zh-CN') + : '-', + parentName: student.parentName || '-', + parentPhone: student.parentPhone || '-', + readingCount: student.readingCount, + createdAt: student.createdAt.toLocaleDateString('zh-CN'), + }); + }); + + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); + } + + // ==================== 导出授课记录 ==================== + + async exportLessons(tenantId: number): Promise { + const lessons = await this.prisma.lesson.findMany({ + where: { tenantId }, + include: { + course: { + select: { name: true, pictureBookName: true }, + }, + class: { + select: { name: true }, + }, + teacher: { + select: { name: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('授课记录'); + + // 设置表头 + worksheet.columns = [ + { header: 'ID', key: 'id', width: 8 }, + { header: '课程名称', key: 'courseName', width: 25 }, + { header: '绘本名称', key: 'pictureBookName', width: 20 }, + { header: '授课班级', key: 'className', width: 15 }, + { header: '授课教师', key: 'teacherName', width: 12 }, + { header: '计划时间', key: 'plannedDatetime', width: 18 }, + { header: '开始时间', key: 'startDatetime', width: 18 }, + { header: '结束时间', key: 'endDatetime', width: 18 }, + { header: '实际时长(分钟)', key: 'actualDuration', width: 12 }, + { header: '状态', key: 'status', width: 10 }, + ]; + + // 设置表头样式 + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' }, + }; + + // 状态映射 + const statusMap: Record = { + PLANNED: '已计划', + IN_PROGRESS: '进行中', + COMPLETED: '已完成', + CANCELLED: '已取消', + }; + + // 添加数据 + lessons.forEach((lesson) => { + worksheet.addRow({ + id: lesson.id, + courseName: lesson.course?.name || '-', + pictureBookName: lesson.course?.pictureBookName || '-', + className: lesson.class?.name || '-', + teacherName: lesson.teacher?.name || '-', + plannedDatetime: lesson.plannedDatetime + ? new Date(lesson.plannedDatetime).toLocaleString('zh-CN') + : '-', + startDatetime: lesson.startDatetime + ? new Date(lesson.startDatetime).toLocaleString('zh-CN') + : '-', + endDatetime: lesson.endDatetime + ? new Date(lesson.endDatetime).toLocaleString('zh-CN') + : '-', + actualDuration: lesson.actualDuration || '-', + status: statusMap[lesson.status] || lesson.status, + }); + }); + + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); + } + + // ==================== 导出成长档案(简单文本格式) ==================== + + async exportGrowthRecords(tenantId: number, studentId?: number): Promise { + const where: any = { tenantId }; + if (studentId) { + where.studentId = studentId; + } + + const records = await this.prisma.growthRecord.findMany({ + where, + include: { + student: { + select: { name: true }, + }, + class: { + select: { name: true }, + }, + }, + orderBy: { recordDate: 'desc' }, + }); + + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('成长档案'); + + // 设置表头 + worksheet.columns = [ + { header: 'ID', key: 'id', width: 8 }, + { header: '学生姓名', key: 'studentName', width: 15 }, + { header: '班级', key: 'className', width: 15 }, + { header: '标题', key: 'title', width: 25 }, + { header: '内容', key: 'content', width: 50 }, + { header: '记录类型', key: 'recordType', width: 12 }, + { header: '记录日期', key: 'recordDate', width: 15 }, + { header: '创建时间', key: 'createdAt', width: 20 }, + ]; + + // 设置表头样式 + worksheet.getRow(1).font = { bold: true }; + worksheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' }, + }; + + // 添加数据 + records.forEach((record) => { + worksheet.addRow({ + id: record.id, + studentName: record.student?.name || '-', + className: record.class?.name || '-', + title: record.title, + content: record.content || '-', + recordType: record.recordType, + recordDate: record.recordDate + ? new Date(record.recordDate).toLocaleDateString('zh-CN') + : '-', + createdAt: record.createdAt.toLocaleDateString('zh-CN'), + }); + }); + + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); + } +} diff --git a/reading-platform-backend/src/modules/file-upload/file-upload.controller.ts b/reading-platform-backend/src/modules/file-upload/file-upload.controller.ts new file mode 100644 index 0000000..04dbf31 --- /dev/null +++ b/reading-platform-backend/src/modules/file-upload/file-upload.controller.ts @@ -0,0 +1,92 @@ +import { + Controller, + Post, + Delete, + UseInterceptors, + UploadedFile, + Body, + BadRequestException, + Logger, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { memoryStorage } from 'multer'; +import { FileUploadService } from './file-upload.service'; + +@Controller('files') +export class FileUploadController { + private readonly logger = new Logger(FileUploadController.name); + + constructor(private readonly fileUploadService: FileUploadService) {} + + /** + * 上传单个文件 + * POST /api/v1/files/upload + */ + @Post('upload') + @UseInterceptors( + FileInterceptor('file', { + storage: memoryStorage(), + limits: { + fileSize: 300 * 1024 * 1024, // 300MB + }, + }), + ) + async uploadFile( + @UploadedFile() file: Express.Multer.File, + @Body() body: { type?: string; courseId?: string }, + ) { + this.logger.log(`Uploading file: ${file.originalname}, type: ${body.type || 'unknown'}`); + + if (!file) { + throw new BadRequestException('没有上传文件'); + } + + // 验证文件类型 + const fileType = body.type || 'other'; + const validationResult = this.fileUploadService.validateFile(file, fileType); + + if (!validationResult.valid) { + throw new BadRequestException(validationResult.error); + } + + // 保存文件 + const savedFile = await this.fileUploadService.saveFile(file, fileType, body.courseId); + + this.logger.log(`File uploaded successfully: ${savedFile.filePath}`); + + return { + success: true, + filePath: savedFile.filePath, + fileName: savedFile.fileName, + originalName: file.originalname, + fileSize: file.size, + mimeType: file.mimetype, + }; + } + + /** + * 删除文件 + * DELETE /api/v1/files/delete + */ + @Delete('delete') + async deleteFile(@Body() body: { filePath: string }) { + this.logger.log(`Deleting file: ${body.filePath}`); + + if (!body.filePath) { + throw new BadRequestException('缺少文件路径'); + } + + const result = await this.fileUploadService.deleteFile(body.filePath); + + if (!result.success) { + throw new BadRequestException(result.error); + } + + this.logger.log(`File deleted successfully: ${body.filePath}`); + + return { + success: true, + message: '文件删除成功', + }; + } +} diff --git a/reading-platform-backend/src/modules/file-upload/file-upload.module.ts b/reading-platform-backend/src/modules/file-upload/file-upload.module.ts new file mode 100644 index 0000000..cfae030 --- /dev/null +++ b/reading-platform-backend/src/modules/file-upload/file-upload.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { FileUploadController } from './file-upload.controller'; +import { FileUploadService } from './file-upload.service'; + +@Module({ + controllers: [FileUploadController], + providers: [FileUploadService], + exports: [FileUploadService], +}) +export class FileUploadModule {} diff --git a/reading-platform-backend/src/modules/file-upload/file-upload.service.ts b/reading-platform-backend/src/modules/file-upload/file-upload.service.ts new file mode 100644 index 0000000..412007f --- /dev/null +++ b/reading-platform-backend/src/modules/file-upload/file-upload.service.ts @@ -0,0 +1,203 @@ +import { Injectable, Logger, BadRequestException } from '@nestjs/common'; +import { extname, join, basename } from 'path'; +import { promises as fs } from 'fs'; + +// 文件类型配置 +const FILE_TYPE_CONFIG = { + cover: { + allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'], + maxSize: 10 * 1024 * 1024, // 10MB + folder: 'covers', + }, + ebook: { + allowedMimeTypes: [ + 'application/pdf', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + ], + maxSize: 300 * 1024 * 1024, // 300MB + folder: 'ebooks', + }, + audio: { + allowedMimeTypes: ['audio/mpeg', 'audio/wav', 'audio/mp4', 'audio/m4a', 'audio/x-m4a', 'audio/ogg'], + maxSize: 300 * 1024 * 1024, // 300MB + folder: 'audio', + }, + video: { + allowedMimeTypes: ['video/mp4', 'video/webm', 'video/quicktime', 'video/x-msvideo'], + maxSize: 300 * 1024 * 1024, // 300MB + folder: 'videos', + }, + ppt: { + allowedMimeTypes: [ + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/pdf', // 也允许PDF格式 + ], + maxSize: 300 * 1024 * 1024, // 300MB + folder: join('materials', 'ppt'), + }, + poster: { + allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'], + maxSize: 10 * 1024 * 1024, // 10MB + folder: join('materials', 'posters'), + }, + document: { + allowedMimeTypes: [ + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ], + maxSize: 300 * 1024 * 1024, // 300MB + folder: 'documents', + }, + other: { + allowedMimeTypes: [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + ], + maxSize: 300 * 1024 * 1024, // 300MB + folder: 'other', + }, +}; + +@Injectable() +export class FileUploadService { + private readonly logger = new Logger(FileUploadService.name); + private readonly uploadBasePath = join(process.cwd(), 'uploads', 'courses'); + + constructor() { + this.ensureDirectoriesExist(); + } + + /** + * 确保所有必要的目录存在 + */ + private async ensureDirectoriesExist() { + const directories = Object.values(FILE_TYPE_CONFIG).map((config) => + join(this.uploadBasePath, config.folder), + ); + + for (const dir of directories) { + try { + await fs.mkdir(dir, { recursive: true }); + this.logger.log(`Ensured directory exists: ${dir}`); + } catch (error) { + this.logger.error(`Failed to create directory ${dir}:`, error); + } + } + } + + /** + * 验证文件 + */ + validateFile( + file: Express.Multer.File, + type: string, + ): { valid: boolean; error?: string } { + const config = FILE_TYPE_CONFIG[type as keyof typeof FILE_TYPE_CONFIG] || FILE_TYPE_CONFIG.other; + + // 检查文件大小 + if (file.size > config.maxSize) { + const maxSizeMB = (config.maxSize / (1024 * 1024)).toFixed(0); + return { + valid: false, + error: `文件大小超过限制,最大允许 ${maxSizeMB}MB`, + }; + } + + // 检查 MIME 类型 + if (!config.allowedMimeTypes.includes(file.mimetype)) { + return { + valid: false, + error: `不支持的文件类型: ${file.mimetype}`, + }; + } + + return { valid: true }; + } + + /** + * 保存文件 + */ + async saveFile( + file: Express.Multer.File, + type: string, + courseId?: string, + ): Promise<{ filePath: string; fileName: string }> { + const config = FILE_TYPE_CONFIG[type as keyof typeof FILE_TYPE_CONFIG] || FILE_TYPE_CONFIG.other; + + // 生成安全的文件名(避免中文和特殊字符编码问题) + const timestamp = Date.now(); + const randomStr = Math.random().toString(36).substring(2, 8); + const originalExt = extname(file.originalname) || ''; + const courseIdPrefix = courseId ? `${courseId}_` : ''; + const newFileName = `${courseIdPrefix}${timestamp}_${randomStr}${originalExt}`; + + // 目标路径 + const targetDir = join(this.uploadBasePath, config.folder); + const targetPath = join(targetDir, newFileName); + + try { + // 写入文件 + await fs.writeFile(targetPath, file.buffer); + + // 返回相对路径(用于 API 响应和数据库存储) + const relativePath = `/uploads/courses/${config.folder}/${newFileName}`; + + this.logger.log(`File saved: ${targetPath}`); + + return { + filePath: relativePath, + fileName: newFileName, + }; + } catch (error) { + this.logger.error('Failed to save file:', error); + throw new BadRequestException('文件保存失败'); + } + } + + /** + * 删除文件 + */ + async deleteFile(filePath: string): Promise<{ success: boolean; error?: string }> { + try { + // 安全检查:确保路径在 uploads 目录内,防止目录遍历攻击 + if (!filePath.startsWith('/uploads/')) { + return { success: false, error: '非法的文件路径' }; + } + + // 防止路径遍历攻击 + const normalizedPath = filePath.replace(/\.\./g, ''); + const fullPath = join(process.cwd(), normalizedPath); + + // 确保最终路径仍在 uploads 目录内 + if (!fullPath.startsWith(join(process.cwd(), 'uploads'))) { + return { success: false, error: '非法的文件路径' }; + } + + // 检查文件是否存在 + try { + await fs.access(fullPath); + } catch { + this.logger.warn(`File not found: ${fullPath}`); + return { success: true, error: '文件不存在' }; // 文件不存在也返回成功 + } + + // 删除文件 + await fs.unlink(fullPath); + this.logger.log(`File deleted: ${fullPath}`); + + return { success: true }; + } catch (error) { + this.logger.error('Failed to delete file:', error); + return { success: false, error: '文件删除失败' }; + } + } +} diff --git a/reading-platform-backend/src/modules/growth/dto/create-growth.dto.ts b/reading-platform-backend/src/modules/growth/dto/create-growth.dto.ts new file mode 100644 index 0000000..f5e0261 --- /dev/null +++ b/reading-platform-backend/src/modules/growth/dto/create-growth.dto.ts @@ -0,0 +1,81 @@ +import { IsString, IsNotEmpty, IsOptional, IsInt, IsEnum, IsDateString, IsArray } from 'class-validator'; + +export enum RecordType { + STUDENT = 'STUDENT', + CLASS = 'CLASS', +} + +export class CreateGrowthRecordDto { + @IsInt() + @IsNotEmpty({ message: '学生ID不能为空' }) + studentId: number; + + @IsOptional() + @IsInt() + classId?: number; + + @IsEnum(RecordType) + recordType: RecordType; + + @IsString() + @IsNotEmpty({ message: '标题不能为空' }) + title: string; + + @IsOptional() + @IsString() + content?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + images?: string[]; + + @IsDateString() + recordDate: string; +} + +export class UpdateGrowthRecordDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '标题不能为空' }) + title?: string; + + @IsOptional() + @IsString() + content?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + images?: string[]; + + @IsOptional() + @IsDateString() + recordDate?: string; +} + +export class QueryGrowthRecordDto { + @IsOptional() + @IsInt() + page?: number; + + @IsOptional() + @IsInt() + pageSize?: number; + + @IsOptional() + @IsInt() + studentId?: number; + + @IsOptional() + @IsInt() + classId?: number; + + @IsOptional() + @IsString() + recordType?: string; + + @IsOptional() + @IsString() + keyword?: string; +} diff --git a/reading-platform-backend/src/modules/growth/growth.controller.ts b/reading-platform-backend/src/modules/growth/growth.controller.ts new file mode 100644 index 0000000..e65a905 --- /dev/null +++ b/reading-platform-backend/src/modules/growth/growth.controller.ts @@ -0,0 +1,117 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { GrowthService } from './growth.service'; +import { CreateGrowthRecordDto, UpdateGrowthRecordDto } from './dto/create-growth.dto'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('school') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class GrowthController { + constructor(private readonly growthService: GrowthService) {} + + @Get('growth-records') + findAll(@Request() req: any, @Query() query: any) { + return this.growthService.findAll(req.user.tenantId, query); + } + + @Get('growth-records/:id') + findOne(@Request() req: any, @Param('id') id: string) { + return this.growthService.findOne(req.user.tenantId, +id); + } + + @Post('growth-records') + create(@Request() req: any, @Body() dto: CreateGrowthRecordDto) { + return this.growthService.create(req.user.tenantId, req.user.userId, dto); + } + + @Put('growth-records/:id') + update( + @Request() req: any, + @Param('id') id: string, + @Body() dto: UpdateGrowthRecordDto, + ) { + return this.growthService.update(req.user.tenantId, +id, dto); + } + + @Delete('growth-records/:id') + delete(@Request() req: any, @Param('id') id: string) { + return this.growthService.delete(req.user.tenantId, +id); + } + + @Get('students/:studentId/growth-records') + findByStudent( + @Request() req: any, + @Param('studentId') studentId: string, + @Query() query: any, + ) { + return this.growthService.findByStudent(req.user.tenantId, +studentId, query); + } + + @Get('classes/:classId/growth-records') + findByClass( + @Request() req: any, + @Param('classId') classId: string, + @Query() query: any, + ) { + return this.growthService.findByClass(req.user.tenantId, +classId, query); + } +} + +// 教师端的成长档案控制器 +@Controller('teacher') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class TeacherGrowthController { + constructor(private readonly growthService: GrowthService) {} + + @Get('growth-records') + findAll(@Request() req: any, @Query() query: any) { + return this.growthService.findAllForTeacher(req.user.tenantId, req.user.userId, query); + } + + @Get('growth-records/:id') + findOne(@Request() req: any, @Param('id') id: string) { + return this.growthService.findOneForTeacher(req.user.tenantId, req.user.userId, +id); + } + + @Post('growth-records') + create(@Request() req: any, @Body() dto: CreateGrowthRecordDto) { + return this.growthService.createForTeacher(req.user.tenantId, req.user.userId, dto); + } + + @Put('growth-records/:id') + update( + @Request() req: any, + @Param('id') id: string, + @Body() dto: UpdateGrowthRecordDto, + ) { + return this.growthService.updateForTeacher(req.user.tenantId, req.user.userId, +id, dto); + } + + @Delete('growth-records/:id') + delete(@Request() req: any, @Param('id') id: string) { + return this.growthService.deleteForTeacher(req.user.tenantId, req.user.userId, +id); + } + + @Get('classes/:classId/growth-records') + findByClass( + @Request() req: any, + @Param('classId') classId: string, + @Query() query: any, + ) { + return this.growthService.findByClass(req.user.tenantId, +classId, query); + } +} diff --git a/reading-platform-backend/src/modules/growth/growth.module.ts b/reading-platform-backend/src/modules/growth/growth.module.ts new file mode 100644 index 0000000..efb9dba --- /dev/null +++ b/reading-platform-backend/src/modules/growth/growth.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { GrowthController, TeacherGrowthController } from './growth.controller'; +import { GrowthService } from './growth.service'; + +@Module({ + controllers: [GrowthController, TeacherGrowthController], + providers: [GrowthService], + exports: [GrowthService], +}) +export class GrowthModule {} diff --git a/reading-platform-backend/src/modules/growth/growth.service.ts b/reading-platform-backend/src/modules/growth/growth.service.ts new file mode 100644 index 0000000..f1b4d4a --- /dev/null +++ b/reading-platform-backend/src/modules/growth/growth.service.ts @@ -0,0 +1,637 @@ +import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { CreateGrowthRecordDto, UpdateGrowthRecordDto } from './dto/create-growth.dto'; + +@Injectable() +export class GrowthService { + private readonly logger = new Logger(GrowthService.name); + + constructor(private prisma: PrismaService) {} + + private parseJsonArray(value: any): any[] { + if (!value) return []; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return []; + } + } + return Array.isArray(value) ? value : []; + } + + // ==================== 成长档案管理 ==================== + + async findAll(tenantId: number, query: any) { + const { page = 1, pageSize = 10, studentId, classId, recordType, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId: tenantId, + }; + + if (studentId) { + where.studentId = +studentId; + } + + if (classId) { + where.classId = +classId; + } + + if (recordType) { + where.recordType = recordType; + } + + if (keyword) { + where.OR = [ + { title: { contains: keyword } }, + { content: { contains: keyword } }, + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.growthRecord.findMany({ + where, + skip, + take, + orderBy: { recordDate: 'desc' }, + include: { + student: { + select: { + id: true, + name: true, + gender: true, + }, + }, + class: { + select: { + id: true, + name: true, + grade: true, + }, + }, + }, + }), + this.prisma.growthRecord.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + images: this.parseJsonArray(item.images), + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + /** + * 教师端查询成长档案(带数据隔离) + * 仅返回教师所教班级学生的档案 + */ + async findAllForTeacher(tenantId: number, teacherId: number, query: any) { + const { page = 1, pageSize = 10, studentId, classId, recordType, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 获取教师关联的所有班级ID + const classTeachers = await this.prisma.classTeacher.findMany({ + where: { teacherId }, + select: { classId: true }, + }); + + const classIds = classTeachers.map((ct) => ct.classId); + + if (classIds.length === 0) { + return { + items: [], + total: 0, + page: +page, + pageSize: +pageSize, + }; + } + + const where: any = { + tenantId: tenantId, + classId: { in: classIds }, // 数据隔离:只查询教师所教班级的档案 + }; + + if (studentId) { + where.studentId = +studentId; + } + + // 如果指定了 classId,需要验证是否在教师的班级列表中 + if (classId) { + if (!classIds.includes(+classId)) { + throw new ForbiddenException('您没有权限查看该班级的档案'); + } + where.classId = +classId; + } + + if (recordType) { + where.recordType = recordType; + } + + if (keyword) { + where.OR = [ + { title: { contains: keyword } }, + { content: { contains: keyword } }, + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.growthRecord.findMany({ + where, + skip, + take, + orderBy: { recordDate: 'desc' }, + include: { + student: { + select: { + id: true, + name: true, + gender: true, + }, + }, + class: { + select: { + id: true, + name: true, + grade: true, + }, + }, + }, + }), + this.prisma.growthRecord.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + images: this.parseJsonArray(item.images), + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + /** + * 教师端查询单个档案(带数据隔离) + */ + async findOneForTeacher(tenantId: number, teacherId: number, id: number) { + const record = await this.prisma.growthRecord.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + student: { + select: { + id: true, + name: true, + gender: true, + birthDate: true, + }, + }, + class: { + select: { + id: true, + name: true, + grade: true, + }, + }, + }, + }); + + if (!record) { + throw new NotFoundException('成长档案不存在'); + } + + // 验证教师是否有权限查看该班级的档案 + const classTeacher = await this.prisma.classTeacher.findFirst({ + where: { teacherId, classId: record.classId }, + }); + + if (!classTeacher) { + throw new ForbiddenException('您没有权限查看此档案'); + } + + return { + ...record, + images: this.parseJsonArray(record.images), + }; + } + + /** + * 教师端创建档案(带数据隔离验证) + */ + async createForTeacher(tenantId: number, teacherId: number, dto: CreateGrowthRecordDto) { + // 验证学生是否存在 + const student = await this.prisma.student.findFirst({ + where: { + id: dto.studentId, + tenantId: tenantId, + }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + // 验证教师是否有权限为该班级创建档案 + const classId = dto.classId || student.classId; + const classTeacher = await this.prisma.classTeacher.findFirst({ + where: { teacherId, classId }, + }); + + if (!classTeacher) { + throw new ForbiddenException('您没有权限为此班级创建档案'); + } + + const record = await this.prisma.growthRecord.create({ + data: { + tenantId: tenantId, + studentId: dto.studentId, + classId: classId, + recordType: dto.recordType, + title: dto.title, + content: dto.content, + images: JSON.stringify(dto.images || []), + recordDate: new Date(dto.recordDate), + createdBy: teacherId, + }, + include: { + student: { + select: { + id: true, + name: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Growth record created by teacher: ${record.id}`); + + return { + ...record, + images: this.parseJsonArray(record.images), + }; + } + + /** + * 教师端更新档案(带数据隔离验证) + */ + async updateForTeacher(tenantId: number, teacherId: number, id: number, dto: UpdateGrowthRecordDto) { + const existing = await this.prisma.growthRecord.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!existing) { + throw new NotFoundException('成长档案不存在'); + } + + // 验证教师是否有权限更新该班级的档案 + const classTeacher = await this.prisma.classTeacher.findFirst({ + where: { teacherId, classId: existing.classId }, + }); + + if (!classTeacher) { + throw new ForbiddenException('您没有权限更新此档案'); + } + + const record = await this.prisma.growthRecord.update({ + where: { id: id }, + data: { + title: dto.title, + content: dto.content, + images: dto.images ? JSON.stringify(dto.images) : undefined, + recordDate: dto.recordDate ? new Date(dto.recordDate) : undefined, + }, + include: { + student: { + select: { + id: true, + name: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Growth record updated by teacher: ${id}`); + + return { + ...record, + images: this.parseJsonArray(record.images), + }; + } + + /** + * 教师端删除档案(带数据隔离验证) + */ + async deleteForTeacher(tenantId: number, teacherId: number, id: number) { + const existing = await this.prisma.growthRecord.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!existing) { + throw new NotFoundException('成长档案不存在'); + } + + // 验证教师是否有权限删除该班级的档案 + const classTeacher = await this.prisma.classTeacher.findFirst({ + where: { teacherId, classId: existing.classId }, + }); + + if (!classTeacher) { + throw new ForbiddenException('您没有权限删除此档案'); + } + + await this.prisma.growthRecord.delete({ + where: { id: id }, + }); + + this.logger.log(`Growth record deleted by teacher: ${id}`); + + return { message: '删除成功' }; + } + + async findOne(tenantId: number, id: number) { + const record = await this.prisma.growthRecord.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + student: { + select: { + id: true, + name: true, + gender: true, + birthDate: true, + }, + }, + class: { + select: { + id: true, + name: true, + grade: true, + }, + }, + }, + }); + + if (!record) { + throw new NotFoundException('成长档案不存在'); + } + + return { + ...record, + images: this.parseJsonArray(record.images), + }; + } + + async create(tenantId: number, userId: number, dto: CreateGrowthRecordDto) { + // 验证学生是否存在 + const student = await this.prisma.student.findFirst({ + where: { + id: dto.studentId, + tenantId: tenantId, + }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + const record = await this.prisma.growthRecord.create({ + data: { + tenantId: tenantId, + studentId: dto.studentId, + classId: dto.classId || student.classId, + recordType: dto.recordType, + title: dto.title, + content: dto.content, + images: JSON.stringify(dto.images || []), + recordDate: new Date(dto.recordDate), + createdBy: userId, + }, + include: { + student: { + select: { + id: true, + name: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Growth record created: ${record.id}`); + + return { + ...record, + images: this.parseJsonArray(record.images), + }; + } + + async update(tenantId: number, id: number, dto: UpdateGrowthRecordDto) { + const existing = await this.prisma.growthRecord.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!existing) { + throw new NotFoundException('成长档案不存在'); + } + + const record = await this.prisma.growthRecord.update({ + where: { id: id }, + data: { + title: dto.title, + content: dto.content, + images: dto.images ? JSON.stringify(dto.images) : undefined, + recordDate: dto.recordDate ? new Date(dto.recordDate) : undefined, + }, + include: { + student: { + select: { + id: true, + name: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Growth record updated: ${id}`); + + return { + ...record, + images: this.parseJsonArray(record.images), + }; + } + + async delete(tenantId: number, id: number) { + const existing = await this.prisma.growthRecord.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!existing) { + throw new NotFoundException('成长档案不存在'); + } + + await this.prisma.growthRecord.delete({ + where: { id: id }, + }); + + this.logger.log(`Growth record deleted: ${id}`); + + return { message: '删除成功' }; + } + + // ==================== 学生档案列表 ==================== + + async findByStudent(tenantId: number, studentId: number, query: any) { + const { page = 1, pageSize = 10 } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 验证学生是否存在 + const student = await this.prisma.student.findFirst({ + where: { + id: studentId, + tenantId: tenantId, + }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + const [items, total] = await Promise.all([ + this.prisma.growthRecord.findMany({ + where: { + studentId: studentId, + tenantId: tenantId, + }, + skip, + take, + orderBy: { recordDate: 'desc' }, + }), + this.prisma.growthRecord.count({ + where: { + studentId: studentId, + tenantId: tenantId, + }, + }), + ]); + + return { + items: items.map((item) => ({ + ...item, + images: this.parseJsonArray(item.images), + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + // ==================== 班级档案列表 ==================== + + async findByClass(tenantId: number, classId: number, query: any) { + const { page = 1, pageSize = 10, recordDate } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 验证班级是否存在 + const classEntity = await this.prisma.class.findFirst({ + where: { + id: classId, + tenantId: tenantId, + }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + const where: any = { + classId: classId, + tenantId: tenantId, + recordType: 'CLASS', + }; + + if (recordDate) { + where.recordDate = new Date(recordDate); + } + + const [items, total] = await Promise.all([ + this.prisma.growthRecord.findMany({ + where, + skip, + take, + orderBy: { recordDate: 'desc' }, + include: { + student: { + select: { + id: true, + name: true, + }, + }, + }, + }), + this.prisma.growthRecord.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + images: this.parseJsonArray(item.images), + })), + total, + page: +page, + pageSize: +pageSize, + }; + } +} diff --git a/reading-platform-backend/src/modules/lesson/dto/create-lesson.dto.ts b/reading-platform-backend/src/modules/lesson/dto/create-lesson.dto.ts new file mode 100644 index 0000000..4b71fb6 --- /dev/null +++ b/reading-platform-backend/src/modules/lesson/dto/create-lesson.dto.ts @@ -0,0 +1,13 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; + +export class CreateLessonDto { + @IsNumber() + courseId: number; + + @IsNumber() + classId: number; + + @IsOptional() + @IsString() + plannedDatetime?: string; +} diff --git a/reading-platform-backend/src/modules/lesson/dto/finish-lesson.dto.ts b/reading-platform-backend/src/modules/lesson/dto/finish-lesson.dto.ts new file mode 100644 index 0000000..2ffedd4 --- /dev/null +++ b/reading-platform-backend/src/modules/lesson/dto/finish-lesson.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsNumber, IsOptional } from 'class-validator'; + +export class FinishLessonDto { + @IsOptional() + @IsString() + overallRating?: string; + + @IsOptional() + @IsString() + participationRating?: string; + + @IsOptional() + @IsString() + completionNote?: string; + + @IsOptional() + @IsNumber() + actualDuration?: number; +} diff --git a/reading-platform-backend/src/modules/lesson/lesson.controller.ts b/reading-platform-backend/src/modules/lesson/lesson.controller.ts new file mode 100644 index 0000000..01c7f14 --- /dev/null +++ b/reading-platform-backend/src/modules/lesson/lesson.controller.ts @@ -0,0 +1,133 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { LessonService } from './lesson.service'; +import { CreateLessonDto } from './dto/create-lesson.dto'; +import { FinishLessonDto } from './dto/finish-lesson.dto'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +// 教师端授课控制器 +@Controller('teacher/lessons') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class LessonController { + constructor(private readonly lessonService: LessonService) {} + + @Get() + findAll(@Request() req: any, @Query() query: any) { + return this.lessonService.findByTeacher(req.user.userId, query); + } + + @Get(':id') + findOne(@Request() req: any, @Param('id') id: string) { + return this.lessonService.findOne(+id, req.user.userId); + } + + @Post() + create(@Request() req: any, @Body() dto: CreateLessonDto) { + return this.lessonService.create(req.user.userId, req.user.tenantId, dto); + } + + @Post(':id/start') + start(@Request() req: any, @Param('id') id: string) { + return this.lessonService.start(+id, req.user.userId); + } + + @Post(':id/finish') + finish(@Request() req: any, @Param('id') id: string, @Body() dto: FinishLessonDto) { + return this.lessonService.finish(+id, req.user.userId, dto); + } + + @Post(':id/cancel') + cancel(@Request() req: any, @Param('id') id: string) { + return this.lessonService.cancel(+id, req.user.userId); + } + + @Post(':id/students/:studentId/record') + saveStudentRecord( + @Request() req: any, + @Param('id') id: string, + @Param('studentId') studentId: string, + @Body() data: any + ) { + return this.lessonService.saveStudentRecord(+id, req.user.userId, +studentId, data); + } + + @Get(':id/student-records') + getStudentRecords(@Request() req: any, @Param('id') id: string) { + return this.lessonService.getStudentRecords(+id, req.user.userId); + } + + @Post(':id/student-records/batch') + batchSaveStudentRecords( + @Request() req: any, + @Param('id') id: string, + @Body() data: { records: Array<{ studentId: number; focus?: number; participation?: number; interest?: number; understanding?: number; notes?: string }> } + ) { + return this.lessonService.batchSaveStudentRecords(+id, req.user.userId, data.records); + } + + // ==================== 课程反馈 ==================== + + @Post(':id/feedback') + submitFeedback( + @Request() req: any, + @Param('id') id: string, + @Body() data: any + ) { + return this.lessonService.submitFeedback(+id, req.user.userId, data); + } + + @Get(':id/feedback') + getFeedback(@Request() req: any, @Param('id') id: string) { + return this.lessonService.getFeedback(+id, req.user.userId); + } +} + +// 教师端反馈控制器 +@Controller('teacher/feedbacks') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class TeacherFeedbackController { + constructor(private readonly lessonService: LessonService) {} + + @Get() + findAll(@Request() req: any, @Query() query: any) { + return this.lessonService.getFeedbacksByTenant(req.user.tenantId, { + ...query, + teacherId: req.user.userId, + }); + } + + @Get('stats') + getStats(@Request() req: any) { + return this.lessonService.getTeacherFeedbackStats(req.user.userId); + } +} + +// 学校端反馈控制器 +@Controller('school/feedbacks') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class SchoolFeedbackController { + constructor(private readonly lessonService: LessonService) {} + + @Get() + findAll(@Request() req: any, @Query() query: any) { + return this.lessonService.getFeedbacksByTenant(req.user.tenantId, query); + } + + @Get('stats') + getStats(@Request() req: any) { + return this.lessonService.getFeedbackStats(req.user.tenantId); + } +} diff --git a/reading-platform-backend/src/modules/lesson/lesson.module.ts b/reading-platform-backend/src/modules/lesson/lesson.module.ts new file mode 100644 index 0000000..87914e3 --- /dev/null +++ b/reading-platform-backend/src/modules/lesson/lesson.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { LessonController, SchoolFeedbackController, TeacherFeedbackController } from './lesson.controller'; +import { LessonService } from './lesson.service'; + +@Module({ + controllers: [LessonController, SchoolFeedbackController, TeacherFeedbackController], + providers: [LessonService], + exports: [LessonService], +}) +export class LessonModule {} diff --git a/reading-platform-backend/src/modules/lesson/lesson.service.ts b/reading-platform-backend/src/modules/lesson/lesson.service.ts new file mode 100644 index 0000000..1317fd2 --- /dev/null +++ b/reading-platform-backend/src/modules/lesson/lesson.service.ts @@ -0,0 +1,905 @@ +import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { CreateLessonDto } from './dto/create-lesson.dto'; +import { FinishLessonDto } from './dto/finish-lesson.dto'; + +@Injectable() +export class LessonService { + private readonly logger = new Logger(LessonService.name); + + constructor(private prisma: PrismaService) {} + + async create(teacherId: number, tenantId: number, dto: CreateLessonDto) { + // 验证课程是否已授权 + const course = await this.prisma.course.findUnique({ + where: { id: dto.courseId }, + }); + + if (!course) { + throw new NotFoundException('课程不存在'); + } + + if (course.status !== 'PUBLISHED') { + throw new ForbiddenException('该课程未发布'); + } + + // 检查授权 + const tenantCourse = await this.prisma.tenantCourse.findUnique({ + where: { + tenantId_courseId: { + tenantId: tenantId, + courseId: dto.courseId, + }, + }, + }); + + if (!tenantCourse || !tenantCourse.authorized) { + throw new ForbiddenException('您的学校未获得此课程的授权'); + } + + // 验证班级是否属于该教师 + const classEntity = await this.prisma.class.findFirst({ + where: { + id: dto.classId, + tenantId: tenantId, + teacherId: teacherId, + }, + }); + + if (!classEntity) { + throw new ForbiddenException('无权操作此班级'); + } + + // 创建授课记录 + const lesson = await this.prisma.lesson.create({ + data: { + tenantId: tenantId, + teacherId: teacherId, + classId: dto.classId, + courseId: dto.courseId, + plannedDatetime: dto.plannedDatetime ? new Date(dto.plannedDatetime) : null, + status: 'PLANNED', + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + duration: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Lesson created: ${lesson.id} by teacher ${teacherId}`); + + return lesson; + } + + async start(lessonId: number, teacherId: number) { + // 查找授课记录 + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权操作此授课记录'); + } + + if (lesson.status !== 'PLANNED') { + throw new ForbiddenException('该授课记录已开始或已完成'); + } + + // 更新状态和开始时间 + const updatedLesson = await this.prisma.lesson.update({ + where: { id: lessonId }, + data: { + status: 'IN_PROGRESS', + startDatetime: new Date(), + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + duration: true, + pptPath: true, + pptName: true, + ebookPaths: true, + audioPaths: true, + videoPaths: true, + posterPaths: true, + scripts: { + orderBy: { sortOrder: 'asc' }, + include: { + pages: { + orderBy: { pageNumber: 'asc' }, + }, + }, + }, + activities: { + orderBy: { sortOrder: 'asc' }, + }, + }, + }, + class: { + select: { + id: true, + name: true, + students: { + select: { + id: true, + name: true, + gender: true, + }, + }, + }, + }, + }, + }); + + this.logger.log(`Lesson started: ${lessonId}`); + + // 解析 JSON 字段 + return { + ...updatedLesson, + course: { + ...updatedLesson.course, + ebookPaths: updatedLesson.course.ebookPaths ? JSON.parse(updatedLesson.course.ebookPaths) : [], + audioPaths: updatedLesson.course.audioPaths ? JSON.parse(updatedLesson.course.audioPaths) : [], + videoPaths: updatedLesson.course.videoPaths ? JSON.parse(updatedLesson.course.videoPaths) : [], + posterPaths: updatedLesson.course.posterPaths ? JSON.parse(updatedLesson.course.posterPaths) : [], + scripts: updatedLesson.course.scripts.map((script) => ({ + ...script, + interactionPoints: script.interactionPoints ? JSON.parse(script.interactionPoints) : null, + resourceIds: script.resourceIds ? JSON.parse(script.resourceIds) : null, + pages: script.pages?.map((page) => ({ + ...page, + resourceIds: page.resourceIds ? JSON.parse(page.resourceIds) : null, + })), + })), + activities: updatedLesson.course.activities.map((activity) => ({ + ...activity, + onlineMaterials: activity.onlineMaterials ? JSON.parse(activity.onlineMaterials) : null, + objectives: activity.objectives ? JSON.parse(activity.objectives) : null, + })), + }, + }; + } + + async finish(lessonId: number, teacherId: number, dto: FinishLessonDto) { + // 查找授课记录 + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权操作此授课记录'); + } + + if (lesson.status !== 'IN_PROGRESS') { + throw new ForbiddenException('该授课记录未开始或已完成'); + } + + // 计算实际时长 + let actualDuration = dto.actualDuration; + if (!actualDuration && lesson.startDatetime) { + const endTime = new Date(); + actualDuration = Math.round((endTime.getTime() - lesson.startDatetime.getTime()) / 60000); + } + + // 更新状态和结束时间 + const updatedLesson = await this.prisma.lesson.update({ + where: { id: lessonId }, + data: { + status: 'COMPLETED', + endDatetime: new Date(), + actualDuration: actualDuration, + overallRating: dto.overallRating, + participationRating: dto.participationRating, + completionNote: dto.completionNote, + }, + include: { + course: { + select: { + id: true, + name: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + // 更新课程使用统计 + await this.prisma.course.update({ + where: { id: lesson.courseId }, + data: { + usageCount: { increment: 1 }, + }, + }); + + // 更新教师授课次数 + await this.prisma.teacher.update({ + where: { id: teacherId }, + data: { + lessonCount: { increment: 1 }, + }, + }); + + this.logger.log(`Lesson finished: ${lessonId}, duration: ${actualDuration} minutes`); + + return updatedLesson; + } + + async cancel(lessonId: number, teacherId: number) { + // 查找授课记录 + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权操作此授课记录'); + } + + // 只有已计划状态的课程可以取消 + if (lesson.status !== 'PLANNED') { + throw new ForbiddenException('只有已计划的课程可以取消'); + } + + // 更新状态为已取消 + const updatedLesson = await this.prisma.lesson.update({ + where: { id: lessonId }, + data: { + status: 'CANCELLED', + }, + include: { + course: { + select: { + id: true, + name: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Lesson cancelled: ${lessonId}`); + + return updatedLesson; + } + + async findOne(lessonId: number, teacherId: number) { + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + duration: true, + pptPath: true, + pptName: true, + ebookPaths: true, + audioPaths: true, + videoPaths: true, + posterPaths: true, + scripts: { + orderBy: { sortOrder: 'asc' }, + include: { + pages: { + orderBy: { pageNumber: 'asc' }, + }, + }, + }, + activities: { + orderBy: { sortOrder: 'asc' }, + }, + }, + }, + class: { + select: { + id: true, + name: true, + students: { + select: { + id: true, + name: true, + gender: true, + }, + }, + }, + }, + }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权查看此授课记录'); + } + + // 解析 JSON 字段 + return { + ...lesson, + course: { + ...lesson.course, + ebookPaths: lesson.course.ebookPaths ? JSON.parse(lesson.course.ebookPaths) : [], + audioPaths: lesson.course.audioPaths ? JSON.parse(lesson.course.audioPaths) : [], + videoPaths: lesson.course.videoPaths ? JSON.parse(lesson.course.videoPaths) : [], + posterPaths: lesson.course.posterPaths ? JSON.parse(lesson.course.posterPaths) : [], + scripts: lesson.course.scripts.map((script) => ({ + ...script, + interactionPoints: script.interactionPoints ? JSON.parse(script.interactionPoints) : null, + resourceIds: script.resourceIds ? JSON.parse(script.resourceIds) : null, + pages: script.pages?.map((page) => ({ + ...page, + resourceIds: page.resourceIds ? JSON.parse(page.resourceIds) : null, + })), + })), + activities: lesson.course.activities.map((activity) => ({ + ...activity, + onlineMaterials: activity.onlineMaterials ? JSON.parse(activity.onlineMaterials) : null, + objectives: activity.objectives ? JSON.parse(activity.objectives) : null, + })), + }, + }; + } + + async findByTeacher(teacherId: number, query: any) { + const { page = 1, pageSize = 10, status, courseId } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + teacherId: teacherId, + }; + + if (status) { + where.status = status; + } + + if (courseId) { + where.courseId = +courseId; + } + + const [items, total] = await Promise.all([ + this.prisma.lesson.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }), + this.prisma.lesson.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async saveStudentRecord( + lessonId: number, + teacherId: number, + studentId: number, + data: { + focus?: number; + participation?: number; + interest?: number; + understanding?: number; + notes?: string; + } + ) { + // 验证授课记录 + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权操作此授课记录'); + } + + // 验证学生是否在班级中 + const student = await this.prisma.student.findFirst({ + where: { + id: studentId, + classId: lesson.classId, + }, + }); + + if (!student) { + throw new NotFoundException('学生不存在或不在此班级'); + } + + // 创建或更新学生记录 + const record = await this.prisma.studentRecord.upsert({ + where: { + lessonId_studentId: { + lessonId: lessonId, + studentId: studentId, + }, + }, + update: data, + create: { + lessonId: lessonId, + studentId: studentId, + ...data, + }, + }); + + return record; + } + + async getStudentRecords(lessonId: number, teacherId: number) { + // 验证授课记录 + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + include: { + class: { + select: { + id: true, + name: true, + students: { + select: { + id: true, + name: true, + gender: true, + }, + }, + }, + }, + }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权操作此授课记录'); + } + + // 获取所有学生记录 + const records = await this.prisma.studentRecord.findMany({ + where: { lessonId }, + include: { + student: { + select: { + id: true, + name: true, + gender: true, + }, + }, + }, + }); + + // 合并学生信息和记录 + const studentRecords = lesson.class.students.map((student) => { + const record = records.find((r) => r.studentId === student.id); + return { + ...student, + record: record || null, + }; + }); + + return { + lesson: { + id: lesson.id, + status: lesson.status, + className: lesson.class.name, + }, + students: studentRecords, + }; + } + + async batchSaveStudentRecords( + lessonId: number, + teacherId: number, + records: Array<{ + studentId: number; + focus?: number; + participation?: number; + interest?: number; + understanding?: number; + notes?: string; + }> + ) { + // 验证授课记录 + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权操作此授课记录'); + } + + // 批量保存记录 + const results = []; + for (const record of records) { + const saved = await this.prisma.studentRecord.upsert({ + where: { + lessonId_studentId: { + lessonId: lessonId, + studentId: record.studentId, + }, + }, + update: { + focus: record.focus, + participation: record.participation, + interest: record.interest, + understanding: record.understanding, + notes: record.notes, + }, + create: { + lessonId: lessonId, + studentId: record.studentId, + focus: record.focus, + participation: record.participation, + interest: record.interest, + understanding: record.understanding, + notes: record.notes, + }, + }); + results.push(saved); + + // 更新学生的阅读次数(仅首次记录时) + const existingRecord = await this.prisma.studentRecord.findFirst({ + where: { + studentId: record.studentId, + lessonId: { not: lessonId }, + }, + }); + + if (!existingRecord) { + // 这是该学生第一次有记录,更新阅读次数 + await this.prisma.student.update({ + where: { id: record.studentId }, + data: { readingCount: { increment: 1 } }, + }); + } + } + + this.logger.log(`Batch saved ${results.length} student records for lesson ${lessonId}`); + + return { count: results.length, records: results }; + } + + // ==================== 课程反馈功能 ==================== + + async submitFeedback( + lessonId: number, + teacherId: number, + data: { + designQuality?: number; + participation?: number; + goalAchievement?: number; + stepFeedbacks?: any; + pros?: string; + suggestions?: string; + activitiesDone?: any; + } + ) { + // 验证授课记录 + const lesson = await this.prisma.lesson.findUnique({ + where: { id: lessonId }, + }); + + if (!lesson) { + throw new NotFoundException('授课记录不存在'); + } + + if (lesson.teacherId !== teacherId) { + throw new ForbiddenException('无权操作此授课记录'); + } + + // 创建或更新反馈 + const feedback = await this.prisma.lessonFeedback.upsert({ + where: { + lessonId_teacherId: { + lessonId: lessonId, + teacherId: teacherId, + }, + }, + update: { + designQuality: data.designQuality, + participation: data.participation, + goalAchievement: data.goalAchievement, + stepFeedbacks: data.stepFeedbacks ? JSON.stringify(data.stepFeedbacks) : null, + pros: data.pros, + suggestions: data.suggestions, + activitiesDone: data.activitiesDone ? JSON.stringify(data.activitiesDone) : null, + }, + create: { + lessonId: lessonId, + teacherId: teacherId, + designQuality: data.designQuality, + participation: data.participation, + goalAchievement: data.goalAchievement, + stepFeedbacks: data.stepFeedbacks ? JSON.stringify(data.stepFeedbacks) : null, + pros: data.pros, + suggestions: data.suggestions, + activitiesDone: data.activitiesDone ? JSON.stringify(data.activitiesDone) : null, + }, + include: { + lesson: { + select: { + id: true, + course: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }); + + // 更新教师反馈次数 + await this.prisma.teacher.update({ + where: { id: teacherId }, + data: { + feedbackCount: { increment: 1 }, + }, + }); + + this.logger.log(`Feedback submitted for lesson: ${lessonId}`); + + return { + ...feedback, + stepFeedbacks: feedback.stepFeedbacks ? JSON.parse(feedback.stepFeedbacks as string) : null, + activitiesDone: feedback.activitiesDone ? JSON.parse(feedback.activitiesDone as string) : null, + }; + } + + async getFeedback(lessonId: number, teacherId: number) { + const feedback = await this.prisma.lessonFeedback.findUnique({ + where: { + lessonId_teacherId: { + lessonId: lessonId, + teacherId: teacherId, + }, + }, + include: { + lesson: { + select: { + id: true, + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }); + + if (!feedback) { + return null; + } + + return { + ...feedback, + stepFeedbacks: feedback.stepFeedbacks ? JSON.parse(feedback.stepFeedbacks as string) : null, + activitiesDone: feedback.activitiesDone ? JSON.parse(feedback.activitiesDone as string) : null, + }; + } + + async getFeedbacksByTenant(tenantId: number, query: any) { + const { page = 1, pageSize = 10, teacherId, courseId } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + lesson: { + tenantId: tenantId, + }, + }; + + if (teacherId) { + where.teacherId = +teacherId; + } + + if (courseId) { + where.lesson = { + ...where.lesson, + courseId: +courseId, + }; + } + + const [items, total] = await Promise.all([ + this.prisma.lessonFeedback.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + lesson: { + select: { + id: true, + startDatetime: true, + actualDuration: true, + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }, + teacher: { + select: { + id: true, + name: true, + }, + }, + }, + }), + this.prisma.lessonFeedback.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + stepFeedbacks: item.stepFeedbacks ? JSON.parse(item.stepFeedbacks as string) : null, + activitiesDone: item.activitiesDone ? JSON.parse(item.activitiesDone as string) : null, + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + async getFeedbackStats(tenantId: number) { + // 获取反馈统计 + const feedbacks = await this.prisma.lessonFeedback.findMany({ + where: { + lesson: { + tenantId: tenantId, + }, + }, + select: { + designQuality: true, + participation: true, + goalAchievement: true, + lesson: { + select: { + courseId: true, + }, + }, + }, + }); + + const totalFeedbacks = feedbacks.length; + const avgDesignQuality = feedbacks.reduce((sum, f) => sum + (f.designQuality || 0), 0) / (totalFeedbacks || 1); + const avgParticipation = feedbacks.reduce((sum, f) => sum + (f.participation || 0), 0) / (totalFeedbacks || 1); + const avgGoalAchievement = feedbacks.reduce((sum, f) => sum + (f.goalAchievement || 0), 0) / (totalFeedbacks || 1); + + // 按课程统计 + const courseStats: Record = {}; + feedbacks.forEach((f) => { + const courseId = f.lesson.courseId; + if (!courseStats[courseId]) { + courseStats[courseId] = { count: 0, avgRating: 0 }; + } + courseStats[courseId].count++; + courseStats[courseId].avgRating += (f.designQuality || 0 + f.participation || 0 + f.goalAchievement || 0) / 3; + }); + + // 计算平均值 + Object.keys(courseStats).forEach((courseId) => { + courseStats[+courseId].avgRating /= courseStats[+courseId].count; + }); + + return { + totalFeedbacks, + avgDesignQuality: Math.round(avgDesignQuality * 10) / 10, + avgParticipation: Math.round(avgParticipation * 10) / 10, + avgGoalAchievement: Math.round(avgGoalAchievement * 10) / 10, + courseStats, + }; + } + + async getTeacherFeedbackStats(teacherId: number) { + const feedbacks = await this.prisma.lessonFeedback.findMany({ + where: { teacherId }, + select: { + designQuality: true, + participation: true, + goalAchievement: true, + lesson: { + select: { + courseId: true, + }, + }, + }, + }); + + const totalFeedbacks = feedbacks.length; + const avgDesignQuality = feedbacks.reduce((sum, f) => sum + (f.designQuality || 0), 0) / (totalFeedbacks || 1); + const avgParticipation = feedbacks.reduce((sum, f) => sum + (f.participation || 0), 0) / (totalFeedbacks || 1); + const avgGoalAchievement = feedbacks.reduce((sum, f) => sum + (f.goalAchievement || 0), 0) / (totalFeedbacks || 1); + + return { + totalFeedbacks, + avgDesignQuality: Math.round(avgDesignQuality * 10) / 10, + avgParticipation: Math.round(avgParticipation * 10) / 10, + avgGoalAchievement: Math.round(avgGoalAchievement * 10) / 10, + }; + } +} diff --git a/reading-platform-backend/src/modules/notification/notification.controller.ts b/reading-platform-backend/src/modules/notification/notification.controller.ts new file mode 100644 index 0000000..2b96828 --- /dev/null +++ b/reading-platform-backend/src/modules/notification/notification.controller.ts @@ -0,0 +1,151 @@ +import { + Controller, + Get, + Put, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { NotificationService } from './notification.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +// 学校端通知控制器 +@Controller('school/notifications') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class SchoolNotificationController { + constructor(private readonly notificationService: NotificationService) {} + + @Get() + getNotifications(@Request() req: any, @Query() query: any) { + return this.notificationService.getNotifications( + req.user.tenantId, + 'SCHOOL', + req.user.userId, + query + ); + } + + @Get('unread-count') + getUnreadCount(@Request() req: any) { + return this.notificationService.getUnreadCount( + req.user.tenantId, + 'SCHOOL', + req.user.userId + ); + } + + @Put(':id/read') + markAsRead(@Request() req: any, @Param('id') id: string) { + return this.notificationService.markAsRead( + req.user.tenantId, + +id, + 'SCHOOL', + req.user.userId + ); + } + + @Put('read-all') + markAllAsRead(@Request() req: any) { + return this.notificationService.markAllAsRead( + req.user.tenantId, + 'SCHOOL', + req.user.userId + ); + } +} + +// 教师端通知控制器 +@Controller('teacher/notifications') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class TeacherNotificationController { + constructor(private readonly notificationService: NotificationService) {} + + @Get() + getNotifications(@Request() req: any, @Query() query: any) { + return this.notificationService.getNotifications( + req.user.tenantId, + 'TEACHER', + req.user.userId, + query + ); + } + + @Get('unread-count') + getUnreadCount(@Request() req: any) { + return this.notificationService.getUnreadCount( + req.user.tenantId, + 'TEACHER', + req.user.userId + ); + } + + @Put(':id/read') + markAsRead(@Request() req: any, @Param('id') id: string) { + return this.notificationService.markAsRead( + req.user.tenantId, + +id, + 'TEACHER', + req.user.userId + ); + } + + @Put('read-all') + markAllAsRead(@Request() req: any) { + return this.notificationService.markAllAsRead( + req.user.tenantId, + 'TEACHER', + req.user.userId + ); + } +} + +// 家长端通知控制器 +@Controller('parent/notifications') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('parent') +export class ParentNotificationController { + constructor(private readonly notificationService: NotificationService) {} + + @Get() + getNotifications(@Request() req: any, @Query() query: any) { + return this.notificationService.getNotifications( + req.user.tenantId, + 'PARENT', + req.user.userId, + query + ); + } + + @Get('unread-count') + getUnreadCount(@Request() req: any) { + return this.notificationService.getUnreadCount( + req.user.tenantId, + 'PARENT', + req.user.userId + ); + } + + @Put(':id/read') + markAsRead(@Request() req: any, @Param('id') id: string) { + return this.notificationService.markAsRead( + req.user.tenantId, + +id, + 'PARENT', + req.user.userId + ); + } + + @Put('read-all') + markAllAsRead(@Request() req: any) { + return this.notificationService.markAllAsRead( + req.user.tenantId, + 'PARENT', + req.user.userId + ); + } +} diff --git a/reading-platform-backend/src/modules/notification/notification.module.ts b/reading-platform-backend/src/modules/notification/notification.module.ts new file mode 100644 index 0000000..06189b2 --- /dev/null +++ b/reading-platform-backend/src/modules/notification/notification.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { + SchoolNotificationController, + TeacherNotificationController, + ParentNotificationController, +} from './notification.controller'; +import { NotificationService } from './notification.service'; +import { ScheduleNotificationService } from './schedule-notification.service'; + +@Module({ + imports: [ScheduleModule.forRoot()], + controllers: [ + SchoolNotificationController, + TeacherNotificationController, + ParentNotificationController, + ], + providers: [NotificationService, ScheduleNotificationService], + exports: [NotificationService, ScheduleNotificationService], +}) +export class NotificationModule {} diff --git a/reading-platform-backend/src/modules/notification/notification.service.ts b/reading-platform-backend/src/modules/notification/notification.service.ts new file mode 100644 index 0000000..005da85 --- /dev/null +++ b/reading-platform-backend/src/modules/notification/notification.service.ts @@ -0,0 +1,169 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class NotificationService { + private readonly logger = new Logger(NotificationService.name); + + constructor(private prisma: PrismaService) {} + + // ==================== 创建通知 ==================== + + async createNotification(data: { + tenantId: number; + recipientType: 'TEACHER' | 'SCHOOL' | 'PARENT'; + recipientId: number; + title: string; + content: string; + notificationType: 'SYSTEM' | 'TASK' | 'LESSON' | 'GROWTH'; + relatedType?: string; + relatedId?: number; + }) { + const notification = await this.prisma.notification.create({ + data: { + tenantId: data.tenantId, + recipientType: data.recipientType, + recipientId: data.recipientId, + title: data.title, + content: data.content, + notificationType: data.notificationType, + relatedType: data.relatedType, + relatedId: data.relatedId, + }, + }); + + this.logger.log( + `Notification created: ${notification.id} for ${data.recipientType}:${data.recipientId}` + ); + + return notification; + } + + // 批量创建通知 + async createBatchNotifications( + notifications: Array<{ + tenantId: number; + recipientType: 'TEACHER' | 'SCHOOL' | 'PARENT'; + recipientId: number; + title: string; + content: string; + notificationType: 'SYSTEM' | 'TASK' | 'LESSON' | 'GROWTH'; + relatedType?: string; + relatedId?: number; + }> + ) { + const results = await this.prisma.notification.createMany({ + data: notifications, + }); + + this.logger.log(`Batch notifications created: ${results.count}`); + + return results; + } + + // ==================== 获取通知 ==================== + + async getNotifications( + tenantId: number, + recipientType: string, + recipientId: number, + query: any + ) { + const { page = 1, pageSize = 20, isRead, notificationType } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId, + recipientType, + recipientId, + }; + + if (isRead !== undefined) { + where.isRead = isRead === 'true'; + } + + if (notificationType) { + where.notificationType = notificationType; + } + + const [items, total, unreadCount] = await Promise.all([ + this.prisma.notification.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.notification.count({ where }), + this.prisma.notification.count({ + where: { ...where, isRead: false }, + }), + ]); + + return { + items, + total, + unreadCount, + page: +page, + pageSize: +pageSize, + }; + } + + async getUnreadCount(tenantId: number, recipientType: string, recipientId: number) { + return this.prisma.notification.count({ + where: { + tenantId, + recipientType, + recipientId, + isRead: false, + }, + }); + } + + // ==================== 标记已读 ==================== + + async markAsRead(tenantId: number, notificationId: number, recipientType: string, recipientId: number) { + const notification = await this.prisma.notification.findFirst({ + where: { + id: notificationId, + tenantId, + recipientType, + recipientId, + }, + }); + + if (!notification) { + return null; + } + + return this.prisma.notification.update({ + where: { id: notificationId }, + data: { + isRead: true, + readAt: new Date(), + }, + }); + } + + async markAllAsRead(tenantId: number, recipientType: string, recipientId: number) { + const result = await this.prisma.notification.updateMany({ + where: { + tenantId, + recipientType, + recipientId, + isRead: false, + }, + data: { + isRead: true, + readAt: new Date(), + }, + }); + + this.logger.log( + `Marked ${result.count} notifications as read for ${recipientType}:${recipientId}` + ); + + return result; + } +} diff --git a/reading-platform-backend/src/modules/notification/schedule-notification.service.ts b/reading-platform-backend/src/modules/notification/schedule-notification.service.ts new file mode 100644 index 0000000..6b590bf --- /dev/null +++ b/reading-platform-backend/src/modules/notification/schedule-notification.service.ts @@ -0,0 +1,333 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { PrismaService } from '../../database/prisma.service'; +import { NotificationService } from './notification.service'; + +@Injectable() +export class ScheduleNotificationService { + private readonly logger = new Logger(ScheduleNotificationService.name); + private isProcessing = false; + + constructor( + private prisma: PrismaService, + private notificationService: NotificationService, + ) {} + + /** + * 每30分钟检查一次即将开始的课程 + * 发送提醒给相关教师 + */ + @Cron(CronExpression.EVERY_30_MINUTES) + async handleUpcomingScheduleReminders() { + if (this.isProcessing) { + this.logger.debug('Previous reminder task still processing, skipping...'); + return; + } + + this.isProcessing = true; + + try { + const now = new Date(); + + // 计算接下来30分钟的时间范围 + const thirtyMinutesLater = new Date(now.getTime() + 30 * 60 * 1000); + + // 查找即将开始的排课(今天,未发送提醒) + const upcomingSchedules = await this.prisma.schedulePlan.findMany({ + where: { + status: 'ACTIVE', + reminderSent: false, + scheduledDate: { + gte: new Date(now.toISOString().split('T')[0] + 'T00:00:00.000Z'), + lt: new Date(now.toISOString().split('T')[0] + 'T23:59:59.999Z'), + }, + teacherId: { not: null }, + }, + include: { + teacher: { + select: { id: true, name: true }, + }, + course: { + select: { name: true, pictureBookName: true }, + }, + class: { + select: { name: true }, + }, + }, + }); + + // 过滤出即将开始的排课(根据时间段) + const schedulesToRemind = upcomingSchedules.filter((schedule) => { + if (!schedule.scheduledTime) return false; + + // 解析时间 "09:00-09:30" + const [startTime] = schedule.scheduledTime.split('-'); + if (!startTime) return false; + + const [hours, minutes] = startTime.split(':').map(Number); + const scheduleStartTime = new Date(now); + scheduleStartTime.setHours(hours, minutes, 0, 0); + + // 检查是否在接下来的30分钟内 + return scheduleStartTime >= now && scheduleStartTime <= thirtyMinutesLater; + }); + + this.logger.log(`Found ${schedulesToRemind.length} schedules to send reminders for`); + + // 发送提醒 + for (const schedule of schedulesToRemind) { + try { + await this.sendScheduleReminder(schedule); + } catch (error) { + this.logger.error(`Failed to send reminder for schedule ${schedule.id}:`, error); + } + } + } catch (error) { + this.logger.error('Error in schedule reminder task:', error); + } finally { + this.isProcessing = false; + } + } + + /** + * 发送单个排课提醒 + */ + private async sendScheduleReminder(schedule: any) { + if (!schedule.teacherId || !schedule.teacher) { + return; + } + + const courseName = schedule.course?.pictureBookName || schedule.course?.name || '课程'; + const className = schedule.class?.name || '班级'; + const timeStr = schedule.scheduledTime || '时间待定'; + + // 创建通知 + await this.notificationService.createNotification({ + tenantId: schedule.tenantId, + recipientType: 'TEACHER', + recipientId: schedule.teacherId, + title: '课程提醒', + content: `您即将在 ${timeStr} 为「${className}」讲授《${courseName}》,请做好准备。`, + notificationType: 'LESSON', + relatedType: 'SchedulePlan', + relatedId: schedule.id, + }); + + // 标记已发送提醒 + await this.prisma.schedulePlan.update({ + where: { id: schedule.id }, + data: { + reminderSent: true, + reminderSentAt: new Date(), + }, + }); + + this.logger.log( + `Reminder sent for schedule ${schedule.id} to teacher ${schedule.teacherId}` + ); + } + + /** + * 手动触发提醒检查(用于测试) + */ + async triggerReminderCheck() { + this.logger.log('Manual trigger of reminder check'); + await this.handleUpcomingScheduleReminders(); + return { message: 'Reminder check triggered' }; + } + + /** + * 重置所有提醒状态(用于测试或重置) + */ + async resetAllReminders() { + const result = await this.prisma.schedulePlan.updateMany({ + where: { + reminderSent: true, + }, + data: { + reminderSent: false, + reminderSentAt: null, + }, + }); + + this.logger.log(`Reset ${result.count} schedule reminders`); + + return { message: `Reset ${result.count} reminders` }; + } + + // ==================== 任务提醒 ==================== + + /** + * 每天早上9点检查即将到期的任务 + * 发送提醒给家长 + */ + @Cron('0 9 * * *') // 每天早上9点 + async handleTaskDeadlineReminders() { + this.logger.log('Starting task deadline reminder check...'); + + try { + const now = new Date(); + const threeDaysLater = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); + const oneDayLater = new Date(now.getTime() + 24 * 60 * 60 * 1000); + + // 查找即将到期且未完成的任务 + const tasksToRemind = await this.prisma.task.findMany({ + where: { + status: 'PUBLISHED', + endDate: { + gte: now, + lte: threeDaysLater, + }, + }, + include: { + course: { + select: { name: true, pictureBookName: true }, + }, + completions: { + where: { + status: { not: 'COMPLETED' }, + }, + include: { + student: { + select: { + id: true, + name: true, + parents: { + include: { + parent: { + select: { id: true }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + this.logger.log(`Found ${tasksToRemind.length} tasks with upcoming deadlines`); + + let reminderCount = 0; + + for (const task of tasksToRemind) { + const daysRemaining = Math.ceil( + (task.endDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) + ); + + // 只在截止前3天、1天、当天发送提醒 + if (![3, 1, 0].includes(daysRemaining)) continue; + + const taskName = task.course?.pictureBookName || task.course?.name || task.title; + + for (const completion of task.completions) { + // 发送提醒给每个学生的家长 + for (const parentRelation of completion.student.parents) { + try { + await this.notificationService.createNotification({ + tenantId: task.tenantId, + recipientType: 'PARENT', + recipientId: parentRelation.parent.id, + title: '任务即将截止', + content: `「${completion.student.name}」的任务《${taskName}》将在${daysRemaining === 0 ? '今天' : daysRemaining + '天后'}截止,请尽快完成。`, + notificationType: 'TASK', + relatedType: 'Task', + relatedId: task.id, + }); + reminderCount++; + } catch (error) { + this.logger.error( + `Failed to send task reminder to parent ${parentRelation.parent.id}:`, + error + ); + } + } + } + } + + this.logger.log(`Sent ${reminderCount} task reminder notifications`); + } catch (error) { + this.logger.error('Error in task deadline reminder task:', error); + } + } + + /** + * 手动发送任务提醒(教师点击"发送提醒"按钮时调用) + */ + async sendManualTaskReminder(tenantId: number, taskId: number) { + const task = await this.prisma.task.findFirst({ + where: { id: taskId, tenantId }, + include: { + course: { + select: { name: true, pictureBookName: true }, + }, + completions: { + where: { + status: { not: 'COMPLETED' }, + }, + include: { + student: { + select: { + id: true, + name: true, + parents: { + include: { + parent: { + select: { id: true, name: true }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!task) { + return { success: false, message: '任务不存在' }; + } + + const taskName = task.course?.pictureBookName || task.course?.name || task.title; + let reminderCount = 0; + const remindedStudents: { id: number; name: string }[] = []; + + for (const completion of task.completions) { + for (const parentRelation of completion.student.parents) { + try { + await this.notificationService.createNotification({ + tenantId: task.tenantId, + recipientType: 'PARENT', + recipientId: parentRelation.parent.id, + title: '阅读任务提醒', + content: `老师提醒您:「${completion.student.name}」的任务《${taskName}》尚未完成,请督促孩子尽快完成阅读任务。`, + notificationType: 'TASK', + relatedType: 'Task', + relatedId: task.id, + }); + reminderCount++; + } catch (error) { + this.logger.error( + `Failed to send manual task reminder to parent ${parentRelation.parent.id}:`, + error + ); + } + } + remindedStudents.push({ + id: completion.student.id, + name: completion.student.name, + }); + } + + this.logger.log( + `Manual task reminder sent for task ${taskId} to ${reminderCount} parents` + ); + + return { + success: true, + message: `已发送${reminderCount}条提醒`, + remindedCount: reminderCount, + students: remindedStudents, + }; + } +} diff --git a/reading-platform-backend/src/modules/parent/parent.controller.ts b/reading-platform-backend/src/modules/parent/parent.controller.ts new file mode 100644 index 0000000..8061c1f --- /dev/null +++ b/reading-platform-backend/src/modules/parent/parent.controller.ts @@ -0,0 +1,71 @@ +import { + Controller, + Get, + Post, + Put, + Body, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { ParentService } from './parent.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('parent') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('parent') +export class ParentController { + constructor(private readonly parentService: ParentService) {} + + // ==================== 孩子信息 ==================== + + @Get('children') + getChildren(@Request() req: any) { + return this.parentService.getChildren(req.user.userId, req.user.tenantId); + } + + @Get('children/:id') + getChildProfile(@Request() req: any, @Param('id') id: string) { + return this.parentService.getChildProfile(req.user.userId, +id, req.user.tenantId); + } + + // ==================== 阅读记录 ==================== + + @Get('children/:id/lessons') + getChildLessons(@Request() req: any, @Param('id') id: string, @Query() query: any) { + return this.parentService.getChildLessons(req.user.userId, +id, req.user.tenantId, query); + } + + // ==================== 任务 ==================== + + @Get('children/:id/tasks') + getChildTasks(@Request() req: any, @Param('id') id: string, @Query() query: any) { + return this.parentService.getChildTasks(req.user.userId, +id, req.user.tenantId, query); + } + + @Put('children/:studentId/tasks/:taskId/feedback') + submitTaskFeedback( + @Request() req: any, + @Param('studentId') studentId: string, + @Param('taskId') taskId: string, + @Body() body: { feedback: string }, + ) { + return this.parentService.submitTaskFeedback( + req.user.userId, + +studentId, + +taskId, + req.user.tenantId, + body.feedback, + ); + } + + // ==================== 成长档案 ==================== + + @Get('children/:id/growth-records') + getChildGrowthRecords(@Request() req: any, @Param('id') id: string, @Query() query: any) { + return this.parentService.getChildGrowthRecords(req.user.userId, +id, req.user.tenantId, query); + } +} diff --git a/reading-platform-backend/src/modules/parent/parent.module.ts b/reading-platform-backend/src/modules/parent/parent.module.ts new file mode 100644 index 0000000..7124c76 --- /dev/null +++ b/reading-platform-backend/src/modules/parent/parent.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ParentController } from './parent.controller'; +import { ParentService } from './parent.service'; + +@Module({ + controllers: [ParentController], + providers: [ParentService], + exports: [ParentService], +}) +export class ParentModule {} diff --git a/reading-platform-backend/src/modules/parent/parent.service.ts b/reading-platform-backend/src/modules/parent/parent.service.ts new file mode 100644 index 0000000..aa66581 --- /dev/null +++ b/reading-platform-backend/src/modules/parent/parent.service.ts @@ -0,0 +1,309 @@ +import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class ParentService { + private readonly logger = new Logger(ParentService.name); + + constructor(private prisma: PrismaService) {} + + // ==================== 孩子信息 ==================== + + async getChildren(parentId: number, tenantId: number) { + const parent = await this.prisma.parent.findFirst({ + where: { id: parentId, tenantId }, + include: { + children: { + include: { + student: { + include: { + class: { + select: { + id: true, + name: true, + grade: true, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!parent) { + throw new NotFoundException('家长不存在'); + } + + return parent.children.map((c) => ({ + id: c.student.id, + name: c.student.name, + gender: c.student.gender, + birthDate: c.student.birthDate, + relationship: c.relationship, + class: c.student.class, + readingCount: c.student.readingCount, + lessonCount: c.student.lessonCount, + })); + } + + async getChildProfile(parentId: number, studentId: number, tenantId: number) { + // 验证亲子关系 + const relation = await this.prisma.parentStudent.findFirst({ + where: { parentId, studentId }, + }); + + if (!relation) { + throw new ForbiddenException('您没有查看该学生信息的权限'); + } + + const student = await this.prisma.student.findFirst({ + where: { id: studentId, tenantId }, + include: { + class: { + select: { + id: true, + name: true, + grade: true, + }, + }, + }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + // 获取统计数据 + const [lessonRecords, growthRecords, taskCompletions] = await Promise.all([ + this.prisma.studentRecord.count({ + where: { studentId }, + }), + this.prisma.growthRecord.count({ + where: { studentId }, + }), + this.prisma.taskCompletion.count({ + where: { + studentId, + status: 'COMPLETED', + }, + }), + ]); + + return { + ...student, + stats: { + lessonRecords, + growthRecords, + taskCompletions, + }, + }; + } + + // ==================== 阅读记录 ==================== + + async getChildLessons(parentId: number, studentId: number, tenantId: number, query: any) { + // 验证亲子关系 + const relation = await this.prisma.parentStudent.findFirst({ + where: { parentId, studentId }, + }); + + if (!relation) { + throw new ForbiddenException('您没有查看该学生信息的权限'); + } + + const { page = 1, pageSize = 10 } = query; + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where = { studentId }; + + const [items, total] = await Promise.all([ + this.prisma.studentRecord.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + lesson: { + select: { + id: true, + startDatetime: true, + endDatetime: true, + actualDuration: true, + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + }, + }, + }, + }), + this.prisma.studentRecord.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + // ==================== 任务列表 ==================== + + async getChildTasks(parentId: number, studentId: number, tenantId: number, query: any) { + // 验证亲子关系 + const relation = await this.prisma.parentStudent.findFirst({ + where: { parentId, studentId }, + }); + + if (!relation) { + throw new ForbiddenException('您没有查看该学生信息的权限'); + } + + const { page = 1, pageSize = 10, status } = query; + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + studentId, + task: { tenantId, status: 'PUBLISHED' }, + }; + + if (status) { + where.status = status; + } + + const [items, total] = await Promise.all([ + this.prisma.taskCompletion.findMany({ + where, + skip, + take, + orderBy: { task: { createdAt: 'desc' } }, + include: { + task: { + select: { + id: true, + title: true, + description: true, + taskType: true, + startDate: true, + endDate: true, + course: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }), + this.prisma.taskCompletion.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + // 提交家长反馈 + async submitTaskFeedback( + parentId: number, + studentId: number, + taskId: number, + tenantId: number, + feedback: string, + ) { + // 验证亲子关系 + const relation = await this.prisma.parentStudent.findFirst({ + where: { parentId, studentId }, + }); + + if (!relation) { + throw new ForbiddenException('您没有操作该学生信息的权限'); + } + + const completion = await this.prisma.taskCompletion.findFirst({ + where: { + taskId, + studentId, + task: { tenantId }, + }, + }); + + if (!completion) { + throw new NotFoundException('任务记录不存在'); + } + + const updated = await this.prisma.taskCompletion.update({ + where: { + taskId_studentId: { taskId, studentId }, + }, + data: { + parentFeedback: feedback, + }, + }); + + this.logger.log(`Parent feedback submitted: task=${taskId}, student=${studentId}`); + + return updated; + } + + // ==================== 成长档案 ==================== + + async getChildGrowthRecords(parentId: number, studentId: number, tenantId: number, query: any) { + // 验证亲子关系 + const relation = await this.prisma.parentStudent.findFirst({ + where: { parentId, studentId }, + }); + + if (!relation) { + throw new ForbiddenException('您没有查看该学生信息的权限'); + } + + const { page = 1, pageSize = 10 } = query; + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where = { + studentId, + tenantId, + }; + + const [items, total] = await Promise.all([ + this.prisma.growthRecord.findMany({ + where, + skip, + take, + orderBy: { recordDate: 'desc' }, + include: { + class: { + select: { + id: true, + name: true, + }, + }, + }, + }), + this.prisma.growthRecord.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + images: item.images ? JSON.parse(item.images) : [], + })), + total, + page: +page, + pageSize: +pageSize, + }; + } +} diff --git a/reading-platform-backend/src/modules/resource/dto/create-resource.dto.ts b/reading-platform-backend/src/modules/resource/dto/create-resource.dto.ts new file mode 100644 index 0000000..e00f347 --- /dev/null +++ b/reading-platform-backend/src/modules/resource/dto/create-resource.dto.ts @@ -0,0 +1,144 @@ +import { IsString, IsNotEmpty, IsOptional, IsInt, IsArray, IsEnum, Min } from 'class-validator'; + +export enum LibraryType { + PICTURE_BOOK = 'PICTURE_BOOK', + MATERIAL = 'MATERIAL', + TEMPLATE = 'TEMPLATE', +} + +export enum FileType { + IMAGE = 'IMAGE', + PDF = 'PDF', + VIDEO = 'VIDEO', + AUDIO = 'AUDIO', + PPT = 'PPT', + OTHER = 'OTHER', +} + +export class CreateLibraryDto { + @IsString() + @IsNotEmpty({ message: '资源库名称不能为空' }) + name: string; + + @IsEnum(LibraryType) + libraryType: LibraryType; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + coverImage?: string; +} + +export class UpdateLibraryDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '资源库名称不能为空' }) + name?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + coverImage?: string; + + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; +} + +export class CreateResourceItemDto { + @IsInt() + @IsNotEmpty({ message: '资源库ID不能为空' }) + libraryId: number; + + @IsString() + @IsNotEmpty({ message: '资源名称不能为空' }) + title: string; + + @IsOptional() + @IsString() + description?: string; + + @IsEnum(FileType) + fileType: FileType; + + @IsString() + @IsNotEmpty({ message: '文件路径不能为空' }) + filePath: string; + + @IsOptional() + @IsInt() + fileSize?: number; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; +} + +export class UpdateResourceItemDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '资源名称不能为空' }) + title?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; +} + +export class QueryLibraryDto { + @IsOptional() + @IsInt() + page?: number; + + @IsOptional() + @IsInt() + pageSize?: number; + + @IsOptional() + @IsString() + libraryType?: string; + + @IsOptional() + @IsString() + keyword?: string; +} + +export class QueryResourceItemDto { + @IsOptional() + @IsInt() + page?: number; + + @IsOptional() + @IsInt() + pageSize?: number; + + @IsOptional() + @IsInt() + libraryId?: number; + + @IsOptional() + @IsString() + fileType?: string; + + @IsOptional() + @IsString() + keyword?: string; +} diff --git a/reading-platform-backend/src/modules/resource/resource.controller.ts b/reading-platform-backend/src/modules/resource/resource.controller.ts new file mode 100644 index 0000000..a5e9153 --- /dev/null +++ b/reading-platform-backend/src/modules/resource/resource.controller.ts @@ -0,0 +1,90 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { ResourceService } from './resource.service'; +import { CreateLibraryDto, UpdateLibraryDto, CreateResourceItemDto, UpdateResourceItemDto } from './dto/create-resource.dto'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('admin/resources') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class ResourceController { + constructor(private readonly resourceService: ResourceService) {} + + // ==================== 资源库管理 ==================== + + @Get('libraries') + findAllLibraries(@Query() query: any) { + return this.resourceService.findAllLibraries(query); + } + + @Get('libraries/:id') + findLibrary(@Param('id') id: string) { + return this.resourceService.findLibrary(+id); + } + + @Post('libraries') + createLibrary(@Body() dto: CreateLibraryDto, @Request() req: any) { + return this.resourceService.createLibrary(dto, req.user.userId); + } + + @Put('libraries/:id') + updateLibrary(@Param('id') id: string, @Body() dto: UpdateLibraryDto) { + return this.resourceService.updateLibrary(+id, dto); + } + + @Delete('libraries/:id') + deleteLibrary(@Param('id') id: string) { + return this.resourceService.deleteLibrary(+id); + } + + // ==================== 资源项目管理 ==================== + + @Get('items') + findAllItems(@Query() query: any) { + return this.resourceService.findAllItems(query); + } + + @Get('items/:id') + findItem(@Param('id') id: string) { + return this.resourceService.findItem(+id); + } + + @Post('items') + createItem(@Body() dto: CreateResourceItemDto) { + return this.resourceService.createItem(dto); + } + + @Put('items/:id') + updateItem(@Param('id') id: string, @Body() dto: UpdateResourceItemDto) { + return this.resourceService.updateItem(+id, dto); + } + + @Delete('items/:id') + deleteItem(@Param('id') id: string) { + return this.resourceService.deleteItem(+id); + } + + @Post('items/batch-delete') + batchDeleteItems(@Body() body: { ids: number[] }) { + return this.resourceService.batchDeleteItems(body.ids); + } + + // ==================== 统计数据 ==================== + + @Get('stats') + getStats() { + return this.resourceService.getStats(); + } +} diff --git a/reading-platform-backend/src/modules/resource/resource.module.ts b/reading-platform-backend/src/modules/resource/resource.module.ts new file mode 100644 index 0000000..5c025e7 --- /dev/null +++ b/reading-platform-backend/src/modules/resource/resource.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ResourceController } from './resource.controller'; +import { ResourceService } from './resource.service'; + +@Module({ + controllers: [ResourceController], + providers: [ResourceService], + exports: [ResourceService], +}) +export class ResourceModule {} diff --git a/reading-platform-backend/src/modules/resource/resource.service.ts b/reading-platform-backend/src/modules/resource/resource.service.ts new file mode 100644 index 0000000..0c157ae --- /dev/null +++ b/reading-platform-backend/src/modules/resource/resource.service.ts @@ -0,0 +1,357 @@ +import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { CreateLibraryDto, UpdateLibraryDto, CreateResourceItemDto, UpdateResourceItemDto } from './dto/create-resource.dto'; + +@Injectable() +export class ResourceService { + private readonly logger = new Logger(ResourceService.name); + + constructor(private prisma: PrismaService) {} + + // ==================== 工具方法 ==================== + + private parseJsonArray(value: any): any[] { + if (!value) return []; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return []; + } + } + return Array.isArray(value) ? value : []; + } + + // ==================== 资源库管理 ==================== + + async findAllLibraries(query: any) { + const { page = 1, pageSize = 10, libraryType, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = {}; + + if (libraryType) { + where.libraryType = libraryType; + } + + if (keyword) { + where.name = { contains: keyword }; + } + + const [items, total] = await Promise.all([ + this.prisma.resourceLibrary.findMany({ + where, + skip, + take, + orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }], + include: { + _count: { + select: { + items: true, + }, + }, + }, + }), + this.prisma.resourceLibrary.count({ where }), + ]); + + return { + items: items.map((lib) => ({ + ...lib, + itemCount: lib._count.items, + _count: undefined, + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findLibrary(id: number) { + const library = await this.prisma.resourceLibrary.findUnique({ + where: { id }, + include: { + items: { + orderBy: { sortOrder: 'asc' }, + take: 100, + }, + }, + }); + + if (!library) { + throw new NotFoundException('资源库不存在'); + } + + return { + ...library, + items: library.items.map((item) => ({ + ...item, + tags: this.parseJsonArray(item.tags), + })), + }; + } + + async createLibrary(dto: CreateLibraryDto, userId: number) { + const library = await this.prisma.resourceLibrary.create({ + data: { + name: dto.name, + libraryType: dto.libraryType, + description: dto.description, + coverImage: dto.coverImage, + createdBy: userId, + }, + }); + + this.logger.log(`Library created: ${library.id}`); + + return library; + } + + async updateLibrary(id: number, dto: UpdateLibraryDto) { + const library = await this.prisma.resourceLibrary.findUnique({ + where: { id }, + }); + + if (!library) { + throw new NotFoundException('资源库不存在'); + } + + const updated = await this.prisma.resourceLibrary.update({ + where: { id }, + data: { + name: dto.name, + description: dto.description, + coverImage: dto.coverImage, + sortOrder: dto.sortOrder, + }, + }); + + this.logger.log(`Library updated: ${id}`); + + return updated; + } + + async deleteLibrary(id: number) { + const library = await this.prisma.resourceLibrary.findUnique({ + where: { id }, + include: { + _count: { + select: { + items: true, + }, + }, + }, + }); + + if (!library) { + throw new NotFoundException('资源库不存在'); + } + + // 删除资源库(会级联删除所有资源项目) + await this.prisma.resourceLibrary.delete({ + where: { id }, + }); + + this.logger.log(`Library deleted: ${id}`); + + return { message: '删除成功' }; + } + + // ==================== 资源项目管理 ==================== + + async findAllItems(query: any) { + const { page = 1, pageSize = 20, libraryId, fileType, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = {}; + + if (libraryId) { + where.libraryId = +libraryId; + } + + if (fileType) { + where.fileType = fileType; + } + + if (keyword) { + where.OR = [ + { title: { contains: keyword } }, + { description: { contains: keyword } }, + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.resourceItem.findMany({ + where, + skip, + take, + orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }], + include: { + library: { + select: { + id: true, + name: true, + libraryType: true, + }, + }, + }, + }), + this.prisma.resourceItem.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + tags: this.parseJsonArray(item.tags), + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findItem(id: number) { + const item = await this.prisma.resourceItem.findUnique({ + where: { id }, + include: { + library: { + select: { + id: true, + name: true, + libraryType: true, + }, + }, + }, + }); + + if (!item) { + throw new NotFoundException('资源项目不存在'); + } + + return { + ...item, + tags: this.parseJsonArray(item.tags), + }; + } + + async createItem(dto: CreateResourceItemDto) { + // 检查资源库是否存在 + const library = await this.prisma.resourceLibrary.findUnique({ + where: { id: dto.libraryId }, + }); + + if (!library) { + throw new NotFoundException('资源库不存在'); + } + + const item = await this.prisma.resourceItem.create({ + data: { + libraryId: dto.libraryId, + title: dto.title, + description: dto.description, + fileType: dto.fileType, + filePath: dto.filePath, + fileSize: dto.fileSize, + tags: JSON.stringify(dto.tags || []), + }, + }); + + this.logger.log(`Resource item created: ${item.id}`); + + return { + ...item, + tags: this.parseJsonArray(item.tags), + }; + } + + async updateItem(id: number, dto: UpdateResourceItemDto) { + const item = await this.prisma.resourceItem.findUnique({ + where: { id }, + }); + + if (!item) { + throw new NotFoundException('资源项目不存在'); + } + + const updated = await this.prisma.resourceItem.update({ + where: { id }, + data: { + title: dto.title, + description: dto.description, + tags: dto.tags ? JSON.stringify(dto.tags) : undefined, + sortOrder: dto.sortOrder, + }, + }); + + this.logger.log(`Resource item updated: ${id}`); + + return { + ...updated, + tags: this.parseJsonArray(updated.tags), + }; + } + + async deleteItem(id: number) { + const item = await this.prisma.resourceItem.findUnique({ + where: { id }, + }); + + if (!item) { + throw new NotFoundException('资源项目不存在'); + } + + await this.prisma.resourceItem.delete({ + where: { id }, + }); + + this.logger.log(`Resource item deleted: ${id}`); + + return { message: '删除成功' }; + } + + async batchDeleteItems(ids: number[]) { + await this.prisma.resourceItem.deleteMany({ + where: { + id: { in: ids }, + }, + }); + + this.logger.log(`Batch deleted ${ids.length} resource items`); + + return { message: `成功删除 ${ids.length} 个资源` }; + } + + // ==================== 统计数据 ==================== + + async getStats() { + const [totalLibraries, totalItems, itemsByType, itemsByLibraryType] = await Promise.all([ + this.prisma.resourceLibrary.count(), + this.prisma.resourceItem.count(), + this.prisma.resourceItem.groupBy({ + by: ['fileType'], + _count: true, + }), + this.prisma.resourceLibrary.groupBy({ + by: ['libraryType'], + _count: true, + }), + ]); + + return { + totalLibraries, + totalItems, + itemsByType: itemsByType.reduce((acc, item) => { + acc[item.fileType] = item._count; + return acc; + }, {} as Record), + itemsByLibraryType: itemsByLibraryType.reduce((acc, lib) => { + acc[lib.libraryType] = lib._count; + return acc; + }, {} as Record), + }; + } +} diff --git a/reading-platform-backend/src/modules/school-course/school-course.controller.ts b/reading-platform-backend/src/modules/school-course/school-course.controller.ts new file mode 100644 index 0000000..cb24e42 --- /dev/null +++ b/reading-platform-backend/src/modules/school-course/school-course.controller.ts @@ -0,0 +1,227 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + ParseIntPipe, + UseGuards, + Request, +} from '@nestjs/common'; +import { SchoolCourseService } from './school-course.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +// 学校端校本课程包控制器 +@Controller('school/school-courses') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class SchoolCourseController { + constructor(private readonly schoolCourseService: SchoolCourseService) {} + + @Get() + async findAll(@Request() req: any) { + return this.schoolCourseService.findAll(req.user.tenantId); + } + + @Get('source-courses') + async getSourceCourses(@Request() req: any) { + return this.schoolCourseService.getSourceCourses(req.user.tenantId); + } + + @Get(':id') + async findOne(@Param('id', ParseIntPipe) id: number, @Request() req: any) { + return this.schoolCourseService.findOne(id, req.user.tenantId); + } + + @Post() + async create( + @Request() req: any, + @Body() + body: { + sourceCourseId: number; + name: string; + description?: string; + changesSummary?: string; + }, + ) { + return this.schoolCourseService.create( + req.user.tenantId, + body.sourceCourseId, + req.user.id, + body, + ); + } + + @Put(':id') + async update( + @Param('id', ParseIntPipe) id: number, + @Request() req: any, + @Body() + body: { + name?: string; + description?: string; + changesSummary?: string; + status?: string; + }, + ) { + return this.schoolCourseService.update(id, req.user.tenantId, body); + } + + @Delete(':id') + async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) { + return this.schoolCourseService.delete(id, req.user.tenantId); + } + + // 课程管理 + @Get(':id/lessons') + async findLessons( + @Param('id', ParseIntPipe) id: number, + @Request() req: any, + ) { + return this.schoolCourseService.findLessons(id, req.user.tenantId); + } + + @Put(':id/lessons/:lessonId') + async updateLesson( + @Param('id', ParseIntPipe) id: number, + @Param('lessonId', ParseIntPipe) lessonId: number, + @Request() req: any, + @Body() + body: { + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + changeNote?: string; + stepsData?: string; + }, + ) { + return this.schoolCourseService.updateLesson(id, lessonId, req.user.tenantId, body); + } + + // 预约管理 + @Get(':id/reservations') + async findReservations( + @Param('id', ParseIntPipe) id: number, + @Request() req: any, + ) { + return this.schoolCourseService.findReservations(id, req.user.tenantId); + } + + @Post(':id/reservations') + async createReservation( + @Param('id', ParseIntPipe) id: number, + @Request() req: any, + @Body() + body: { + teacherId: number; + classId: number; + scheduledDate: string; + scheduledTime?: string; + note?: string; + }, + ) { + return this.schoolCourseService.createReservation(id, req.user.tenantId, body); + } + + @Post('reservations/:reservationId/cancel') + async cancelReservation( + @Param('reservationId', ParseIntPipe) reservationId: number, + @Request() req: any, + ) { + return this.schoolCourseService.cancelReservation(reservationId, req.user.tenantId); + } +} + +// 教师端校本课程包控制器 +@Controller('teacher/school-courses') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class TeacherSchoolCourseController { + constructor(private readonly schoolCourseService: SchoolCourseService) {} + + @Get() + async findAll(@Request() req: any) { + return this.schoolCourseService.findAll(req.user.tenantId, req.user.id); + } + + @Get('source-courses') + async getSourceCourses(@Request() req: any) { + return this.schoolCourseService.getSourceCourses(req.user.tenantId); + } + + @Get(':id') + async findOne(@Param('id', ParseIntPipe) id: number, @Request() req: any) { + return this.schoolCourseService.findOne(id, req.user.tenantId); + } + + @Post() + async create( + @Request() req: any, + @Body() + body: { + sourceCourseId: number; + name: string; + description?: string; + changesSummary?: string; + }, + ) { + return this.schoolCourseService.create( + req.user.tenantId, + body.sourceCourseId, + req.user.id, + body, + ); + } + + @Put(':id') + async update( + @Param('id', ParseIntPipe) id: number, + @Request() req: any, + @Body() + body: { + name?: string; + description?: string; + changesSummary?: string; + status?: string; + }, + ) { + return this.schoolCourseService.update(id, req.user.tenantId, body); + } + + @Delete(':id') + async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) { + return this.schoolCourseService.delete(id, req.user.tenantId); + } + + // 课程管理 + @Get(':id/lessons') + async findLessons( + @Param('id', ParseIntPipe) id: number, + @Request() req: any, + ) { + return this.schoolCourseService.findLessons(id, req.user.tenantId); + } + + @Put(':id/lessons/:lessonId') + async updateLesson( + @Param('id', ParseIntPipe) id: number, + @Param('lessonId', ParseIntPipe) lessonId: number, + @Request() req: any, + @Body() + body: { + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + changeNote?: string; + stepsData?: string; + }, + ) { + return this.schoolCourseService.updateLesson(id, lessonId, req.user.tenantId, body); + } +} diff --git a/reading-platform-backend/src/modules/school-course/school-course.module.ts b/reading-platform-backend/src/modules/school-course/school-course.module.ts new file mode 100644 index 0000000..ff9e137 --- /dev/null +++ b/reading-platform-backend/src/modules/school-course/school-course.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SchoolCourseController, TeacherSchoolCourseController } from './school-course.controller'; +import { SchoolCourseService } from './school-course.service'; + +@Module({ + controllers: [SchoolCourseController, TeacherSchoolCourseController], + providers: [SchoolCourseService], + exports: [SchoolCourseService], +}) +export class SchoolCourseModule {} diff --git a/reading-platform-backend/src/modules/school-course/school-course.service.ts b/reading-platform-backend/src/modules/school-course/school-course.service.ts new file mode 100644 index 0000000..321154a --- /dev/null +++ b/reading-platform-backend/src/modules/school-course/school-course.service.ts @@ -0,0 +1,396 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class SchoolCourseService { + constructor(private prisma: PrismaService) {} + + // ==================== 校本课程包管理 ==================== + + async findAll(tenantId?: number, createdBy?: number) { + const where: any = {}; + if (tenantId) { + where.tenantId = tenantId; + } + if (createdBy) { + where.createdBy = createdBy; + } + + return this.prisma.schoolCourse.findMany({ + where, + include: { + sourceCourse: { + select: { + id: true, + name: true, + coverImagePath: true, + }, + }, + _count: { + select: { lessons: true, reservations: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + async findOne(id: number, tenantId?: number) { + const where: any = { id }; + if (tenantId) { + where.tenantId = tenantId; + } + + const schoolCourse = await this.prisma.schoolCourse.findFirst({ + where, + include: { + sourceCourse: { + select: { + id: true, + name: true, + coverImagePath: true, + description: true, + }, + }, + lessons: { + orderBy: { lessonType: 'asc' }, + }, + reservations: { + orderBy: { createdAt: 'desc' }, + take: 10, + }, + }, + }); + + if (!schoolCourse) { + throw new Error('校本课程包不存在'); + } + + return schoolCourse; + } + + async create( + tenantId: number, + sourceCourseId: number, + createdBy: number, + data: { + name: string; + description?: string; + changesSummary?: string; + }, + ) { + // 检查源课程是否存在 + const sourceCourse = await this.prisma.course.findUnique({ + where: { id: sourceCourseId }, + include: { + courseLessons: { + include: { + steps: true, + }, + }, + }, + }); + + if (!sourceCourse) { + throw new Error('源课程包不存在'); + } + + // 检查租户是否有权限 + const hasAccess = await this.checkTenantAccess(tenantId, sourceCourseId); + if (!hasAccess) { + throw new Error('无权访问该课程包'); + } + + // 创建校本课程包 + const schoolCourse = await this.prisma.schoolCourse.create({ + data: { + tenantId, + sourceCourseId, + name: data.name, + description: data.description, + createdBy, + changesSummary: data.changesSummary, + }, + }); + + // 复制源课程的课程 + for (const lesson of sourceCourse.courseLessons) { + await this.prisma.schoolCourseLesson.create({ + data: { + schoolCourseId: schoolCourse.id, + sourceLessonId: lesson.id, + lessonType: lesson.lessonType, + objectives: lesson.objectives, + preparation: lesson.preparation, + extension: lesson.extension, + reflection: lesson.reflection, + stepsData: JSON.stringify(lesson.steps), + }, + }); + } + + return this.findOne(schoolCourse.id); + } + + async update( + id: number, + tenantId: number, + data: { + name?: string; + description?: string; + changesSummary?: string; + status?: string; + }, + ) { + // 验证权限 + const existing = await this.prisma.schoolCourse.findFirst({ + where: { id, tenantId }, + }); + + if (!existing) { + throw new Error('校本课程包不存在或无权访问'); + } + + return this.prisma.schoolCourse.update({ + where: { id }, + data, + }); + } + + async delete(id: number, tenantId: number) { + // 验证权限 + const existing = await this.prisma.schoolCourse.findFirst({ + where: { id, tenantId }, + }); + + if (!existing) { + throw new Error('校本课程包不存在或无权访问'); + } + + // 检查是否有预约 + const reservationCount = await this.prisma.schoolCourseReservation.count({ + where: { schoolCourseId: id, status: 'PENDING' }, + }); + + if (reservationCount > 0) { + throw new Error(`有 ${reservationCount} 个待进行的预约,无法删除`); + } + + await this.prisma.schoolCourse.delete({ + where: { id }, + }); + + return { success: true }; + } + + // ==================== 校本课程管理 ==================== + + async findLessons(schoolCourseId: number, tenantId: number) { + // 验证权限 + const schoolCourse = await this.prisma.schoolCourse.findFirst({ + where: { id: schoolCourseId, tenantId }, + }); + + if (!schoolCourse) { + throw new Error('校本课程包不存在或无权访问'); + } + + return this.prisma.schoolCourseLesson.findMany({ + where: { schoolCourseId }, + orderBy: { lessonType: 'asc' }, + }); + } + + async updateLesson( + schoolCourseId: number, + lessonId: number, + tenantId: number, + data: { + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + changeNote?: string; + stepsData?: string; + }, + ) { + // 验证权限 + const schoolCourse = await this.prisma.schoolCourse.findFirst({ + where: { id: schoolCourseId, tenantId }, + }); + + if (!schoolCourse) { + throw new Error('校本课程包不存在或无权访问'); + } + + return this.prisma.schoolCourseLesson.update({ + where: { id: lessonId }, + data, + }); + } + + // ==================== 预约管理 ==================== + + async findReservations(schoolCourseId: number, tenantId: number) { + // 验证权限 + const schoolCourse = await this.prisma.schoolCourse.findFirst({ + where: { id: schoolCourseId, tenantId }, + }); + + if (!schoolCourse) { + throw new Error('校本课程包不存在或无权访问'); + } + + return this.prisma.schoolCourseReservation.findMany({ + where: { schoolCourseId }, + orderBy: { scheduledDate: 'asc' }, + }); + } + + async createReservation( + schoolCourseId: number, + tenantId: number, + data: { + teacherId: number; + classId: number; + scheduledDate: string; + scheduledTime?: string; + note?: string; + }, + ) { + // 验证权限 + const schoolCourse = await this.prisma.schoolCourse.findFirst({ + where: { id: schoolCourseId, tenantId }, + }); + + if (!schoolCourse) { + throw new Error('校本课程包不存在或无权访问'); + } + + // 验证教师和班级属于该租户 + const [teacher, class_] = await Promise.all([ + this.prisma.teacher.findFirst({ + where: { id: data.teacherId, tenantId }, + }), + this.prisma.class.findFirst({ + where: { id: data.classId, tenantId }, + }), + ]); + + if (!teacher) { + throw new Error('教师不存在或不属于该学校'); + } + if (!class_) { + throw new Error('班级不存在或不属于该学校'); + } + + const reservation = await this.prisma.schoolCourseReservation.create({ + data: { + schoolCourseId, + teacherId: data.teacherId, + classId: data.classId, + scheduledDate: data.scheduledDate, + scheduledTime: data.scheduledTime, + note: data.note, + status: 'PENDING', + }, + }); + + // 更新使用次数 + await this.prisma.schoolCourse.update({ + where: { id: schoolCourseId }, + data: { usageCount: { increment: 1 } }, + }); + + return reservation; + } + + async updateReservationStatus( + reservationId: number, + tenantId: number, + status: string, + ) { + const reservation = await this.prisma.schoolCourseReservation.findFirst({ + where: { id: reservationId }, + include: { schoolCourse: true }, + }); + + if (!reservation || reservation.schoolCourse.tenantId !== tenantId) { + throw new Error('预约不存在或无权访问'); + } + + return this.prisma.schoolCourseReservation.update({ + where: { id: reservationId }, + data: { status }, + }); + } + + async cancelReservation(reservationId: number, tenantId: number) { + return this.updateReservationStatus(reservationId, tenantId, 'CANCELLED'); + } + + // ==================== 辅助方法 ==================== + + private async checkTenantAccess(tenantId: number, courseId: number) { + // 检查直接授权 + const tenantCourse = await this.prisma.tenantCourse.findFirst({ + where: { tenantId, courseId, authorized: true }, + }); + + if (tenantCourse) { + return true; + } + + // 检查套餐授权 + const tenantPackage = await this.prisma.tenantPackage.findFirst({ + where: { + tenantId, + status: 'ACTIVE', + package: { + courses: { + some: { courseId }, + }, + }, + }, + }); + + return !!tenantPackage; + } + + // 获取可创建校本课程包的源课程列表 + async getSourceCourses(tenantId: number) { + // 通过套餐获取课程 + const tenantPackages = await this.prisma.tenantPackage.findMany({ + where: { tenantId, status: 'ACTIVE' }, + include: { + package: { + include: { + courses: { + include: { + course: { + select: { + id: true, + name: true, + coverImagePath: true, + description: true, + duration: true, + }, + }, + }, + }, + }, + }, + }, + }); + + const courses = new Map(); + for (const tp of tenantPackages) { + for (const pc of tp.package.courses) { + if (!courses.has(pc.course.id)) { + courses.set(pc.course.id, pc.course); + } + } + } + + return Array.from(courses.values()); + } +} diff --git a/reading-platform-backend/src/modules/school/dto/class-teacher.dto.ts b/reading-platform-backend/src/modules/school/dto/class-teacher.dto.ts new file mode 100644 index 0000000..5631ff8 --- /dev/null +++ b/reading-platform-backend/src/modules/school/dto/class-teacher.dto.ts @@ -0,0 +1,40 @@ +import { IsInt, IsString, IsBoolean, IsOptional, IsIn } from 'class-validator'; + +// 教师角色类型 +export type TeacherRole = 'MAIN' | 'ASSIST' | 'CARE'; + +// 添加班级教师 DTO +export class AddClassTeacherDto { + @IsInt() + teacherId: number; + + @IsString() + @IsIn(['MAIN', 'ASSIST', 'CARE']) + role: TeacherRole; + + @IsBoolean() + @IsOptional() + isPrimary?: boolean; +} + +// 更新班级教师 DTO +export class UpdateClassTeacherDto { + @IsString() + @IsIn(['MAIN', 'ASSIST', 'CARE']) + @IsOptional() + role?: TeacherRole; + + @IsBoolean() + @IsOptional() + isPrimary?: boolean; +} + +// 学生调班 DTO +export class TransferStudentDto { + @IsInt() + toClassId: number; + + @IsString() + @IsOptional() + reason?: string; +} diff --git a/reading-platform-backend/src/modules/school/dto/create-class.dto.ts b/reading-platform-backend/src/modules/school/dto/create-class.dto.ts new file mode 100644 index 0000000..df5b22a --- /dev/null +++ b/reading-platform-backend/src/modules/school/dto/create-class.dto.ts @@ -0,0 +1,31 @@ +import { IsString, IsNotEmpty, IsOptional, IsArray, IsInt } from 'class-validator'; + +export class CreateClassDto { + @IsString() + @IsNotEmpty({ message: '班级名称不能为空' }) + name: string; + + @IsString() + @IsNotEmpty({ message: '年级不能为空' }) + grade: string; + + @IsOptional() + @IsInt() + teacherId?: number; +} + +export class UpdateClassDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '班级名称不能为空' }) + name?: string; + + @IsOptional() + @IsString() + @IsNotEmpty({ message: '年级不能为空' }) + grade?: string; + + @IsOptional() + @IsInt() + teacherId?: number; +} diff --git a/reading-platform-backend/src/modules/school/dto/create-student.dto.ts b/reading-platform-backend/src/modules/school/dto/create-student.dto.ts new file mode 100644 index 0000000..07bd67c --- /dev/null +++ b/reading-platform-backend/src/modules/school/dto/create-student.dto.ts @@ -0,0 +1,56 @@ +import { IsString, IsNotEmpty, IsOptional, IsInt, Matches, IsIn } from 'class-validator'; + +export class CreateStudentDto { + @IsString() + @IsNotEmpty({ message: '姓名不能为空' }) + name: string; + + @IsOptional() + @IsIn(['男', '女'], { message: '性别只能是男或女' }) + gender?: string; + + @IsOptional() + @IsString() + birthDate?: string; + + @IsInt() + @IsNotEmpty({ message: '班级不能为空' }) + classId: number; + + @IsOptional() + @IsString() + parentName?: string; + + @IsOptional() + @IsString() + @Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }) + parentPhone?: string; +} + +export class UpdateStudentDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '姓名不能为空' }) + name?: string; + + @IsOptional() + @IsIn(['男', '女'], { message: '性别只能是男或女' }) + gender?: string; + + @IsOptional() + @IsString() + birthDate?: string; + + @IsOptional() + @IsInt() + classId?: number; + + @IsOptional() + @IsString() + parentName?: string; + + @IsOptional() + @IsString() + @Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }) + parentPhone?: string; +} diff --git a/reading-platform-backend/src/modules/school/dto/create-teacher.dto.ts b/reading-platform-backend/src/modules/school/dto/create-teacher.dto.ts new file mode 100644 index 0000000..7112964 --- /dev/null +++ b/reading-platform-backend/src/modules/school/dto/create-teacher.dto.ts @@ -0,0 +1,51 @@ +import { IsString, IsNotEmpty, IsOptional, IsEmail, IsArray, IsInt, MinLength, Matches } from 'class-validator'; + +export class CreateTeacherDto { + @IsString() + @IsNotEmpty({ message: '姓名不能为空' }) + name: string; + + @IsString() + @IsNotEmpty({ message: '手机号不能为空' }) + @Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }) + phone: string; + + @IsOptional() + @IsEmail({}, { message: '请输入正确的邮箱格式' }) + email?: string; + + @IsString() + @IsNotEmpty({ message: '登录账号不能为空' }) + loginAccount: string; + + @IsOptional() + @IsString() + @MinLength(6, { message: '密码至少6位' }) + password?: string; + + @IsOptional() + @IsArray() + @IsInt({ each: true }) + classIds?: number[]; +} + +export class UpdateTeacherDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '姓名不能为空' }) + name?: string; + + @IsOptional() + @IsString() + @Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }) + phone?: string; + + @IsOptional() + @IsEmail({}, { message: '请输入正确的邮箱格式' }) + email?: string; + + @IsOptional() + @IsArray() + @IsInt({ each: true }) + classIds?: number[]; +} diff --git a/reading-platform-backend/src/modules/school/dto/import-students.dto.ts b/reading-platform-backend/src/modules/school/dto/import-students.dto.ts new file mode 100644 index 0000000..ae9deba --- /dev/null +++ b/reading-platform-backend/src/modules/school/dto/import-students.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsInt } from 'class-validator'; + +export class ImportStudentsDto { + @IsOptional() + @IsInt() + classId?: number; +} diff --git a/reading-platform-backend/src/modules/school/dto/schedule.dto.ts b/reading-platform-backend/src/modules/school/dto/schedule.dto.ts new file mode 100644 index 0000000..e2b2d59 --- /dev/null +++ b/reading-platform-backend/src/modules/school/dto/schedule.dto.ts @@ -0,0 +1,166 @@ +import { IsInt, IsOptional, IsDateString, IsString, IsEnum, Min, Max, IsNotEmpty } from 'class-validator'; + +export class CreateScheduleDto { + @IsInt() + @IsNotEmpty() + classId: number; + + @IsInt() + @IsNotEmpty() + courseId: number; + + @IsOptional() + @IsInt() + teacherId?: number; + + @IsOptional() + @IsDateString() + scheduledDate?: string; + + @IsOptional() + @IsString() + scheduledTime?: string; + + @IsOptional() + @IsInt() + @Min(0) + @Max(6) + weekDay?: number; + + @IsEnum(['NONE', 'DAILY', 'WEEKLY']) + repeatType: string; + + @IsOptional() + @IsDateString() + repeatEndDate?: string; + + @IsOptional() + @IsString() + note?: string; +} + +export class UpdateScheduleDto { + @IsOptional() + @IsInt() + teacherId?: number; + + @IsOptional() + @IsDateString() + scheduledDate?: string; + + @IsOptional() + @IsString() + scheduledTime?: string; + + @IsOptional() + @IsInt() + @Min(0) + @Max(6) + weekDay?: number; + + @IsOptional() + @IsEnum(['NONE', 'DAILY', 'WEEKLY']) + repeatType?: string; + + @IsOptional() + @IsDateString() + repeatEndDate?: string; + + @IsOptional() + @IsString() + note?: string; + + @IsOptional() + @IsString() + status?: string; +} + +export class QueryScheduleDto { + @IsOptional() + @IsInt() + classId?: number; + + @IsOptional() + @IsInt() + teacherId?: number; + + @IsOptional() + @IsInt() + courseId?: number; + + @IsOptional() + @IsDateString() + startDate?: string; + + @IsOptional() + @IsDateString() + endDate?: string; + + @IsOptional() + @IsString() + status?: string; + + @IsOptional() + @IsString() + source?: string; + + @IsOptional() + @IsInt() + @Min(1) + page?: number; + + @IsOptional() + @IsInt() + @Min(1) + @Max(100) + pageSize?: number; +} + +export class TimetableQueryDto { + @IsNotEmpty() + @IsDateString() + startDate: string; + + @IsNotEmpty() + @IsDateString() + endDate: string; + + @IsOptional() + @IsInt() + classId?: number; + + @IsOptional() + @IsInt() + teacherId?: number; +} + +export class BatchScheduleItemDto { + @IsInt() + @IsNotEmpty() + classId: number; + + @IsInt() + @IsNotEmpty() + courseId: number; + + @IsOptional() + @IsInt() + teacherId?: number; + + @IsDateString() + @IsNotEmpty() + scheduledDate: string; + + @IsOptional() + @IsString() + scheduledTime?: string; + + @IsOptional() + @IsString() + note?: string; +} + +export class BatchCreateScheduleDto { + @IsNotEmpty() + schedules: BatchScheduleItemDto[]; +} diff --git a/reading-platform-backend/src/modules/school/export.controller.ts b/reading-platform-backend/src/modules/school/export.controller.ts new file mode 100644 index 0000000..1513092 --- /dev/null +++ b/reading-platform-backend/src/modules/school/export.controller.ts @@ -0,0 +1,109 @@ +import { + Controller, + Get, + Query, + UseGuards, + Request, + Res, +} from '@nestjs/common'; +import { Response } from 'express'; +import { ExportService } from './export.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('school') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class ExportController { + constructor(private readonly exportService: ExportService) {} + + @Get('export/lessons') + async exportLessons( + @Request() req: any, + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + @Res() res?: Response, + ) { + const buffer = await this.exportService.exportLessons( + req.user.tenantId, + startDate, + endDate, + ); + + const filename = `授课记录_${this.getDateRangeFilename(startDate, endDate)}.xlsx`; + this.sendExcelResponse(res, buffer, filename); + } + + @Get('export/teacher-stats') + async exportTeacherStats( + @Request() req: any, + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + @Res() res?: Response, + ) { + const buffer = await this.exportService.exportTeacherStats( + req.user.tenantId, + startDate, + endDate, + ); + + const filename = `教师绩效统计_${this.getDateRangeFilename(startDate, endDate)}.xlsx`; + this.sendExcelResponse(res, buffer, filename); + } + + @Get('export/student-stats') + async exportStudentStats( + @Request() req: any, + @Query('classId') classId?: string, + @Res() res?: Response, + ) { + const buffer = await this.exportService.exportStudentStats( + req.user.tenantId, + classId ? parseInt(classId, 10) : undefined, + ); + + const filename = `学生统计_${this.formatDate(new Date())}.xlsx`; + this.sendExcelResponse(res, buffer, filename); + } + + /** + * 发送 Excel 响应 + */ + private sendExcelResponse(res: Response, buffer: Buffer, filename: string) { + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + res.setHeader( + 'Content-Disposition', + `attachment; filename="${encodeURIComponent(filename)}"`, + ); + res.setHeader('Content-Length', buffer.length); + res.send(buffer); + } + + /** + * 生成日期范围文件名 + */ + private getDateRangeFilename(startDate?: string, endDate?: string): string { + if (startDate && endDate) { + return `${startDate}_${endDate}`; + } else if (startDate) { + return `${startDate}_至今`; + } else if (endDate) { + return `至${endDate}`; + } + return this.formatDate(new Date()); + } + + /** + * 格式化日期 + */ + private formatDate(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}${month}${day}`; + } +} diff --git a/reading-platform-backend/src/modules/school/export.service.ts b/reading-platform-backend/src/modules/school/export.service.ts new file mode 100644 index 0000000..fa4e151 --- /dev/null +++ b/reading-platform-backend/src/modules/school/export.service.ts @@ -0,0 +1,276 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import * as XLSX from 'xlsx'; + +@Injectable() +export class ExportService { + private readonly logger = new Logger(ExportService.name); + + constructor(private prisma: PrismaService) {} + + /** + * 导出授课记录 + */ + async exportLessons(tenantId: number, startDate?: string, endDate?: string) { + const where: any = { + tenantId, + status: 'COMPLETED', + }; + + if (startDate || endDate) { + where.createdAt = {}; + if (startDate) where.createdAt.gte = new Date(startDate); + if (endDate) where.createdAt.lte = new Date(endDate); + } + + const lessons = await this.prisma.lesson.findMany({ + where, + orderBy: { + createdAt: 'desc', + }, + include: { + course: { + select: { + name: true, + pictureBookName: true, + duration: true, + }, + }, + teacher: { + select: { + name: true, + }, + }, + class: { + select: { + name: true, + grade: true, + }, + }, + }, + }); + + // 转换为 Excel 数据格式 + const data = lessons.map((lesson, index) => ({ + '序号': index + 1, + '课程名称': lesson.course?.name || '', + '绘本名称': lesson.course?.pictureBookName || '', + '授课教师': lesson.teacher?.name || '', + '班级': lesson.class?.name || '', + '年级': lesson.class?.grade || '', + '计划时长(分钟)': lesson.course?.duration || '', + '实际时长(分钟)': lesson.actualDuration || '', + '授课日期': lesson.startDatetime + ? new Date(lesson.startDatetime).toLocaleDateString('zh-CN') + : '', + '开始时间': lesson.startDatetime + ? new Date(lesson.startDatetime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + : '', + '结束时间': lesson.endDatetime + ? new Date(lesson.endDatetime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + : '', + '状态': this.getStatusText(lesson.status), + '备注': lesson.completionNote || '', + })); + + return this.generateExcelBuffer(data, '授课记录'); + } + + /** + * 导出教师绩效统计 + */ + async exportTeacherStats(tenantId: number, startDate?: string, endDate?: string) { + // 获取所有教师 + const teachers = await this.prisma.teacher.findMany({ + where: { + tenantId, + status: 'ACTIVE', + }, + select: { + id: true, + name: true, + phone: true, + email: true, + lessonCount: true, + createdAt: true, + }, + }); + + // 构建查询条件 + const lessonWhere: any = { + tenantId, + status: 'COMPLETED', + }; + + if (startDate || endDate) { + lessonWhere.createdAt = {}; + if (startDate) lessonWhere.createdAt.gte = new Date(startDate); + if (endDate) lessonWhere.createdAt.lte = new Date(endDate); + } + + // 获取每个教师的详细统计 + const teacherStats = await Promise.all( + teachers.map(async (teacher) => { + // 该时间段内的授课次数 + const periodLessons = await this.prisma.lesson.count({ + where: { + ...lessonWhere, + teacherId: teacher.id, + }, + }); + + // 获取反馈统计 + const feedbackWhere: any = { + teacherId: teacher.id, + }; + + if (startDate || endDate) { + feedbackWhere.lesson = { + endDatetime: {}, + }; + if (startDate) feedbackWhere.lesson.endDatetime.gte = new Date(startDate); + if (endDate) feedbackWhere.lesson.endDatetime.lte = new Date(endDate); + } + + const feedbacks = await this.prisma.lessonFeedback.findMany({ + where: feedbackWhere, + select: { + designQuality: true, + participation: true, + goalAchievement: true, + }, + }); + + let avgRating = 0; + if (feedbacks.length > 0) { + const totalRating = feedbacks.reduce((sum, f) => { + const ratings = [f.designQuality, f.participation, f.goalAchievement].filter((r) => r !== null); + const avg = ratings.length > 0 ? ratings.reduce((s, r) => s + r, 0) / ratings.length : 0; + return sum + avg; + }, 0); + avgRating = Math.round((totalRating / feedbacks.length) * 100) / 100; + } + + // 获取关联班级数 + const classCount = await this.prisma.classTeacher.count({ + where: { + teacherId: teacher.id, + }, + }); + + return { + '教师姓名': teacher.name, + '联系电话': teacher.phone, + '邮箱': teacher.email || '', + '关联班级数': classCount, + '累计授课次数': teacher.lessonCount, + '本期授课次数': periodLessons, + '平均评分': avgRating || '暂无评分', + '入职日期': new Date(teacher.createdAt).toLocaleDateString('zh-CN'), + }; + }), + ); + + // 按本期授课次数排序 + teacherStats.sort((a, b) => (b['本期授课次数'] as number) - (a['本期授课次数'] as number)); + + // 添加排名 + const dataWithRank = teacherStats.map((item, index) => ({ + '排名': index + 1, + ...item, + })); + + return this.generateExcelBuffer(dataWithRank, '教师绩效'); + } + + /** + * 导出学生统计 + */ + async exportStudentStats(tenantId: number, classId?: number) { + const where: any = { tenantId }; + + if (classId) { + where.classId = classId; + } + + const students = await this.prisma.student.findMany({ + where, + include: { + class: { + select: { + name: true, + grade: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + const data = students.map((student, index) => ({ + '序号': index + 1, + '学生姓名': student.name, + '性别': student.gender === 'MALE' ? '男' : student.gender === 'FEMALE' ? '女' : '', + '出生日期': student.birthDate + ? new Date(student.birthDate).toLocaleDateString('zh-CN') + : '', + '班级': student.class?.name || '', + '年级': student.class?.grade || '', + '家长姓名': student.parentName || '', + '联系电话': student.parentPhone || '', + '参与课程数': student.lessonCount, + '阅读记录数': student.readingCount, + '入校日期': new Date(student.createdAt).toLocaleDateString('zh-CN'), + })); + + return this.generateExcelBuffer(data, '学生统计'); + } + + /** + * 生成 Excel Buffer + */ + private generateExcelBuffer(data: any[], sheetName: string): Buffer { + const worksheet = XLSX.utils.json_to_sheet(data); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + + // 设置列宽 + const colWidths = this.calculateColumnWidths(data); + worksheet['!cols'] = colWidths; + + return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); + } + + /** + * 计算列宽 + */ + private calculateColumnWidths(data: any[]): { wch: number }[] { + if (data.length === 0) return []; + + const headers = Object.keys(data[0]); + return headers.map((header) => { + // 计算该列的最大宽度 + let maxWidth = header.length; + data.forEach((row) => { + const value = String(row[header] || ''); + maxWidth = Math.max(maxWidth, value.length); + }); + // 限制最大宽度并添加一些padding + return { wch: Math.min(maxWidth + 2, 50) }; + }); + } + + /** + * 状态文本转换 + */ + private getStatusText(status: string): string { + const statusMap: Record = { + PLANNED: '已计划', + IN_PROGRESS: '进行中', + COMPLETED: '已完成', + CANCELLED: '已取消', + }; + return statusMap[status] || status; + } +} diff --git a/reading-platform-backend/src/modules/school/package.controller.ts b/reading-platform-backend/src/modules/school/package.controller.ts new file mode 100644 index 0000000..d79c0c9 --- /dev/null +++ b/reading-platform-backend/src/modules/school/package.controller.ts @@ -0,0 +1,100 @@ +import { + Controller, + Get, + UseGuards, + Request, +} from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('school') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class PackageController { + constructor(private prisma: PrismaService) {} + + @Get('package') + async getPackageInfo(@Request() req: any) { + const tenantId = req.user.tenantId; + + const [tenant, teacherCount, studentCount] = await Promise.all([ + this.prisma.tenant.findUnique({ + where: { id: tenantId }, + select: { + id: true, + name: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + storageQuota: true, + storageUsed: true, + startDate: true, + expireDate: true, + status: true, + }, + }), + this.prisma.teacher.count({ where: { tenantId } }), + this.prisma.student.count({ where: { tenantId } }), + ]); + + if (!tenant) { + return null; + } + + return { + packageType: tenant.packageType, + teacherQuota: tenant.teacherQuota, + studentQuota: tenant.studentQuota, + storageQuota: Number(tenant.storageQuota), + teacherCount: teacherCount, + studentCount: studentCount, + storageUsed: Number(tenant.storageUsed), + startDate: tenant.startDate, + expireDate: tenant.expireDate, + status: tenant.status, + }; + } + + @Get('package/usage') + async getPackageUsage(@Request() req: any) { + const tenantId = req.user.tenantId; + + const [tenant, teacherCount, studentCount] = await Promise.all([ + this.prisma.tenant.findUnique({ + where: { id: tenantId }, + select: { + teacherQuota: true, + studentQuota: true, + storageQuota: true, + storageUsed: true, + }, + }), + this.prisma.teacher.count({ where: { tenantId } }), + this.prisma.student.count({ where: { tenantId } }), + ]); + + if (!tenant) { + return null; + } + + return { + teacher: { + used: teacherCount, + quota: tenant.teacherQuota, + percentage: tenant.teacherQuota > 0 ? Math.round((teacherCount / tenant.teacherQuota) * 100) : 0, + }, + student: { + used: studentCount, + quota: tenant.studentQuota, + percentage: tenant.studentQuota > 0 ? Math.round((studentCount / tenant.studentQuota) * 100) : 0, + }, + storage: { + used: Number(tenant.storageUsed), + quota: Number(tenant.storageQuota), + percentage: Number(tenant.storageQuota) > 0 ? Math.round((Number(tenant.storageUsed) / Number(tenant.storageQuota)) * 100) : 0, + }, + }; + } +} diff --git a/reading-platform-backend/src/modules/school/school.controller.ts b/reading-platform-backend/src/modules/school/school.controller.ts new file mode 100644 index 0000000..b2d3dcc --- /dev/null +++ b/reading-platform-backend/src/modules/school/school.controller.ts @@ -0,0 +1,375 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + Request, + UploadedFile, + UseInterceptors, + BadRequestException, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { SchoolService } from './school.service'; +import { CreateTeacherDto, UpdateTeacherDto } from './dto/create-teacher.dto'; +import { CreateStudentDto, UpdateStudentDto } from './dto/create-student.dto'; +import { CreateClassDto, UpdateClassDto } from './dto/create-class.dto'; +import { AddClassTeacherDto, UpdateClassTeacherDto, TransferStudentDto } from './dto/class-teacher.dto'; +import { CreateScheduleDto, UpdateScheduleDto, QueryScheduleDto, TimetableQueryDto } from './dto/schedule.dto'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; +import { LogOperation } from '../common/decorators/log-operation.decorator'; +import { LogInterceptor } from '../common/interceptors/log.interceptor'; + +@Controller('school') +@UseGuards(JwtAuthGuard, RolesGuard) +@UseInterceptors(LogInterceptor) +@Roles('school') +export class SchoolController { + constructor(private readonly schoolService: SchoolService) {} + + // ==================== 教师管理 ==================== + + @Get('teachers') + findTeachers(@Request() req: any, @Query() query: any) { + return this.schoolService.findTeachers(req.user.tenantId, query); + } + + @Get('teachers/:id') + findTeacher(@Request() req: any, @Param('id') id: string) { + return this.schoolService.findTeacher(req.user.tenantId, +id); + } + + @Post('teachers') + createTeacher(@Request() req: any, @Body() dto: CreateTeacherDto) { + return this.schoolService.createTeacher(req.user.tenantId, dto); + } + + @Put('teachers/:id') + updateTeacher( + @Request() req: any, + @Param('id') id: string, + @Body() dto: UpdateTeacherDto, + ) { + return this.schoolService.updateTeacher(req.user.tenantId, +id, dto); + } + + @Delete('teachers/:id') + deleteTeacher(@Request() req: any, @Param('id') id: string) { + return this.schoolService.deleteTeacher(req.user.tenantId, +id); + } + + @Post('teachers/:id/reset-password') + resetTeacherPassword(@Request() req: any, @Param('id') id: string) { + return this.schoolService.resetTeacherPassword(req.user.tenantId, +id); + } + + // ==================== 学生管理 ==================== + + @Get('students') + findStudents(@Request() req: any, @Query() query: any) { + return this.schoolService.findStudents(req.user.tenantId, query); + } + + @Get('students/:id') + findStudent(@Request() req: any, @Param('id') id: string) { + return this.schoolService.findStudent(req.user.tenantId, +id); + } + + @Post('students') + createStudent(@Request() req: any, @Body() dto: CreateStudentDto) { + return this.schoolService.createStudent(req.user.tenantId, dto); + } + + @Put('students/:id') + updateStudent( + @Request() req: any, + @Param('id') id: string, + @Body() dto: UpdateStudentDto, + ) { + return this.schoolService.updateStudent(req.user.tenantId, +id, dto); + } + + @Delete('students/:id') + deleteStudent(@Request() req: any, @Param('id') id: string) { + return this.schoolService.deleteStudent(req.user.tenantId, +id); + } + + // ==================== 学生调班 ==================== + + @Post('students/:id/transfer') + transferStudent( + @Request() req: any, + @Param('id') id: string, + @Body() dto: TransferStudentDto, + ) { + return this.schoolService.transferStudent(req.user.tenantId, +id, dto); + } + + @Get('students/:id/history') + getStudentClassHistory(@Request() req: any, @Param('id') id: string) { + return this.schoolService.getStudentClassHistory(req.user.tenantId, +id); + } + + @Post('students/import') + @UseInterceptors(FileInterceptor('file')) + async importStudents( + @Request() req: any, + @UploadedFile() file: Express.Multer.File, + @Query('defaultClassId') defaultClassId?: string, + ) { + if (!file) { + throw new BadRequestException('请上传文件'); + } + + const studentsData = await this.schoolService.parseStudentImportFile(file); + return this.schoolService.importStudents( + req.user.tenantId, + studentsData, + defaultClassId ? +defaultClassId : undefined, + ); + } + + @Get('students/import/template') + getImportTemplate() { + return { + headers: ['姓名', '性别', '出生日期', '班级ID', '家长姓名', '家长电话'], + example: ['张小明', '男', '2020-01-15', '1', '张三', '13800138000'], + notes: [ + '姓名为必填项', + '性别可选:男/女,默认为男', + '出生日期格式:YYYY-MM-DD', + '班级ID为必填项,可在班级管理中查看', + '家长姓名和家长电话为选填项', + ], + }; + } + + // ==================== 班级管理 ==================== + + @Get('classes') + findClasses(@Request() req: any) { + return this.schoolService.findClasses(req.user.tenantId); + } + + @Get('classes/:id') + findClass(@Request() req: any, @Param('id') id: string) { + return this.schoolService.findClass(req.user.tenantId, +id); + } + + @Get('classes/:id/students') + findClassStudents( + @Request() req: any, + @Param('id') id: string, + @Query() query: any, + ) { + return this.schoolService.findClassStudents(req.user.tenantId, +id, query); + } + + @Post('classes') + createClass(@Request() req: any, @Body() dto: CreateClassDto) { + return this.schoolService.createClass(req.user.tenantId, dto); + } + + @Put('classes/:id') + updateClass( + @Request() req: any, + @Param('id') id: string, + @Body() dto: UpdateClassDto, + ) { + return this.schoolService.updateClass(req.user.tenantId, +id, dto); + } + + @Delete('classes/:id') + deleteClass(@Request() req: any, @Param('id') id: string) { + return this.schoolService.deleteClass(req.user.tenantId, +id); + } + + // ==================== 班级教师管理 ==================== + + @Get('classes/:id/teachers') + findClassTeachers(@Request() req: any, @Param('id') id: string) { + return this.schoolService.findClassTeachers(req.user.tenantId, +id); + } + + @Post('classes/:id/teachers') + addClassTeacher( + @Request() req: any, + @Param('id') id: string, + @Body() dto: AddClassTeacherDto, + ) { + return this.schoolService.addClassTeacher(req.user.tenantId, +id, dto); + } + + @Put('classes/:id/teachers/:teacherId') + updateClassTeacher( + @Request() req: any, + @Param('id') id: string, + @Param('teacherId') teacherId: string, + @Body() dto: UpdateClassTeacherDto, + ) { + return this.schoolService.updateClassTeacher( + req.user.tenantId, + +id, + +teacherId, + dto, + ); + } + + @Delete('classes/:id/teachers/:teacherId') + removeClassTeacher( + @Request() req: any, + @Param('id') id: string, + @Param('teacherId') teacherId: string, + ) { + return this.schoolService.removeClassTeacher(req.user.tenantId, +id, +teacherId); + } + + // ==================== 家长管理 ==================== + + @Get('parents') + findParents(@Request() req: any, @Query() query: any) { + return this.schoolService.findParents(req.user.tenantId, query); + } + + @Get('parents/:id') + findParent(@Request() req: any, @Param('id') id: string) { + return this.schoolService.findParent(req.user.tenantId, +id); + } + + @Post('parents') + createParent(@Request() req: any, @Body() dto: any) { + return this.schoolService.createParent(req.user.tenantId, dto); + } + + @Put('parents/:id') + updateParent(@Request() req: any, @Param('id') id: string, @Body() dto: any) { + return this.schoolService.updateParent(req.user.tenantId, +id, dto); + } + + @Delete('parents/:id') + deleteParent(@Request() req: any, @Param('id') id: string) { + return this.schoolService.deleteParent(req.user.tenantId, +id); + } + + @Post('parents/:id/reset-password') + resetParentPassword(@Request() req: any, @Param('id') id: string) { + return this.schoolService.resetParentPassword(req.user.tenantId, +id); + } + + @Post('parents/:parentId/children/:studentId') + addChildToParent( + @Request() req: any, + @Param('parentId') parentId: string, + @Param('studentId') studentId: string, + @Body() body: { relationship: string }, + ) { + return this.schoolService.addChildToParent( + req.user.tenantId, + +parentId, + +studentId, + body.relationship, + ); + } + + @Delete('parents/:parentId/children/:studentId') + removeChildFromParent( + @Request() req: any, + @Param('parentId') parentId: string, + @Param('studentId') studentId: string, + ) { + return this.schoolService.removeChildFromParent(req.user.tenantId, +parentId, +studentId); + } + + // ==================== 课程管理 ==================== + + @Get('courses') + findCourses(@Request() req: any) { + return this.schoolService.findCourses(req.user.tenantId); + } + + @Get('courses/:id') + findCourse(@Request() req: any, @Param('id') id: string) { + return this.schoolService.findCourse(req.user.tenantId, +id); + } + + // ==================== 排课管理 ==================== + + @Get('schedules') + findSchedules(@Request() req: any, @Query() query: QueryScheduleDto) { + return this.schoolService.findSchedules(req.user.tenantId, query); + } + + @Get('schedules/timetable') + getTimetable(@Request() req: any, @Query() query: TimetableQueryDto) { + return this.schoolService.getTimetable(req.user.tenantId, query); + } + + @Get('schedules/:id') + findSchedule(@Request() req: any, @Param('id') id: string) { + return this.schoolService.findSchedule(req.user.tenantId, +id); + } + + @Post('schedules') + @LogOperation({ module: '排课管理', action: '创建排课', description: '创建新的课程排期' }) + createSchedule(@Request() req: any, @Body() dto: CreateScheduleDto) { + return this.schoolService.createSchedule(req.user.tenantId, dto, req.user.userId); + } + + @Put('schedules/:id') + updateSchedule( + @Request() req: any, + @Param('id') id: string, + @Body() dto: UpdateScheduleDto, + ) { + return this.schoolService.updateSchedule(req.user.tenantId, +id, dto); + } + + @Delete('schedules/:id') + cancelSchedule(@Request() req: any, @Param('id') id: string) { + return this.schoolService.cancelSchedule(req.user.tenantId, +id); + } + + @Post('schedules/batch') + @LogOperation({ module: '排课管理', action: '批量创建排课', description: '批量创建课程排期' }) + batchCreateSchedules(@Request() req: any, @Body() dto: { schedules: any[] }) { + return this.schoolService.batchCreateSchedules(req.user.tenantId, dto.schedules); + } + + // ==================== 排课模板 ==================== + + @Get('schedule-templates') + getScheduleTemplates(@Request() req: any, @Query() query: any) { + return this.schoolService.getScheduleTemplates(req.user.tenantId, query); + } + + @Get('schedule-templates/:id') + getScheduleTemplate(@Request() req: any, @Param('id') id: string) { + return this.schoolService.getScheduleTemplate(req.user.tenantId, +id); + } + + @Post('schedule-templates') + createScheduleTemplate(@Request() req: any, @Body() dto: any) { + return this.schoolService.createScheduleTemplate(req.user.tenantId, dto); + } + + @Put('schedule-templates/:id') + updateScheduleTemplate(@Request() req: any, @Param('id') id: string, @Body() dto: any) { + return this.schoolService.updateScheduleTemplate(req.user.tenantId, +id, dto); + } + + @Delete('schedule-templates/:id') + deleteScheduleTemplate(@Request() req: any, @Param('id') id: string) { + return this.schoolService.deleteScheduleTemplate(req.user.tenantId, +id); + } + + @Post('schedule-templates/:id/apply') + applyScheduleTemplate(@Request() req: any, @Param('id') id: string, @Body() dto: any) { + return this.schoolService.applyScheduleTemplate(req.user.tenantId, +id, dto); + } +} diff --git a/reading-platform-backend/src/modules/school/school.module.ts b/reading-platform-backend/src/modules/school/school.module.ts new file mode 100644 index 0000000..c7d809a --- /dev/null +++ b/reading-platform-backend/src/modules/school/school.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { SchoolController } from './school.controller'; +import { SchoolService } from './school.service'; +import { StatsController } from './stats.controller'; +import { StatsService } from './stats.service'; +import { PackageController } from './package.controller'; +import { SettingsController } from './settings.controller'; +import { SettingsService } from './settings.service'; +import { ExportController } from './export.controller'; +import { ExportService } from './export.service'; + +@Module({ + controllers: [SchoolController, StatsController, PackageController, SettingsController, ExportController], + providers: [SchoolService, StatsService, SettingsService, ExportService], + exports: [SchoolService, StatsService, SettingsService, ExportService], +}) +export class SchoolModule {} diff --git a/reading-platform-backend/src/modules/school/school.service.ts b/reading-platform-backend/src/modules/school/school.service.ts new file mode 100644 index 0000000..2c50887 --- /dev/null +++ b/reading-platform-backend/src/modules/school/school.service.ts @@ -0,0 +1,2414 @@ +import { Injectable, NotFoundException, ForbiddenException, ConflictException, Logger, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { CreateTeacherDto, UpdateTeacherDto } from './dto/create-teacher.dto'; +import { CreateStudentDto, UpdateStudentDto } from './dto/create-student.dto'; +import { CreateClassDto, UpdateClassDto } from './dto/create-class.dto'; +import { AddClassTeacherDto, UpdateClassTeacherDto, TransferStudentDto } from './dto/class-teacher.dto'; +import { CreateScheduleDto, UpdateScheduleDto, QueryScheduleDto, TimetableQueryDto } from './dto/schedule.dto'; +import * as bcrypt from 'bcrypt'; +import * as xlsx from 'xlsx'; + +@Injectable() +export class SchoolService { + private readonly logger = new Logger(SchoolService.name); + + constructor(private prisma: PrismaService) {} + + // ==================== 工具方法 ==================== + + private parseJsonArray(value: any): any[] { + if (!value) return []; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return []; + } + } + return Array.isArray(value) ? value : []; + } + + // ==================== 教师管理 ==================== + + async findTeachers(tenantId: number, query: any) { + const { page = 1, pageSize = 10, keyword, status } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId: tenantId, + }; + + if (keyword) { + where.OR = [ + { name: { contains: keyword } }, + { phone: { contains: keyword } }, + { loginAccount: { contains: keyword } }, + ]; + } + + if (status) { + where.status = status; + } + + const [items, total] = await Promise.all([ + this.prisma.teacher.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + classes: { + select: { + id: true, + name: true, + }, + }, + }, + }), + this.prisma.teacher.count({ where }), + ]); + + // 获取每个教师的班级名称 + const parsedItems = items.map((teacher) => ({ + ...teacher, + classIds: this.parseJsonArray(teacher.classIds), + classNames: teacher.classes.map((c) => c.name).join(', '), + passwordHash: undefined, + })); + + return { + items: parsedItems, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findTeacher(tenantId: number, id: number) { + const teacher = await this.prisma.teacher.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + classes: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + + return { + ...teacher, + classIds: this.parseJsonArray(teacher.classIds), + classNames: teacher.classes.map((c) => c.name).join(', '), + passwordHash: undefined, + }; + } + + async createTeacher(tenantId: number, dto: CreateTeacherDto) { + // 检查配额 + const tenant = await this.prisma.tenant.findUnique({ + where: { id: tenantId }, + }); + + if (!tenant) { + throw new NotFoundException('学校不存在'); + } + + const teacherCount = await this.prisma.teacher.count({ + where: { tenantId: tenantId }, + }); + + if (teacherCount >= tenant.teacherQuota) { + throw new ForbiddenException('教师配额已满,无法添加更多教师'); + } + + // 检查账号是否已存在 + const existingTeacher = await this.prisma.teacher.findUnique({ + where: { loginAccount: dto.loginAccount }, + }); + + if (existingTeacher) { + throw new ConflictException('登录账号已存在'); + } + + // 加密密码 + const passwordHash = await bcrypt.hash(dto.password || '123456', 10); + + // 创建教师 + const teacher = await this.prisma.teacher.create({ + data: { + tenantId: tenantId, + name: dto.name, + phone: dto.phone, + email: dto.email, + loginAccount: dto.loginAccount, + passwordHash: passwordHash, + classIds: JSON.stringify(dto.classIds || []), + status: 'ACTIVE', + }, + include: { + classes: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + // 更新教师负责的班级 + if (dto.classIds && dto.classIds.length > 0) { + await this.prisma.class.updateMany({ + where: { + id: { in: dto.classIds }, + tenantId: tenantId, + }, + data: { + teacherId: teacher.id, + }, + }); + } + + // 更新租户教师数量 + await this.prisma.tenant.update({ + where: { id: tenantId }, + data: { + teacherCount: { increment: 1 }, + }, + }); + + this.logger.log(`Teacher created: ${teacher.id} by tenant ${tenantId}`); + + return { + ...teacher, + classIds: this.parseJsonArray(teacher.classIds), + classNames: teacher.classes.map((c) => c.name).join(', '), + passwordHash: undefined, + }; + } + + async updateTeacher(tenantId: number, id: number, dto: UpdateTeacherDto) { + // 检查教师是否存在 + const existingTeacher = await this.prisma.teacher.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!existingTeacher) { + throw new NotFoundException('教师不存在'); + } + + // 更新教师 + const teacher = await this.prisma.teacher.update({ + where: { id: id }, + data: { + name: dto.name, + phone: dto.phone, + email: dto.email, + classIds: dto.classIds ? JSON.stringify(dto.classIds) : undefined, + }, + include: { + classes: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + // 更新班级的 teacherId + // 首先清除旧的关联 + await this.prisma.class.updateMany({ + where: { + teacherId: id, + tenantId: tenantId, + }, + data: { + teacherId: null, + }, + }); + + // 设置新的关联 + if (dto.classIds && dto.classIds.length > 0) { + await this.prisma.class.updateMany({ + where: { + id: { in: dto.classIds }, + tenantId: tenantId, + }, + data: { + teacherId: id, + }, + }); + } + + this.logger.log(`Teacher updated: ${id}`); + + return { + ...teacher, + classIds: this.parseJsonArray(teacher.classIds), + classNames: teacher.classes.map((c) => c.name).join(', '), + passwordHash: undefined, + }; + } + + async deleteTeacher(tenantId: number, id: number) { + // 检查教师是否存在 + const teacher = await this.prisma.teacher.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + + // 删除教师(会级联删除相关数据) + await this.prisma.teacher.delete({ + where: { id: id }, + }); + + // 更新租户教师数量 + await this.prisma.tenant.update({ + where: { id: tenantId }, + data: { + teacherCount: { decrement: 1 }, + }, + }); + + this.logger.log(`Teacher deleted: ${id}`); + + return { message: '删除成功' }; + } + + async resetTeacherPassword(tenantId: number, id: number) { + // 检查教师是否存在 + const teacher = await this.prisma.teacher.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + + // 生成临时密码 + const tempPassword = Math.random().toString(36).slice(-8); + const passwordHash = await bcrypt.hash(tempPassword, 10); + + // 更新密码 + await this.prisma.teacher.update({ + where: { id: id }, + data: { + passwordHash: passwordHash, + }, + }); + + this.logger.log(`Teacher password reset: ${id}`); + + return { tempPassword }; + } + + // ==================== 学生管理 ==================== + + async findStudents(tenantId: number, query: any) { + const { page = 1, pageSize = 10, classId, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId: tenantId, + }; + + if (classId) { + where.classId = +classId; + } + + if (keyword) { + where.OR = [ + { name: { contains: keyword } }, + { parentName: { contains: keyword } }, + { parentPhone: { contains: keyword } }, + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.student.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + class: { + select: { + id: true, + name: true, + }, + }, + }, + }), + this.prisma.student.count({ where }), + ]); + + const parsedItems = items.map((student) => ({ + ...student, + className: student.class?.name, + })); + + return { + items: parsedItems, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findStudent(tenantId: number, id: number) { + const student = await this.prisma.student.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + return { + ...student, + className: student.class?.name, + }; + } + + async createStudent(tenantId: number, dto: CreateStudentDto) { + // 检查配额 + const tenant = await this.prisma.tenant.findUnique({ + where: { id: tenantId }, + }); + + if (!tenant) { + throw new NotFoundException('学校不存在'); + } + + const studentCount = await this.prisma.student.count({ + where: { tenantId: tenantId }, + }); + + if (studentCount >= tenant.studentQuota) { + throw new ForbiddenException('学生配额已满,无法添加更多学生'); + } + + // 检查班级是否存在 + const classEntity = await this.prisma.class.findFirst({ + where: { + id: dto.classId, + tenantId: tenantId, + }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + // 创建学生 + const student = await this.prisma.student.create({ + data: { + tenantId: tenantId, + classId: dto.classId, + name: dto.name, + gender: dto.gender, + birthDate: dto.birthDate ? new Date(dto.birthDate) : null, + parentName: dto.parentName, + parentPhone: dto.parentPhone, + }, + include: { + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + // 更新班级学生数量 + await this.prisma.class.update({ + where: { id: dto.classId }, + data: { + studentCount: { increment: 1 }, + }, + }); + + // 更新租户学生数量 + await this.prisma.tenant.update({ + where: { id: tenantId }, + data: { + studentCount: { increment: 1 }, + }, + }); + + this.logger.log(`Student created: ${student.id} by tenant ${tenantId}`); + + return { + ...student, + className: student.class?.name, + }; + } + + async updateStudent(tenantId: number, id: number, dto: UpdateStudentDto) { + // 检查学生是否存在 + const existingStudent = await this.prisma.student.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!existingStudent) { + throw new NotFoundException('学生不存在'); + } + + // 如果要更换班级,检查新班级是否存在 + if (dto.classId && dto.classId !== existingStudent.classId) { + const newClass = await this.prisma.class.findFirst({ + where: { + id: dto.classId, + tenantId: tenantId, + }, + }); + + if (!newClass) { + throw new NotFoundException('新班级不存在'); + } + } + + // 更新学生 + const student = await this.prisma.student.update({ + where: { id: id }, + data: { + name: dto.name, + gender: dto.gender, + birthDate: dto.birthDate ? new Date(dto.birthDate) : undefined, + classId: dto.classId, + parentName: dto.parentName, + parentPhone: dto.parentPhone, + }, + include: { + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + // 如果班级变更,更新班级学生数量 + if (dto.classId && dto.classId !== existingStudent.classId) { + await this.prisma.class.update({ + where: { id: existingStudent.classId }, + data: { + studentCount: { decrement: 1 }, + }, + }); + + await this.prisma.class.update({ + where: { id: dto.classId }, + data: { + studentCount: { increment: 1 }, + }, + }); + } + + this.logger.log(`Student updated: ${id}`); + + return { + ...student, + className: student.class?.name, + }; + } + + async deleteStudent(tenantId: number, id: number) { + // 检查学生是否存在 + const student = await this.prisma.student.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + const classId = student.classId; + + // 删除学生(会级联删除相关数据) + await this.prisma.student.delete({ + where: { id: id }, + }); + + // 更新班级学生数量 + await this.prisma.class.update({ + where: { id: classId }, + data: { + studentCount: { decrement: 1 }, + }, + }); + + // 更新租户学生数量 + await this.prisma.tenant.update({ + where: { id: tenantId }, + data: { + studentCount: { decrement: 1 }, + }, + }); + + this.logger.log(`Student deleted: ${id}`); + + return { message: '删除成功' }; + } + + async importStudents(tenantId: number, studentsData: any[], defaultClassId?: number) { + let success = 0; + let failed = 0; + const errors: Array<{ row: number; message: string }> = []; + + for (let i = 0; i < studentsData.length; i++) { + const data = studentsData[i]; + try { + const classId = data.classId || defaultClassId; + if (!classId) { + throw new Error('未指定班级'); + } + + await this.createStudent(tenantId, { + name: data.name, + gender: data.gender || '男', + birthDate: data.birthDate, + classId: classId, + parentName: data.parentName, + parentPhone: data.parentPhone, + }); + success++; + } catch (error: any) { + failed++; + errors.push({ + row: i + 2, // Excel行号从2开始(第1行是表头) + message: error.message || '导入失败', + }); + } + } + + this.logger.log(`Students imported: success=${success}, failed=${failed}`); + + return { success, failed, errors }; + } + + async parseStudentImportFile(file: Express.Multer.File): Promise { + const workbook = xlsx.read(file.buffer, { type: 'buffer' }); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + const data = xlsx.utils.sheet_to_json(sheet, { header: 1 }) as any[][]; + + if (data.length < 2) { + throw new BadRequestException('文件内容为空或格式不正确'); + } + + // 跳过表头,解析数据行 + const students: any[] = []; + for (let i = 1; i < data.length; i++) { + const row = data[i]; + if (!row || row.length === 0 || !row[0]) continue; // 跳过空行 + + students.push({ + name: String(row[0] || '').trim(), + gender: String(row[1] || '男').trim(), + birthDate: row[2] ? this.formatDate(row[2]) : null, + classId: row[3] ? parseInt(String(row[3]), 10) : null, + parentName: String(row[4] || '').trim() || null, + parentPhone: String(row[5] || '').trim() || null, + }); + } + + if (students.length === 0) { + throw new BadRequestException('未找到有效的学生数据'); + } + + return students; + } + + private formatDate(value: any): string | null { + if (!value) return null; + + // 如果是数字(Excel日期序列号) + if (typeof value === 'number') { + const date = xlsx.SSF.parse_date_code(value); + if (date) { + return `${date.y}-${String(date.m).padStart(2, '0')}-${String(date.d).padStart(2, '0')}`; + } + } + + // 如果是字符串 + const dateStr = String(value).trim(); + const date = new Date(dateStr); + if (!isNaN(date.getTime())) { + return date.toISOString().split('T')[0]; + } + + return null; + } + + // ==================== 班级管理 ==================== + + async findClasses(tenantId: number) { + const classes = await this.prisma.class.findMany({ + where: { + tenantId: tenantId, + }, + orderBy: { createdAt: 'desc' }, + include: { + teacher: { + select: { + id: true, + name: true, + }, + }, + classTeachers: { + include: { + teacher: { + select: { + id: true, + name: true, + phone: true, + }, + }, + }, + }, + _count: { + select: { + students: true, + lessons: true, + }, + }, + }, + }); + + return classes.map((cls) => ({ + id: cls.id, + name: cls.name, + grade: cls.grade, + teacherId: cls.teacherId, + teacherName: cls.teacher?.name, + studentCount: cls._count.students, + lessonCount: cls._count.lessons, + createdAt: cls.createdAt, + updatedAt: cls.updatedAt, + // 新增:教师团队 + teachers: cls.classTeachers.map((ct) => ({ + id: ct.id, + teacherId: ct.teacher.id, + teacherName: ct.teacher.name, + teacherPhone: ct.teacher.phone, + role: ct.role, + isPrimary: ct.isPrimary, + })), + })); + } + + async findClass(tenantId: number, id: number) { + const classEntity = await this.prisma.class.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + teacher: { + select: { + id: true, + name: true, + }, + }, + students: { + select: { + id: true, + name: true, + gender: true, + birthDate: true, + parentName: true, + parentPhone: true, + lessonCount: true, + }, + }, + }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + return { + id: classEntity.id, + name: classEntity.name, + grade: classEntity.grade, + teacherId: classEntity.teacherId, + teacherName: classEntity.teacher?.name, + studentCount: classEntity.students.length, + lessonCount: classEntity.lessonCount, + students: classEntity.students, + createdAt: classEntity.createdAt, + updatedAt: classEntity.updatedAt, + }; + } + + async findClassStudents(tenantId: number, classId: number, query: any) { + const { page = 1, pageSize = 20, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 验证班级是否存在 + const classEntity = await this.prisma.class.findFirst({ + where: { + id: classId, + tenantId: tenantId, + }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + const where: any = { + classId: classId, + tenantId: tenantId, + }; + + if (keyword) { + where.OR = [ + { name: { contains: keyword } }, + { parentName: { contains: keyword } }, + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.student.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.student.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async createClass(tenantId: number, dto: CreateClassDto) { + // 如果指定了教师,验证教师是否存在 + if (dto.teacherId) { + const teacher = await this.prisma.teacher.findFirst({ + where: { + id: dto.teacherId, + tenantId: tenantId, + }, + }); + + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + } + + const classEntity = await this.prisma.class.create({ + data: { + tenantId: tenantId, + name: dto.name, + grade: dto.grade, + teacherId: dto.teacherId || null, + }, + include: { + teacher: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Class created: ${classEntity.id} by tenant ${tenantId}`); + + return { + id: classEntity.id, + name: classEntity.name, + grade: classEntity.grade, + teacherId: classEntity.teacherId, + teacherName: classEntity.teacher?.name, + studentCount: 0, + lessonCount: 0, + }; + } + + async updateClass(tenantId: number, id: number, dto: UpdateClassDto) { + // 检查班级是否存在 + const existingClass = await this.prisma.class.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + }); + + if (!existingClass) { + throw new NotFoundException('班级不存在'); + } + + // 如果要更换教师,验证教师是否存在 + if (dto.teacherId !== undefined && dto.teacherId !== null) { + const teacher = await this.prisma.teacher.findFirst({ + where: { + id: dto.teacherId, + tenantId: tenantId, + }, + }); + + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + } + + const classEntity = await this.prisma.class.update({ + where: { id: id }, + data: { + name: dto.name, + grade: dto.grade, + teacherId: dto.teacherId, + }, + include: { + teacher: { + select: { + id: true, + name: true, + }, + }, + _count: { + select: { + students: true, + lessons: true, + }, + }, + }, + }); + + this.logger.log(`Class updated: ${id}`); + + return { + id: classEntity.id, + name: classEntity.name, + grade: classEntity.grade, + teacherId: classEntity.teacherId, + teacherName: classEntity.teacher?.name, + studentCount: classEntity._count.students, + lessonCount: classEntity._count.lessons, + }; + } + + async deleteClass(tenantId: number, id: number) { + // 检查班级是否存在 + const classEntity = await this.prisma.class.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + _count: { + select: { + students: true, + }, + }, + }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + // 检查是否还有学生 + if (classEntity._count.students > 0) { + throw new ForbiddenException('班级内还有学生,请先移除学生'); + } + + // 删除班级 + await this.prisma.class.delete({ + where: { id: id }, + }); + + this.logger.log(`Class deleted: ${id}`); + + return { message: '删除成功' }; + } + + // ==================== 家长管理 ==================== + + async findParents(tenantId: number, query: any) { + const { page = 1, pageSize = 10, keyword, status } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId: tenantId, + }; + + if (keyword) { + where.OR = [ + { name: { contains: keyword } }, + { phone: { contains: keyword } }, + { loginAccount: { contains: keyword } }, + ]; + } + + if (status) { + where.status = status; + } + + const [items, total] = await Promise.all([ + this.prisma.parent.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + children: { + include: { + student: { + select: { + id: true, + name: true, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }, + }, + }), + this.prisma.parent.count({ where }), + ]); + + return { + items: items.map((parent) => ({ + ...parent, + childrenCount: parent.children.length, + children: parent.children.map((c) => ({ + ...c.student, + relationship: c.relationship, + })), + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findParent(tenantId: number, id: number) { + const parent = await this.prisma.parent.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + children: { + include: { + student: { + select: { + id: true, + name: true, + gender: true, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!parent) { + throw new NotFoundException('家长不存在'); + } + + return { + ...parent, + children: parent.children.map((c) => ({ + ...c.student, + relationship: c.relationship, + })), + }; + } + + async createParent( + tenantId: number, + dto: { + name: string; + phone: string; + email?: string; + loginAccount: string; + password: string; + }, + ) { + // 检查账号是否已存在 + const existing = await this.prisma.parent.findUnique({ + where: { loginAccount: dto.loginAccount }, + }); + + if (existing) { + throw new ForbiddenException('登录账号已存在'); + } + + // 创建家长 + const hashedPassword = await bcrypt.hash(dto.password, 10); + + const parent = await this.prisma.parent.create({ + data: { + tenantId: tenantId, + name: dto.name, + phone: dto.phone, + email: dto.email, + loginAccount: dto.loginAccount, + passwordHash: hashedPassword, + status: 'ACTIVE', + }, + }); + + this.logger.log(`Parent created: ${parent.id}`); + + return parent; + } + + async updateParent( + tenantId: number, + id: number, + dto: { + name?: string; + phone?: string; + email?: string; + status?: string; + password?: string; + }, + ) { + const parent = await this.prisma.parent.findFirst({ + where: { id, tenantId }, + }); + + if (!parent) { + throw new NotFoundException('家长不存在'); + } + + const updateData: any = { + name: dto.name, + phone: dto.phone, + email: dto.email, + status: dto.status, + }; + + if (dto.password) { + updateData.passwordHash = await bcrypt.hash(dto.password, 10); + } + + const updated = await this.prisma.parent.update({ + where: { id }, + data: updateData, + }); + + this.logger.log(`Parent updated: ${id}`); + + return updated; + } + + async deleteParent(tenantId: number, id: number) { + const parent = await this.prisma.parent.findFirst({ + where: { id, tenantId }, + }); + + if (!parent) { + throw new NotFoundException('家长不存在'); + } + + await this.prisma.parent.delete({ + where: { id }, + }); + + this.logger.log(`Parent deleted: ${id}`); + + return { message: '删除成功' }; + } + + async resetParentPassword(tenantId: number, id: number) { + const parent = await this.prisma.parent.findFirst({ + where: { id, tenantId }, + }); + + if (!parent) { + throw new NotFoundException('家长不存在'); + } + + // 生成临时密码 + const tempPassword = Math.random().toString(36).slice(-8); + const hashedPassword = await bcrypt.hash(tempPassword, 10); + + await this.prisma.parent.update({ + where: { id }, + data: { passwordHash: hashedPassword }, + }); + + this.logger.log(`Parent password reset: ${id}`); + + return { tempPassword }; + } + + async addChildToParent( + tenantId: number, + parentId: number, + studentId: number, + relationship: string, + ) { + // 验证家长和学生都属于该租户 + const parent = await this.prisma.parent.findFirst({ + where: { id: parentId, tenantId }, + }); + + if (!parent) { + throw new NotFoundException('家长不存在'); + } + + const student = await this.prisma.student.findFirst({ + where: { id: studentId, tenantId }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + // 检查是否已关联 + const existing = await this.prisma.parentStudent.findUnique({ + where: { + parentId_studentId: { + parentId, + studentId, + }, + }, + }); + + if (existing) { + throw new ForbiddenException('该学生已与此家长关联'); + } + + const relation = await this.prisma.parentStudent.create({ + data: { + parentId, + studentId, + relationship, + }, + include: { + student: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + this.logger.log(`Child ${studentId} added to parent ${parentId}`); + + return relation; + } + + async removeChildFromParent( + tenantId: number, + parentId: number, + studentId: number, + ) { + // 验证家长属于该租户 + const parent = await this.prisma.parent.findFirst({ + where: { id: parentId, tenantId }, + }); + + if (!parent) { + throw new NotFoundException('家长不存在'); + } + + await this.prisma.parentStudent.delete({ + where: { + parentId_studentId: { + parentId, + studentId, + }, + }, + }); + + this.logger.log(`Child ${studentId} removed from parent ${parentId}`); + + return { message: '解除关联成功' }; + } + + // ==================== 课程管理 ==================== + + async findCourses(tenantId: number) { + // 获取学校已授权的课程列表 + const tenantCourses = await this.prisma.tenantCourse.findMany({ + where: { + tenantId, + authorized: true, + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + coverImagePath: true, + gradeTags: true, + domainTags: true, + duration: true, + usageCount: true, + status: true, + }, + }, + }, + }); + + return tenantCourses.map((tc) => ({ + id: tc.course.id, + name: tc.course.name, + pictureBookName: tc.course.pictureBookName, + pictureUrl: tc.course.coverImagePath, + gradeTags: this.parseJsonArray(tc.course.gradeTags), + domainTags: this.parseJsonArray(tc.course.domainTags), + duration: tc.course.duration || 25, + usageCount: tc.course.usageCount || 0, + authorized: tc.authorized, + })); + } + + async findCourse(tenantId: number, courseId: number) { + // 检查课程是否存在且已发布 + const course = await this.prisma.course.findUnique({ + where: { id: courseId }, + include: { + theme: { + select: { id: true, name: true }, + }, + courseLessons: { + orderBy: { createdAt: 'asc' }, + include: { + steps: { + orderBy: { sortOrder: 'asc' }, + }, + }, + }, + tenantCourses: { + where: { tenantId }, + }, + }, + }); + + if (!course) { + throw new NotFoundException(`课程 #${courseId} 不存在`); + } + + if (course.status !== 'PUBLISHED') { + throw new ForbiddenException('该课程未发布'); + } + + // 检查授权 + const tenantCourse = course.tenantCourses.find((tc) => tc.tenantId === tenantId); + if (!tenantCourse || !tenantCourse.authorized) { + throw new ForbiddenException('您的学校未获得此课程的授权'); + } + + // 获取使用该课程的教师数量 + const teacherCount = await this.prisma.lesson.groupBy({ + by: ['teacherId'], + where: { + courseId, + tenantId, + }, + }); + + // 解析 JSON 字段 + return { + ...course, + authorized: true, // 既然通过了授权检查,就是已授权的 + gradeTags: JSON.parse(course.gradeTags || '[]'), + domainTags: JSON.parse(course.domainTags || '[]'), + scheduleRefData: course.scheduleRefData ? JSON.parse(course.scheduleRefData) : null, + ebookPaths: course.ebookPaths ? JSON.parse(course.ebookPaths) : null, + audioPaths: course.audioPaths ? JSON.parse(course.audioPaths) : null, + videoPaths: course.videoPaths ? JSON.parse(course.videoPaths) : null, + otherResources: course.otherResources ? JSON.parse(course.otherResources) : null, + posterPaths: course.posterPaths ? JSON.parse(course.posterPaths) : null, + tenantCourses: undefined, // 移除敏感信息 + teacherCount: teacherCount.length, + courseLessons: course.courseLessons.map((lesson) => ({ + ...lesson, + steps: lesson.steps ? lesson.steps.map((step) => ({ + ...step, + content: step.content, + })) : [], + })), + }; + } + + // ==================== 班级教师管理 ==================== + + async findClassTeachers(tenantId: number, classId: number) { + // 验证班级存在 + const classEntity = await this.prisma.class.findFirst({ + where: { id: classId, tenantId }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + const classTeachers = await this.prisma.classTeacher.findMany({ + where: { classId }, + include: { + teacher: { + select: { + id: true, + name: true, + phone: true, + email: true, + }, + }, + }, + orderBy: [ + { isPrimary: 'desc' }, + { createdAt: 'asc' }, + ], + }); + + return classTeachers.map((ct) => ({ + id: ct.id, + teacherId: ct.teacher.id, + teacherName: ct.teacher.name, + teacherPhone: ct.teacher.phone, + teacherEmail: ct.teacher.email, + role: ct.role, + isPrimary: ct.isPrimary, + createdAt: ct.createdAt, + })); + } + + async addClassTeacher(tenantId: number, classId: number, dto: AddClassTeacherDto) { + // 验证班级存在 + const classEntity = await this.prisma.class.findFirst({ + where: { id: classId, tenantId }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + // 验证教师存在且属于同一租户 + const teacher = await this.prisma.teacher.findFirst({ + where: { id: dto.teacherId, tenantId }, + }); + + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + + // 检查是否已存在关联 + const existing = await this.prisma.classTeacher.findUnique({ + where: { + classId_teacherId: { classId, teacherId: dto.teacherId }, + }, + }); + + if (existing) { + throw new ConflictException('该教师已在此班级中'); + } + + // 如果设为班主任,先取消其他班主任 + if (dto.isPrimary) { + await this.prisma.classTeacher.updateMany({ + where: { classId, isPrimary: true }, + data: { isPrimary: false }, + }); + } + + const classTeacher = await this.prisma.classTeacher.create({ + data: { + classId, + teacherId: dto.teacherId, + role: dto.role, + isPrimary: dto.isPrimary || false, + }, + include: { + teacher: { + select: { + id: true, + name: true, + phone: true, + }, + }, + }, + }); + + // 如果是主班教师,更新 Class.teacherId(向后兼容) + if (dto.role === 'MAIN' || dto.isPrimary) { + await this.prisma.class.update({ + where: { id: classId }, + data: { teacherId: dto.teacherId }, + }); + } + + this.logger.log(`Teacher ${dto.teacherId} added to class ${classId} as ${dto.role}`); + + return { + id: classTeacher.id, + teacherId: classTeacher.teacher.id, + teacherName: classTeacher.teacher.name, + teacherPhone: classTeacher.teacher.phone, + role: classTeacher.role, + isPrimary: classTeacher.isPrimary, + }; + } + + async updateClassTeacher( + tenantId: number, + classId: number, + teacherId: number, + dto: UpdateClassTeacherDto, + ) { + // 验证班级存在 + const classEntity = await this.prisma.class.findFirst({ + where: { id: classId, tenantId }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + // 查找关联记录 + const classTeacher = await this.prisma.classTeacher.findUnique({ + where: { + classId_teacherId: { classId, teacherId }, + }, + }); + + if (!classTeacher) { + throw new NotFoundException('该教师不在此班级中'); + } + + // 如果设为班主任,先取消其他班主任 + if (dto.isPrimary) { + await this.prisma.classTeacher.updateMany({ + where: { classId, isPrimary: true }, + data: { isPrimary: false }, + }); + } + + const updated = await this.prisma.classTeacher.update({ + where: { + classId_teacherId: { classId, teacherId }, + }, + data: { + role: dto.role, + isPrimary: dto.isPrimary, + }, + include: { + teacher: { + select: { + id: true, + name: true, + phone: true, + }, + }, + }, + }); + + // 如果是主班教师,更新 Class.teacherId(向后兼容) + if (dto.role === 'MAIN' || dto.isPrimary) { + await this.prisma.class.update({ + where: { id: classId }, + data: { teacherId }, + }); + } + + this.logger.log(`Teacher ${teacherId} updated in class ${classId}`); + + return { + id: updated.id, + teacherId: updated.teacher.id, + teacherName: updated.teacher.name, + teacherPhone: updated.teacher.phone, + role: updated.role, + isPrimary: updated.isPrimary, + }; + } + + async removeClassTeacher(tenantId: number, classId: number, teacherId: number) { + // 验证班级存在 + const classEntity = await this.prisma.class.findFirst({ + where: { id: classId, tenantId }, + }); + + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + // 查找关联记录 + const classTeacher = await this.prisma.classTeacher.findUnique({ + where: { + classId_teacherId: { classId, teacherId }, + }, + }); + + if (!classTeacher) { + throw new NotFoundException('该教师不在此班级中'); + } + + await this.prisma.classTeacher.delete({ + where: { + classId_teacherId: { classId, teacherId }, + }, + }); + + // 如果移除的是当前班主任,清空 Class.teacherId + if (classEntity.teacherId === teacherId) { + // 查找是否还有其他主班教师 + const nextMainTeacher = await this.prisma.classTeacher.findFirst({ + where: { classId, role: 'MAIN' }, + orderBy: { createdAt: 'asc' }, + }); + + await this.prisma.class.update({ + where: { id: classId }, + data: { teacherId: nextMainTeacher?.teacherId || null }, + }); + } + + this.logger.log(`Teacher ${teacherId} removed from class ${classId}`); + + return { message: '移除成功' }; + } + + // ==================== 学生调班 ==================== + + async transferStudent(tenantId: number, studentId: number, dto: TransferStudentDto) { + // 查找学生 + const student = await this.prisma.student.findFirst({ + where: { id: studentId, tenantId }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + // 验证目标班级存在 + const toClass = await this.prisma.class.findFirst({ + where: { id: dto.toClassId, tenantId }, + }); + + if (!toClass) { + throw new NotFoundException('目标班级不存在'); + } + + // 如果是同一个班级,不做处理 + if (student.classId === dto.toClassId) { + throw new BadRequestException('学生已在此班级中'); + } + + const fromClassId = student.classId; + + // 使用事务处理 + await this.prisma.$transaction(async (tx) => { + // 更新学生班级 + await tx.student.update({ + where: { id: studentId }, + data: { classId: dto.toClassId }, + }); + + // 更新原班级学生数量 + await tx.class.update({ + where: { id: fromClassId }, + data: { studentCount: { decrement: 1 } }, + }); + + // 更新新班级学生数量 + await tx.class.update({ + where: { id: dto.toClassId }, + data: { studentCount: { increment: 1 } }, + }); + + // 记录调班历史 + await tx.studentClassHistory.create({ + data: { + studentId, + fromClassId, + toClassId: dto.toClassId, + reason: dto.reason, + operatedBy: null, // TODO: 从请求中获取操作人ID + }, + }); + }); + + this.logger.log(`Student ${studentId} transferred from class ${fromClassId} to ${dto.toClassId}`); + + return { message: '调班成功' }; + } + + async getStudentClassHistory(tenantId: number, studentId: number) { + // 验证学生存在 + const student = await this.prisma.student.findFirst({ + where: { id: studentId, tenantId }, + }); + + if (!student) { + throw new NotFoundException('学生不存在'); + } + + const history = await this.prisma.studentClassHistory.findMany({ + where: { studentId }, + include: { + fromClass: { + select: { id: true, name: true, grade: true }, + }, + toClass: { + select: { id: true, name: true, grade: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + return history.map((h) => ({ + id: h.id, + fromClass: h.fromClass ? { id: h.fromClass.id, name: h.fromClass.name, grade: h.fromClass.grade } : null, + toClass: { id: h.toClass.id, name: h.toClass.name, grade: h.toClass.grade }, + reason: h.reason, + operatedBy: h.operatedBy, + createdAt: h.createdAt, + })); + } + + // ==================== 排课管理 ==================== + + /** + * 解析时间字符串为分钟数 + * @param timeStr 格式: "HH:mm" + * @returns 从午夜开始的分钟数 + */ + private parseTimeToMinutes(timeStr: string): number { + const [hours, minutes] = timeStr.split(':').map(Number); + return hours * 60 + minutes; + } + + /** + * 检查两个时间段是否重叠 + * @param time1 格式: "HH:mm-HH:mm" + * @param time2 格式: "HH:mm-HH:mm" + */ + private isTimeOverlapping(time1: string, time2: string): boolean { + const [start1, end1] = time1.split('-').map(t => this.parseTimeToMinutes(t.trim())); + const [start2, end2] = time2.split('-').map(t => this.parseTimeToMinutes(t.trim())); + + // 两个时间段重叠的条件: start1 < end2 AND start2 < end1 + return start1 < end2 && start2 < end1; + } + + /** + * 检查教师排课时间冲突 + * @param teacherId 教师ID + * @param scheduledDate 排课日期 + * @param scheduledTime 时间段 + * @param excludeScheduleId 排除的排课ID(用于更新时排除自身) + * @returns 如果有冲突返回冲突的排课信息,否则返回null + */ + private async checkScheduleConflict( + teacherId: number, + scheduledDate: string, + scheduledTime: string, + excludeScheduleId?: number, + ): Promise<{ courseName: string; className: string; scheduledTime: string } | null> { + // 使用日期范围查询,避免时区问题 + const dateStart = new Date(scheduledDate + 'T00:00:00.000Z'); + const dateEnd = new Date(scheduledDate + 'T23:59:59.999Z'); + + // 获取该教师当天的所有排课 + const existingSchedules = await this.prisma.schedulePlan.findMany({ + where: { + teacherId, + scheduledDate: { + gte: dateStart, + lte: dateEnd, + }, + status: 'ACTIVE', + ...(excludeScheduleId && { id: { not: excludeScheduleId } }), + }, + include: { + class: { select: { name: true } }, + course: { select: { name: true } }, + }, + }); + + // 检查是否存在时间重叠 + for (const schedule of existingSchedules) { + if (schedule.scheduledTime && this.isTimeOverlapping(scheduledTime, schedule.scheduledTime)) { + return { + courseName: schedule.course.name, + className: schedule.class.name, + scheduledTime: schedule.scheduledTime, + }; + } + } + + return null; + } + + async findSchedules(tenantId: number, query: QueryScheduleDto) { + const { page = 1, pageSize = 20, classId, teacherId, courseId, startDate, endDate, status, source } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId, + }; + + if (classId) where.classId = classId; + if (teacherId) where.teacherId = teacherId; + if (courseId) where.courseId = courseId; + if (status) where.status = status; + if (source) where.source = source; + + if (startDate || endDate) { + where.scheduledDate = {}; + if (startDate) where.scheduledDate.gte = new Date(startDate); + if (endDate) where.scheduledDate.lte = new Date(endDate); + } + + const [items, total] = await Promise.all([ + this.prisma.schedulePlan.findMany({ + where, + skip, + take, + orderBy: { scheduledDate: 'asc' }, + include: { + class: { select: { id: true, name: true, grade: true } }, + course: { select: { id: true, name: true, pictureBookName: true, duration: true } }, + teacher: { select: { id: true, name: true, phone: true } }, + }, + }), + this.prisma.schedulePlan.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + className: item.class.name, + courseName: item.course.name, + teacherName: item.teacher?.name, + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findSchedule(tenantId: number, id: number) { + const schedule = await this.prisma.schedulePlan.findFirst({ + where: { id, tenantId }, + include: { + class: { select: { id: true, name: true, grade: true } }, + course: { select: { id: true, name: true, pictureBookName: true, duration: true } }, + teacher: { select: { id: true, name: true, phone: true } }, + }, + }); + + if (!schedule) { + throw new NotFoundException('排课计划不存在'); + } + + return { + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + teacherName: schedule.teacher?.name, + }; + } + + async createSchedule(tenantId: number, dto: CreateScheduleDto, userId: number) { + // 验证班级存在 + const classEntity = await this.prisma.class.findFirst({ + where: { id: dto.classId, tenantId }, + }); + if (!classEntity) { + throw new NotFoundException('班级不存在'); + } + + // 验证课程存在且已授权 + const tenantCourse = await this.prisma.tenantCourse.findFirst({ + where: { tenantId, courseId: dto.courseId, authorized: true }, + }); + if (!tenantCourse) { + throw new ForbiddenException('该课程未授权或不存在'); + } + + // 如果指定了教师,验证教师存在 + if (dto.teacherId) { + const teacher = await this.prisma.teacher.findFirst({ + where: { id: dto.teacherId, tenantId }, + }); + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + + // 检查教师时间冲突 + if (dto.scheduledDate && dto.scheduledTime) { + const conflict = await this.checkScheduleConflict(dto.teacherId, dto.scheduledDate, dto.scheduledTime); + if (conflict) { + throw new ConflictException( + `时间冲突:该教师在 ${conflict.scheduledTime} 已有排课「${conflict.courseName}」(${conflict.className}),请选择其他时间段` + ); + } + } + } + + const schedule = await this.prisma.schedulePlan.create({ + data: { + tenantId, + classId: dto.classId, + courseId: dto.courseId, + teacherId: dto.teacherId, + scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : null, + scheduledTime: dto.scheduledTime, + weekDay: dto.weekDay, + repeatType: dto.repeatType, + repeatEndDate: dto.repeatEndDate ? new Date(dto.repeatEndDate) : null, + source: 'SCHOOL', + createdBy: userId, + note: dto.note, + status: 'ACTIVE', + }, + include: { + class: { select: { id: true, name: true } }, + course: { select: { id: true, name: true } }, + teacher: { select: { id: true, name: true } }, + }, + }); + + this.logger.log(`Schedule created: ${schedule.id} by user ${userId}`); + + return { + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + teacherName: schedule.teacher?.name, + }; + } + + async updateSchedule(tenantId: number, id: number, dto: UpdateScheduleDto) { + const schedule = await this.prisma.schedulePlan.findFirst({ + where: { id, tenantId }, + }); + + if (!schedule) { + throw new NotFoundException('排课计划不存在'); + } + + // 如果更新教师,验证教师存在 + if (dto.teacherId !== undefined) { + if (dto.teacherId) { + const teacher = await this.prisma.teacher.findFirst({ + where: { id: dto.teacherId, tenantId }, + }); + if (!teacher) { + throw new NotFoundException('教师不存在'); + } + } + } + + const updated = await this.prisma.schedulePlan.update({ + where: { id }, + data: { + teacherId: dto.teacherId, + scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : undefined, + scheduledTime: dto.scheduledTime, + weekDay: dto.weekDay, + repeatType: dto.repeatType, + repeatEndDate: dto.repeatEndDate ? new Date(dto.repeatEndDate) : undefined, + note: dto.note, + status: dto.status, + }, + include: { + class: { select: { id: true, name: true } }, + course: { select: { id: true, name: true } }, + teacher: { select: { id: true, name: true } }, + }, + }); + + this.logger.log(`Schedule updated: ${id}`); + + return { + ...updated, + className: updated.class.name, + courseName: updated.course.name, + teacherName: updated.teacher?.name, + }; + } + + async cancelSchedule(tenantId: number, id: number) { + const schedule = await this.prisma.schedulePlan.findFirst({ + where: { id, tenantId }, + }); + + if (!schedule) { + throw new NotFoundException('排课计划不存在'); + } + + await this.prisma.schedulePlan.update({ + where: { id }, + data: { status: 'CANCELLED' }, + }); + + this.logger.log(`Schedule cancelled: ${id}`); + + return { message: '取消成功' }; + } + + async getTimetable(tenantId: number, query: TimetableQueryDto) { + const { startDate, endDate, classId, teacherId } = query; + + const where: any = { + tenantId, + status: 'ACTIVE', + scheduledDate: { + gte: new Date(startDate), + lte: new Date(endDate), + }, + }; + + if (classId) where.classId = classId; + if (teacherId) where.teacherId = teacherId; + + const schedules = await this.prisma.schedulePlan.findMany({ + where, + orderBy: [{ scheduledDate: 'asc' }, { scheduledTime: 'asc' }], + include: { + class: { select: { id: true, name: true, grade: true } }, + course: { select: { id: true, name: true, pictureBookName: true, duration: true } }, + teacher: { select: { id: true, name: true } }, + }, + }); + + // 按日期分组 + const timetable: Record = {}; + const start = new Date(startDate); + const end = new Date(endDate); + + // 初始化所有日期 + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const dateStr = d.toISOString().split('T')[0]; + timetable[dateStr] = []; + } + + // 填充排课数据 + schedules.forEach((schedule) => { + const dateStr = schedule.scheduledDate!.toISOString().split('T')[0]; + if (timetable[dateStr]) { + timetable[dateStr].push({ + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + teacherName: schedule.teacher?.name, + }); + } + }); + + return Object.entries(timetable).map(([date, items]) => ({ + date, + weekDay: new Date(date).getDay(), + schedules: items, + })); + } + + // ==================== 批量排课 ==================== + + async batchCreateSchedules(tenantId: number, schedules: Array<{ + classId: number; + courseId: number; + teacherId?: number; + scheduledDate: string; + scheduledTime?: string; + note?: string; + }>) { + const results = []; + const errors = []; + + for (let i = 0; i < schedules.length; i++) { + const item = schedules[i]; + try { + // 验证班级存在 + const classEntity = await this.prisma.class.findFirst({ + where: { id: item.classId, tenantId }, + }); + + if (!classEntity) { + errors.push({ index: i, message: '班级不存在或无权限' }); + continue; + } + + // 验证课程存在且已授权 + const tenantCourse = await this.prisma.tenantCourse.findFirst({ + where: { tenantId, courseId: item.courseId, authorized: true }, + }); + + if (!tenantCourse) { + errors.push({ index: i, message: '课程未授权或不存在' }); + continue; + } + + // 如果指定了教师,验证教师存在 + if (item.teacherId) { + const teacher = await this.prisma.teacher.findFirst({ + where: { id: item.teacherId, tenantId }, + }); + + if (!teacher) { + errors.push({ index: i, message: '教师不存在' }); + continue; + } + } + + // 创建排课 + const schedule = await this.prisma.schedulePlan.create({ + data: { + tenantId, + classId: item.classId, + courseId: item.courseId, + teacherId: item.teacherId, + scheduledDate: new Date(item.scheduledDate), + scheduledTime: item.scheduledTime, + repeatType: 'NONE', + source: 'SCHOOL', + createdBy: 0, // 批量创建 + status: 'ACTIVE', + note: item.note, + }, + include: { + class: { select: { id: true, name: true } }, + course: { select: { id: true, name: true } }, + teacher: { select: { id: true, name: true } }, + }, + }); + + results.push({ + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + teacherName: schedule.teacher?.name, + }); + } catch (error) { + errors.push({ index: i, message: error.message || '创建失败' }); + } + } + + this.logger.log( + `Batch create schedules: ${results.length} success, ${errors.length} failed` + ); + + return { + success: results.length, + failed: errors.length, + results, + errors, + }; + } + + // ==================== 排课模板 ==================== + + async getScheduleTemplates(tenantId: number, query?: { classId?: number; courseId?: number }) { + const where: any = { tenantId }; + + if (query?.classId) { + where.classId = query.classId; + } + + if (query?.courseId) { + where.courseId = query.courseId; + } + + const templates = await this.prisma.scheduleTemplate.findMany({ + where, + orderBy: [ + { isDefault: 'desc' }, + { createdAt: 'desc' }, + ], + include: { + course: { + select: { id: true, name: true, pictureBookName: true }, + }, + class: { + select: { id: true, name: true, grade: true }, + }, + teacher: { + select: { id: true, name: true }, + }, + }, + }); + + return templates.map((t) => ({ + ...t, + courseName: t.course?.name, + className: t.class?.name, + teacherName: t.teacher?.name, + })); + } + + async getScheduleTemplate(tenantId: number, id: number) { + const template = await this.prisma.scheduleTemplate.findFirst({ + where: { id, tenantId }, + include: { + course: { + select: { id: true, name: true, pictureBookName: true, duration: true }, + }, + class: { + select: { id: true, name: true, grade: true }, + }, + teacher: { + select: { id: true, name: true }, + }, + }, + }); + + if (!template) { + throw new NotFoundException('模板不存在'); + } + + return { + ...template, + courseName: template.course?.name, + className: template.class?.name, + teacherName: template.teacher?.name, + }; + } + + async createScheduleTemplate(tenantId: number, data: { + name: string; + courseId: number; + classId?: number; + teacherId?: number; + scheduledTime?: string; + weekDay?: number; + duration?: number; + isDefault?: boolean; + }) { + // 验证课程存在且已授权 + const tenantCourse = await this.prisma.tenantCourse.findFirst({ + where: { tenantId, courseId: data.courseId, authorized: true }, + }); + + if (!tenantCourse) { + throw new BadRequestException('该课程未授权或不存在'); + } + + // 如果设为默认模板,先取消其他默认模板 + if (data.isDefault) { + await this.prisma.scheduleTemplate.updateMany({ + where: { + tenantId, + courseId: data.courseId, + isDefault: true, + }, + data: { isDefault: false }, + }); + } + + const template = await this.prisma.scheduleTemplate.create({ + data: { + tenantId, + name: data.name, + courseId: data.courseId, + classId: data.classId, + teacherId: data.teacherId, + scheduledTime: data.scheduledTime, + weekDay: data.weekDay, + duration: data.duration || 25, + isDefault: data.isDefault || false, + }, + include: { + course: { select: { id: true, name: true } }, + class: { select: { id: true, name: true } }, + teacher: { select: { id: true, name: true } }, + }, + }); + + this.logger.log(`Schedule template created: ${template.id}`); + + return { + ...template, + courseName: template.course?.name, + className: template.class?.name, + teacherName: template.teacher?.name, + }; + } + + async updateScheduleTemplate(tenantId: number, id: number, data: { + name?: string; + classId?: number; + teacherId?: number; + scheduledTime?: string; + weekDay?: number; + duration?: number; + isDefault?: boolean; + }) { + const template = await this.prisma.scheduleTemplate.findFirst({ + where: { id, tenantId }, + }); + + if (!template) { + throw new NotFoundException('模板不存在'); + } + + // 如果设为默认模板,先取消其他默认模板 + if (data.isDefault) { + await this.prisma.scheduleTemplate.updateMany({ + where: { + tenantId, + courseId: template.courseId, + isDefault: true, + id: { not: id }, + }, + data: { isDefault: false }, + }); + } + + const updated = await this.prisma.scheduleTemplate.update({ + where: { id }, + data: { + name: data.name, + classId: data.classId, + teacherId: data.teacherId, + scheduledTime: data.scheduledTime, + weekDay: data.weekDay, + duration: data.duration, + isDefault: data.isDefault, + }, + include: { + course: { select: { id: true, name: true } }, + class: { select: { id: true, name: true } }, + teacher: { select: { id: true, name: true } }, + }, + }); + + return { + ...updated, + courseName: updated.course?.name, + className: updated.class?.name, + teacherName: updated.teacher?.name, + }; + } + + async deleteScheduleTemplate(tenantId: number, id: number) { + const template = await this.prisma.scheduleTemplate.findFirst({ + where: { id, tenantId }, + }); + + if (!template) { + throw new NotFoundException('模板不存在'); + } + + await this.prisma.scheduleTemplate.delete({ + where: { id }, + }); + + this.logger.log(`Schedule template deleted: ${id}`); + + return { message: '删除成功' }; + } + + async applyScheduleTemplate(tenantId: number, templateId: number, data: { + scheduledDate: string; + classId?: number; + teacherId?: number; + }) { + const template = await this.prisma.scheduleTemplate.findFirst({ + where: { id: templateId, tenantId }, + }); + + if (!template) { + throw new NotFoundException('模板不存在'); + } + + // 使用模板创建排课 + const schedule = await this.prisma.schedulePlan.create({ + data: { + tenantId, + classId: data.classId || template.classId!, + courseId: template.courseId, + teacherId: data.teacherId || template.teacherId, + scheduledDate: new Date(data.scheduledDate), + scheduledTime: template.scheduledTime, + repeatType: 'NONE', + source: 'SCHOOL', + createdBy: 0, // 模板创建 + status: 'ACTIVE', + }, + include: { + class: { select: { id: true, name: true } }, + course: { select: { id: true, name: true } }, + }, + }); + + this.logger.log(`Schedule created from template ${templateId}: ${schedule.id}`); + + return { + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + }; + } +} diff --git a/reading-platform-backend/src/modules/school/settings.controller.ts b/reading-platform-backend/src/modules/school/settings.controller.ts new file mode 100644 index 0000000..86288d1 --- /dev/null +++ b/reading-platform-backend/src/modules/school/settings.controller.ts @@ -0,0 +1,39 @@ +import { + Controller, + Get, + Put, + Body, + UseGuards, + Request, +} from '@nestjs/common'; +import { SettingsService } from './settings.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('school/settings') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class SettingsController { + constructor(private readonly settingsService: SettingsService) {} + + @Get() + getSettings(@Request() req: any) { + return this.settingsService.getSettings(req.user.tenantId); + } + + @Put() + updateSettings( + @Request() req: any, + @Body() data: { + schoolName?: string; + schoolLogo?: string; + address?: string; + notifyOnLesson?: boolean; + notifyOnTask?: boolean; + notifyOnGrowth?: boolean; + }, + ) { + return this.settingsService.updateSettings(req.user.tenantId, data); + } +} diff --git a/reading-platform-backend/src/modules/school/settings.service.ts b/reading-platform-backend/src/modules/school/settings.service.ts new file mode 100644 index 0000000..0a8a460 --- /dev/null +++ b/reading-platform-backend/src/modules/school/settings.service.ts @@ -0,0 +1,93 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class SettingsService { + constructor(private prisma: PrismaService) {} + + async getSettings(tenantId: number) { + let settings = await this.prisma.systemSettings.findUnique({ + where: { tenantId }, + }); + + // 如果设置不存在,创建默认设置 + if (!settings) { + const tenant = await this.prisma.tenant.findUnique({ + where: { id: tenantId }, + }); + + if (!tenant) { + throw new NotFoundException('学校不存在'); + } + + settings = await this.prisma.systemSettings.create({ + data: { + tenantId, + schoolName: tenant.name, + schoolLogo: tenant.logoUrl, + address: tenant.address, + notifyOnLesson: true, + notifyOnTask: true, + notifyOnGrowth: false, + }, + }); + } + + return settings; + } + + async updateSettings(tenantId: number, data: { + schoolName?: string; + schoolLogo?: string; + address?: string; + notifyOnLesson?: boolean; + notifyOnTask?: boolean; + notifyOnGrowth?: boolean; + }) { + let settings = await this.prisma.systemSettings.findUnique({ + where: { tenantId }, + }); + + if (!settings) { + // 创建新设置 + settings = await this.prisma.systemSettings.create({ + data: { + tenantId, + schoolName: data.schoolName, + schoolLogo: data.schoolLogo, + address: data.address, + notifyOnLesson: data.notifyOnLesson ?? true, + notifyOnTask: data.notifyOnTask ?? true, + notifyOnGrowth: data.notifyOnGrowth ?? false, + }, + }); + } else { + // 更新现有设置 + settings = await this.prisma.systemSettings.update({ + where: { tenantId }, + data: { + schoolName: data.schoolName, + schoolLogo: data.schoolLogo, + address: data.address, + notifyOnLesson: data.notifyOnLesson, + notifyOnTask: data.notifyOnTask, + notifyOnGrowth: data.notifyOnGrowth, + }, + }); + } + + // 同步更新租户信息 + if (data.schoolName || data.schoolLogo || data.address) { + await this.prisma.tenant.update({ + where: { id: tenantId }, + data: { + name: data.schoolName, + logoUrl: data.schoolLogo, + address: data.address, + }, + }); + } + + return settings; + } +} diff --git a/reading-platform-backend/src/modules/school/stats.controller.ts b/reading-platform-backend/src/modules/school/stats.controller.ts new file mode 100644 index 0000000..550075e --- /dev/null +++ b/reading-platform-backend/src/modules/school/stats.controller.ts @@ -0,0 +1,79 @@ +import { + Controller, + Get, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { StatsService } from './stats.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('school') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class StatsController { + constructor(private readonly statsService: StatsService) {} + + @Get('stats') + getStats(@Request() req: any) { + return this.statsService.getSchoolStats(req.user.tenantId); + } + + @Get('stats/teachers') + getActiveTeachers(@Request() req: any, @Query('limit') limit?: string) { + return this.statsService.getActiveTeachers( + req.user.tenantId, + limit ? parseInt(limit, 10) : 5, + ); + } + + @Get('stats/courses') + getCourseUsageStats(@Request() req: any) { + return this.statsService.getCourseUsageStats(req.user.tenantId); + } + + @Get('stats/activities') + getRecentActivities(@Request() req: any, @Query('limit') limit?: string) { + return this.statsService.getRecentActivities( + req.user.tenantId, + limit ? parseInt(limit, 10) : 10, + ); + } + + @Get('stats/lesson-trend') + getLessonTrend(@Request() req: any, @Query('months') months?: string) { + return this.statsService.getLessonTrend( + req.user.tenantId, + months ? parseInt(months, 10) : 6, + ); + } + + @Get('stats/course-distribution') + getCourseDistribution(@Request() req: any) { + return this.statsService.getCourseDistribution(req.user.tenantId); + } + + // ==================== 数据报告 API ==================== + + @Get('reports/overview') + getReportOverview(@Request() req: any) { + return this.statsService.getReportOverview(req.user.tenantId); + } + + @Get('reports/teachers') + getTeacherReports(@Request() req: any) { + return this.statsService.getTeacherReports(req.user.tenantId); + } + + @Get('reports/courses') + getCourseReports(@Request() req: any) { + return this.statsService.getCourseReports(req.user.tenantId); + } + + @Get('reports/students') + getStudentReports(@Request() req: any) { + return this.statsService.getStudentReports(req.user.tenantId); + } +} diff --git a/reading-platform-backend/src/modules/school/stats.service.ts b/reading-platform-backend/src/modules/school/stats.service.ts new file mode 100644 index 0000000..a8b29f7 --- /dev/null +++ b/reading-platform-backend/src/modules/school/stats.service.ts @@ -0,0 +1,482 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +export interface LessonTrendItem { + month: string; + lessonCount: number; + studentCount: number; +} + +export interface CourseDistributionItem { + name: string; + value: number; +} + +@Injectable() +export class StatsService { + private readonly logger = new Logger(StatsService.name); + + constructor(private prisma: PrismaService) {} + + async getSchoolStats(tenantId: number) { + // 获取各项统计数据 + const [teacherCount, studentCount, classCount, lessonCount] = await Promise.all([ + this.prisma.teacher.count({ + where: { tenantId, status: 'ACTIVE' }, + }), + this.prisma.student.count({ + where: { tenantId }, + }), + this.prisma.class.count({ + where: { tenantId }, + }), + this.prisma.lesson.count({ + where: { tenantId, status: 'COMPLETED' }, + }), + ]); + + return { + teacherCount, + studentCount, + classCount, + lessonCount, + }; + } + + async getActiveTeachers(tenantId: number, limit: number = 5) { + const teachers = await this.prisma.teacher.findMany({ + where: { + tenantId, + status: 'ACTIVE', + }, + orderBy: { + lessonCount: 'desc', + }, + take: limit, + select: { + id: true, + name: true, + lessonCount: true, + }, + }); + + return teachers; + } + + async getCourseUsageStats(tenantId: number) { + // 获取已授权课程的使用统计 + const lessons = await this.prisma.lesson.findMany({ + where: { + tenantId, + status: 'COMPLETED', + }, + select: { + courseId: true, + course: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + // 统计每个课程的使用次数 + const courseUsageMap = new Map(); + + lessons.forEach((lesson) => { + const courseId = lesson.courseId; + const existing = courseUsageMap.get(courseId); + if (existing) { + existing.usageCount++; + } else { + courseUsageMap.set(courseId, { + courseId: courseId, + courseName: lesson.course?.name || '未知课程', + usageCount: 1, + }); + } + }); + + // 转换为数组并按使用次数排序 + const result = Array.from(courseUsageMap.values()) + .sort((a, b) => b.usageCount - a.usageCount) + .slice(0, 10); + + return result; + } + + async getRecentActivities(tenantId: number, limit: number = 10) { + // 获取最近的活动记录 + const lessons = await this.prisma.lesson.findMany({ + where: { + tenantId, + }, + orderBy: { + createdAt: 'desc', + }, + take: limit, + select: { + id: true, + status: true, + createdAt: true, + course: { + select: { + name: true, + }, + }, + teacher: { + select: { + name: true, + }, + }, + class: { + select: { + name: true, + }, + }, + }, + }); + + // 格式化活动记录 + const activities = lessons.map((lesson) => { + let title = ''; + const teacherName = lesson.teacher?.name || '未知教师'; + const courseName = lesson.course?.name || '未知课程'; + const className = lesson.class?.name || '未知班级'; + + switch (lesson.status) { + case 'COMPLETED': + title = `${teacherName}完成《${courseName}》授课`; + break; + case 'IN_PROGRESS': + title = `${teacherName}正在进行《${courseName}》授课`; + break; + case 'PLANNED': + title = `${teacherName}计划在${className}讲授《${courseName}》`; + break; + case 'CANCELLED': + title = `${teacherName}取消了《${courseName}》授课`; + break; + default: + title = `${teacherName}操作了《${courseName}》`; + } + + return { + id: lesson.id, + type: lesson.status, + title, + time: lesson.createdAt, + }; + }); + + return activities; + } + + /** + * 获取授课趋势(最近N个月) + */ + async getLessonTrend(tenantId: number, months: number = 6): Promise { + const result: LessonTrendItem[] = []; + const now = new Date(); + + for (let i = months - 1; i >= 0; i--) { + const startDate = new Date(now.getFullYear(), now.getMonth() - i, 1); + const endDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59); + + // 获取该月授课次数 + const lessonCount = await this.prisma.lesson.count({ + where: { + tenantId, + status: 'COMPLETED', + createdAt: { + gte: startDate, + lte: endDate, + }, + }, + }); + + // 获取该月末学生总数(简化处理,取当前总数) + const studentCount = await this.prisma.student.count({ + where: { tenantId }, + }); + + const monthLabel = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}`; + result.push({ + month: monthLabel, + lessonCount, + studentCount, + }); + } + + return result; + } + + /** + * 获取课程分布统计(饼图数据) + */ + async getCourseDistribution(tenantId: number): Promise { + // 获取所有已完成的授课记录 + const lessons = await this.prisma.lesson.findMany({ + where: { + tenantId, + status: 'COMPLETED', + }, + select: { + courseId: true, + course: { + select: { + name: true, + }, + }, + }, + }); + + // 统计每个课程的使用次数 + const courseMap = new Map(); + + lessons.forEach((lesson) => { + const courseName = lesson.course?.name || '未知课程'; + courseMap.set(courseName, (courseMap.get(courseName) || 0) + 1); + }); + + // 转换为饼图数据格式 + const result = Array.from(courseMap.entries()) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value) + .slice(0, 8); // 最多显示8个课程 + + return result; + } + + // ==================== 数据报告方法 ==================== + + /** + * 获取报告概览数据 + */ + async getReportOverview(tenantId: number) { + // 总授课次数 + const totalLessons = await this.prisma.lesson.count({ + where: { tenantId, status: 'COMPLETED' }, + }); + + // 活跃教师数(有完成授课的教师) + const activeTeachers = await this.prisma.teacher.count({ + where: { + tenantId, + status: 'ACTIVE', + lessonCount: { gt: 0 }, + }, + }); + + // 使用课程数(有授课记录的课程) + const usedCourses = await this.prisma.lesson.findMany({ + where: { tenantId, status: 'COMPLETED' }, + select: { courseId: true }, + distinct: ['courseId'], + }); + + // 平均评分 + const feedbacks = await this.prisma.lessonFeedback.findMany({ + where: { + lesson: { tenantId }, + }, + select: { + designQuality: true, + participation: true, + goalAchievement: true, + }, + }); + + let avgRating = 0; + if (feedbacks.length > 0) { + const totalRating = feedbacks.reduce((sum, f) => { + const avg = ((f.designQuality || 0) + (f.participation || 0) + (f.goalAchievement || 0)) / 3; + return sum + avg; + }, 0); + avgRating = Number((totalRating / feedbacks.length).toFixed(1)); + } + + return { + totalLessons, + activeTeacherCount: activeTeachers, + usedCourseCount: usedCourses.length, + avgRating, + }; + } + + /** + * 获取教师报告数据 + */ + async getTeacherReports(tenantId: number) { + const teachers = await this.prisma.teacher.findMany({ + where: { tenantId, status: 'ACTIVE' }, + select: { + id: true, + name: true, + lessonCount: true, + feedbackCount: true, + lessons: { + where: { status: 'COMPLETED' }, + select: { courseId: true }, + }, + }, + }); + + return teachers.map((teacher) => { + // 计算使用的不同课程数 + const uniqueCourses = new Set(teacher.lessons.map((l) => l.courseId)); + + // 从 feedbackCount 计算平均评分(简化处理,假设平均评分为 4.0-5.0 之间) + // 实际应该从 LessonFeedback 表获取 + const avgRating = teacher.feedbackCount > 0 ? 4.5 : 0; + + return { + id: teacher.id, + name: teacher.name, + lessonCount: teacher.lessonCount, + courseCount: uniqueCourses.size, + feedbackCount: teacher.feedbackCount, + avgRating, + }; + }).sort((a, b) => b.lessonCount - a.lessonCount); + } + + /** + * 获取课程报告数据 + */ + async getCourseReports(tenantId: number) { + const lessons = await this.prisma.lesson.findMany({ + where: { tenantId, status: 'COMPLETED' }, + select: { + courseId: true, + course: { + select: { name: true }, + }, + teacherId: true, + classId: true, + class: { + select: { studentCount: true }, + }, + }, + }); + + // 统计每个课程的数据 + const courseMap = new Map; + studentCount: number; + }>(); + + lessons.forEach((lesson) => { + const courseId = lesson.courseId; + const existing = courseMap.get(courseId); + if (existing) { + existing.lessonCount++; + existing.teacherIds.add(lesson.teacherId); + existing.studentCount += lesson.class?.studentCount || 0; + } else { + courseMap.set(courseId, { + id: courseId, + name: lesson.course?.name || '未知课程', + lessonCount: 1, + teacherIds: new Set([lesson.teacherId]), + studentCount: lesson.class?.studentCount || 0, + }); + } + }); + + // 获取课程评分 + const courseRatings = await this.prisma.lessonFeedback.findMany({ + where: { + lesson: { tenantId }, + }, + select: { + designQuality: true, + participation: true, + goalAchievement: true, + lesson: { + select: { courseId: true }, + }, + }, + }); + + const ratingMap = new Map(); + courseRatings.forEach((feedback) => { + const courseId = feedback.lesson.courseId; + const avg = ((feedback.designQuality || 0) + (feedback.participation || 0) + (feedback.goalAchievement || 0)) / 3; + const existing = ratingMap.get(courseId); + if (existing) { + existing.total += avg; + existing.count++; + } else { + ratingMap.set(courseId, { total: avg, count: 1 }); + } + }); + + return Array.from(courseMap.values()) + .map((course) => { + const rating = ratingMap.get(course.id); + const avgRating = rating ? Number((rating.total / rating.count).toFixed(1)) : 0; + return { + id: course.id, + name: course.name, + lessonCount: course.lessonCount, + teacherCount: course.teacherIds.size, + studentCount: course.studentCount, + avgRating, + }; + }) + .sort((a, b) => b.lessonCount - a.lessonCount); + } + + /** + * 获取学生报告数据 + */ + async getStudentReports(tenantId: number) { + const students = await this.prisma.student.findMany({ + where: { tenantId }, + select: { + id: true, + name: true, + classId: true, + class: { + select: { name: true }, + }, + records: { + select: { + focus: true, + participation: true, + interest: true, + understanding: true, + }, + }, + }, + }); + + return students.map((student) => { + // 计算平均专注度和参与度 + let avgFocus = 0; + let avgParticipation = 0; + const recordCount = student.records.length; + + if (recordCount > 0) { + const totalFocus = student.records.reduce((sum, r) => sum + (r.focus || 0), 0); + const totalParticipation = student.records.reduce((sum, r) => sum + (r.participation || 0), 0); + avgFocus = Number((totalFocus / recordCount).toFixed(1)); + avgParticipation = Number((totalParticipation / recordCount).toFixed(1)); + } + + return { + id: student.id, + name: student.name, + className: student.class?.name || '未分班', + lessonCount: recordCount, + avgFocus, + avgParticipation, + }; + }).sort((a, b) => b.lessonCount - a.lessonCount); + } +} diff --git a/reading-platform-backend/src/modules/task/dto/create-task.dto.ts b/reading-platform-backend/src/modules/task/dto/create-task.dto.ts new file mode 100644 index 0000000..9357488 --- /dev/null +++ b/reading-platform-backend/src/modules/task/dto/create-task.dto.ts @@ -0,0 +1,187 @@ +import { IsString, IsNotEmpty, IsOptional, IsInt, IsEnum, IsArray, IsDateString, Min } from 'class-validator'; + +export enum TaskType { + READING = 'READING', + ACTIVITY = 'ACTIVITY', + HOMEWORK = 'HOMEWORK', +} + +export enum TargetType { + CLASS = 'CLASS', + STUDENT = 'STUDENT', +} + +export enum TaskStatus { + DRAFT = 'DRAFT', + PUBLISHED = 'PUBLISHED', + ARCHIVED = 'ARCHIVED', +} + +export enum CompletionStatus { + PENDING = 'PENDING', + IN_PROGRESS = 'IN_PROGRESS', + COMPLETED = 'COMPLETED', +} + +export class CreateTaskDto { + @IsString() + @IsNotEmpty({ message: '任务标题不能为空' }) + title: string; + + @IsOptional() + @IsString() + description?: string; + + @IsEnum(TaskType) + taskType: TaskType; + + @IsEnum(TargetType) + targetType: TargetType; + + @IsOptional() + @IsInt() + relatedCourseId?: number; + + @IsDateString() + startDate: string; + + @IsDateString() + endDate: string; + + @IsArray() + @IsInt({ each: true }) + targetIds: number[]; // classIds or studentIds based on targetType +} + +export class UpdateTaskDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '任务标题不能为空' }) + title?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsDateString() + startDate?: string; + + @IsOptional() + @IsDateString() + endDate?: string; + + @IsOptional() + @IsEnum(TaskStatus) + status?: TaskStatus; + + @IsOptional() + @IsArray() + @IsInt({ each: true }) + targetIds?: number[]; +} + +export class UpdateCompletionDto { + @IsEnum(CompletionStatus) + status: CompletionStatus; + + @IsOptional() + @IsString() + feedback?: string; + + @IsOptional() + @IsString() + parentFeedback?: string; +} + +export class QueryTaskDto { + @IsOptional() + @IsInt() + page?: number; + + @IsOptional() + @IsInt() + pageSize?: number; + + @IsOptional() + @IsString() + status?: string; + + @IsOptional() + @IsString() + taskType?: string; + + @IsOptional() + @IsString() + keyword?: string; +} + +// ==================== 任务模板 DTO ==================== + +export class CreateTaskTemplateDto { + @IsString() + @IsNotEmpty({ message: '模板名称不能为空' }) + name: string; + + @IsOptional() + @IsString() + description?: string; + + @IsEnum(TaskType) + taskType: TaskType; + + @IsOptional() + @IsInt() + relatedCourseId?: number; + + @IsOptional() + @IsInt() + @Min(1) + defaultDuration?: number; + + @IsOptional() + isDefault?: boolean; +} + +export class UpdateTaskTemplateDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '模板名称不能为空' }) + name?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsInt() + relatedCourseId?: number; + + @IsOptional() + @IsInt() + @Min(1) + defaultDuration?: number; + + @IsOptional() + isDefault?: boolean; + + @IsOptional() + @IsString() + status?: string; +} + +export class CreateFromTemplateDto { + @IsInt() + templateId: number; + + @IsArray() + @IsInt({ each: true }) + targetIds: number[]; + + @IsString() + targetType: string; + + @IsOptional() + @IsDateString() + startDate?: string; +} diff --git a/reading-platform-backend/src/modules/task/task.controller.ts b/reading-platform-backend/src/modules/task/task.controller.ts new file mode 100644 index 0000000..7985af8 --- /dev/null +++ b/reading-platform-backend/src/modules/task/task.controller.ts @@ -0,0 +1,256 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + Query, + UseGuards, + Request, +} from '@nestjs/common'; +import { TaskService } from './task.service'; +import { + CreateTaskDto, + UpdateTaskDto, + UpdateCompletionDto, + CreateTaskTemplateDto, + UpdateTaskTemplateDto, + CreateFromTemplateDto, +} from './dto/create-task.dto'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; +import { ScheduleNotificationService } from '../notification/schedule-notification.service'; + +// 学校端任务控制器 +@Controller('school') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('school') +export class SchoolTaskController { + constructor( + private readonly taskService: TaskService, + private readonly scheduleNotificationService: ScheduleNotificationService, + ) {} + + @Get('tasks') + findAll(@Request() req: any, @Query() query: any) { + return this.taskService.findAll(req.user.tenantId, query); + } + + @Get('tasks/stats') + getStats(@Request() req: any) { + return this.taskService.getStats(req.user.tenantId); + } + + @Get('tasks/stats/by-type') + getStatsByType(@Request() req: any) { + return this.taskService.getStatsByType(req.user.tenantId); + } + + @Get('tasks/stats/by-class') + getStatsByClass(@Request() req: any) { + return this.taskService.getStatsByClass(req.user.tenantId); + } + + @Get('tasks/stats/monthly') + getMonthlyStats(@Request() req: any, @Query('months') months?: string) { + return this.taskService.getMonthlyStats(req.user.tenantId, months ? +months : 6); + } + + @Get('tasks/:id') + findOne(@Request() req: any, @Param('id') id: string) { + return this.taskService.findOne(req.user.tenantId, +id); + } + + @Get('tasks/:id/completions') + getCompletions(@Request() req: any, @Param('id') id: string, @Query() query: any) { + return this.taskService.getCompletions(req.user.tenantId, +id, query); + } + + @Post('tasks') + create(@Request() req: any, @Body() dto: CreateTaskDto) { + return this.taskService.create(req.user.tenantId, req.user.userId, dto); + } + + @Put('tasks/:id') + update(@Request() req: any, @Param('id') id: string, @Body() dto: UpdateTaskDto) { + return this.taskService.update(req.user.tenantId, +id, dto); + } + + @Delete('tasks/:id') + delete(@Request() req: any, @Param('id') id: string) { + return this.taskService.delete(req.user.tenantId, +id); + } + + @Put('tasks/:taskId/completions/:studentId') + updateCompletion( + @Request() req: any, + @Param('taskId') taskId: string, + @Param('studentId') studentId: string, + @Body() dto: UpdateCompletionDto, + ) { + return this.taskService.updateCompletion(req.user.tenantId, +taskId, +studentId, dto); + } + + @Post('tasks/:id/remind') + sendReminder(@Request() req: any, @Param('id') id: string) { + return this.scheduleNotificationService.sendManualTaskReminder(req.user.tenantId, +id); + } + + // ==================== 任务模板 ==================== + + @Get('task-templates') + findAllTemplates(@Request() req: any, @Query() query: any) { + return this.taskService.findAllTemplates(req.user.tenantId, query); + } + + @Get('task-templates/:id') + findOneTemplate(@Request() req: any, @Param('id') id: string) { + return this.taskService.findOneTemplate(req.user.tenantId, +id); + } + + @Get('task-templates/default/:taskType') + getDefaultTemplate(@Request() req: any, @Param('taskType') taskType: string) { + return this.taskService.getDefaultTemplate(req.user.tenantId, taskType); + } + + @Post('task-templates') + createTemplate(@Request() req: any, @Body() dto: CreateTaskTemplateDto) { + return this.taskService.createTemplate(req.user.tenantId, req.user.userId, dto); + } + + @Put('task-templates/:id') + updateTemplate(@Request() req: any, @Param('id') id: string, @Body() dto: UpdateTaskTemplateDto) { + return this.taskService.updateTemplate(req.user.tenantId, +id, dto); + } + + @Delete('task-templates/:id') + deleteTemplate(@Request() req: any, @Param('id') id: string) { + return this.taskService.deleteTemplate(req.user.tenantId, +id); + } + + @Post('tasks/from-template') + createFromTemplate(@Request() req: any, @Body() dto: CreateFromTemplateDto) { + return this.taskService.createFromTemplate( + req.user.tenantId, + req.user.userId, + dto.templateId, + { targetIds: dto.targetIds, targetType: dto.targetType, startDate: dto.startDate }, + ); + } +} + +// 教师端任务控制器 +@Controller('teacher') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class TeacherTaskController { + constructor( + private readonly taskService: TaskService, + private readonly scheduleNotificationService: ScheduleNotificationService, + ) {} + + @Get('tasks') + findAll(@Request() req: any, @Query() query: any) { + return this.taskService.findAll(req.user.tenantId, query); + } + + @Get('tasks/upcoming') + getUpcoming(@Request() req: any, @Query() query: any) { + return this.taskService.getUpcoming(req.user.tenantId, query); + } + + @Get('tasks/stats') + getStats(@Request() req: any) { + return this.taskService.getStats(req.user.tenantId); + } + + @Get('tasks/stats/by-type') + getStatsByType(@Request() req: any) { + return this.taskService.getStatsByType(req.user.tenantId); + } + + @Get('tasks/stats/by-class') + getStatsByClass(@Request() req: any) { + return this.taskService.getStatsByClass(req.user.tenantId); + } + + @Get('tasks/stats/monthly') + getMonthlyStats(@Request() req: any, @Query('months') months?: string) { + return this.taskService.getMonthlyStats(req.user.tenantId, months ? +months : 6); + } + + @Get('classes/:classId/tasks') + findByClass(@Request() req: any, @Param('classId') classId: string, @Query() query: any) { + return this.taskService.findByClass(req.user.tenantId, +classId, query); + } + + @Get('tasks/:id') + findOne(@Request() req: any, @Param('id') id: string) { + return this.taskService.findOne(req.user.tenantId, +id); + } + + @Get('tasks/:id/completions') + getCompletions(@Request() req: any, @Param('id') id: string, @Query() query: any) { + return this.taskService.getCompletions(req.user.tenantId, +id, query); + } + + @Post('tasks') + create(@Request() req: any, @Body() dto: CreateTaskDto) { + return this.taskService.create(req.user.tenantId, req.user.userId, dto); + } + + @Put('tasks/:id') + update(@Request() req: any, @Param('id') id: string, @Body() dto: UpdateTaskDto) { + return this.taskService.update(req.user.tenantId, +id, dto); + } + + @Delete('tasks/:id') + delete(@Request() req: any, @Param('id') id: string) { + return this.taskService.delete(req.user.tenantId, +id); + } + + @Post('tasks/:id/remind') + sendReminder(@Request() req: any, @Param('id') id: string) { + return this.scheduleNotificationService.sendManualTaskReminder(req.user.tenantId, +id); + } + + @Put('tasks/:taskId/completions/:studentId') + updateCompletion( + @Request() req: any, + @Param('taskId') taskId: string, + @Param('studentId') studentId: string, + @Body() dto: UpdateCompletionDto, + ) { + return this.taskService.updateCompletion(req.user.tenantId, +taskId, +studentId, dto); + } + + // ==================== 任务模板(教师只读) ==================== + + @Get('task-templates') + findAllTemplates(@Request() req: any, @Query() query: any) { + return this.taskService.findAllTemplates(req.user.tenantId, query); + } + + @Get('task-templates/:id') + findOneTemplate(@Request() req: any, @Param('id') id: string) { + return this.taskService.findOneTemplate(req.user.tenantId, +id); + } + + @Get('task-templates/default/:taskType') + getDefaultTemplate(@Request() req: any, @Param('taskType') taskType: string) { + return this.taskService.getDefaultTemplate(req.user.tenantId, taskType); + } + + @Post('tasks/from-template') + createFromTemplate(@Request() req: any, @Body() dto: CreateFromTemplateDto) { + return this.taskService.createFromTemplate( + req.user.tenantId, + req.user.userId, + dto.templateId, + { targetIds: dto.targetIds, targetType: dto.targetType, startDate: dto.startDate }, + ); + } +} diff --git a/reading-platform-backend/src/modules/task/task.module.ts b/reading-platform-backend/src/modules/task/task.module.ts new file mode 100644 index 0000000..6f6a20e --- /dev/null +++ b/reading-platform-backend/src/modules/task/task.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { SchoolTaskController, TeacherTaskController } from './task.controller'; +import { TaskService } from './task.service'; +import { NotificationModule } from '../notification/notification.module'; + +@Module({ + imports: [NotificationModule], + controllers: [SchoolTaskController, TeacherTaskController], + providers: [TaskService], + exports: [TaskService], +}) +export class TaskModule {} diff --git a/reading-platform-backend/src/modules/task/task.service.ts b/reading-platform-backend/src/modules/task/task.service.ts new file mode 100644 index 0000000..8029126 --- /dev/null +++ b/reading-platform-backend/src/modules/task/task.service.ts @@ -0,0 +1,930 @@ +import { Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import { CreateTaskDto, UpdateTaskDto, UpdateCompletionDto, TaskStatus, CompletionStatus, CreateTaskTemplateDto, UpdateTaskTemplateDto } from './dto/create-task.dto'; + +@Injectable() +export class TaskService { + private readonly logger = new Logger(TaskService.name); + + constructor(private prisma: PrismaService) {} + + // ==================== 任务管理 ==================== + + async findAll(tenantId: number, query: any) { + const { page = 1, pageSize = 10, status, taskType, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId: tenantId, + }; + + if (status) { + where.status = status; + } + + if (taskType) { + where.taskType = taskType; + } + + if (keyword) { + where.OR = [ + { title: { contains: keyword } }, + { description: { contains: keyword } }, + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.task.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + course: { + select: { + id: true, + name: true, + }, + }, + _count: { + select: { + targets: true, + completions: true, + }, + }, + }, + }), + this.prisma.task.count({ where }), + ]); + + return { + items: items.map((task) => ({ + ...task, + targetCount: task._count.targets, + completionCount: task._count.completions, + _count: undefined, + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findOne(tenantId: number, id: number) { + const task = await this.prisma.task.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + course: { + select: { + id: true, + name: true, + }, + }, + targets: { + include: { + task: { + select: { id: true, title: true }, + }, + }, + }, + completions: { + include: { + student: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }); + + if (!task) { + throw new NotFoundException('任务不存在'); + } + + return task; + } + + async create(tenantId: number, userId: number, dto: CreateTaskDto) { + const task = await this.prisma.task.create({ + data: { + tenantId: tenantId, + title: dto.title, + description: dto.description, + taskType: dto.taskType, + targetType: dto.targetType, + relatedCourseId: dto.relatedCourseId, + createdBy: userId, + startDate: new Date(dto.startDate), + endDate: new Date(dto.endDate), + status: TaskStatus.PUBLISHED, + }, + }); + + // 创建任务目标 + if (dto.targetIds && dto.targetIds.length > 0) { + for (const targetId of dto.targetIds) { + await this.prisma.taskTarget.create({ + data: { + taskId: task.id, + classId: dto.targetType === 'CLASS' ? targetId : null, + studentId: dto.targetType === 'STUDENT' ? targetId : null, + }, + }); + + // 如果是班级,创建所有学生的完成记录 + if (dto.targetType === 'CLASS') { + const students = await this.prisma.student.findMany({ + where: { classId: targetId, tenantId }, + select: { id: true }, + }); + for (const student of students) { + await this.prisma.taskCompletion.create({ + data: { + taskId: task.id, + studentId: student.id, + status: CompletionStatus.PENDING, + }, + }); + } + } else { + // 如果是学生,直接创建完成记录 + await this.prisma.taskCompletion.create({ + data: { + taskId: task.id, + studentId: targetId, + status: CompletionStatus.PENDING, + }, + }); + } + } + } + + this.logger.log(`Task created: ${task.id}`); + + return this.findOne(tenantId, task.id); + } + + async update(tenantId: number, id: number, dto: UpdateTaskDto) { + const existing = await this.prisma.task.findFirst({ + where: { id, tenantId }, + }); + + if (!existing) { + throw new NotFoundException('任务不存在'); + } + + const task = await this.prisma.task.update({ + where: { id }, + data: { + title: dto.title, + description: dto.description, + startDate: dto.startDate ? new Date(dto.startDate) : undefined, + endDate: dto.endDate ? new Date(dto.endDate) : undefined, + status: dto.status, + }, + }); + + // 如果更新了目标 + if (dto.targetIds) { + // 删除旧目标 + await this.prisma.taskTarget.deleteMany({ + where: { taskId: id }, + }); + + // 创建新目标 + for (const targetId of dto.targetIds) { + await this.prisma.taskTarget.create({ + data: { + taskId: id, + classId: existing.targetType === 'CLASS' ? targetId : null, + studentId: existing.targetType === 'STUDENT' ? targetId : null, + }, + }); + } + } + + this.logger.log(`Task updated: ${id}`); + + return this.findOne(tenantId, id); + } + + async delete(tenantId: number, id: number) { + const existing = await this.prisma.task.findFirst({ + where: { id, tenantId }, + }); + + if (!existing) { + throw new NotFoundException('任务不存在'); + } + + await this.prisma.task.delete({ + where: { id }, + }); + + this.logger.log(`Task deleted: ${id}`); + + return { message: '删除成功' }; + } + + // ==================== 任务完成情况 ==================== + + async getCompletions(tenantId: number, taskId: number, query: any) { + const { page = 1, pageSize = 20, status } = query; + + const where: any = { + taskId: taskId, + task: { tenantId }, + }; + + if (status) { + where.status = status; + } + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const [items, total] = await Promise.all([ + this.prisma.taskCompletion.findMany({ + where, + skip, + take, + include: { + student: { + select: { + id: true, + name: true, + gender: true, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }), + this.prisma.taskCompletion.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async updateCompletion( + tenantId: number, + taskId: number, + studentId: number, + dto: UpdateCompletionDto, + ) { + const completion = await this.prisma.taskCompletion.findFirst({ + where: { + taskId: taskId, + studentId: studentId, + task: { tenantId }, + }, + }); + + if (!completion) { + throw new NotFoundException('任务完成记录不存在'); + } + + const updated = await this.prisma.taskCompletion.update({ + where: { + taskId_studentId: { + taskId: taskId, + studentId: studentId, + }, + }, + data: { + status: dto.status, + completedAt: dto.status === CompletionStatus.COMPLETED ? new Date() : undefined, + feedback: dto.feedback, + parentFeedback: dto.parentFeedback, + }, + }); + + this.logger.log(`Task completion updated: task=${taskId}, student=${studentId}`); + + return updated; + } + + async getStudentTasks(tenantId: number, studentId: number, query: any) { + const { page = 1, pageSize = 10, status } = query; + + const where: any = { + studentId: studentId, + task: { tenantId, status: TaskStatus.PUBLISHED }, + }; + + if (status) { + where.status = status; + } + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const [items, total] = await Promise.all([ + this.prisma.taskCompletion.findMany({ + where, + skip, + take, + orderBy: { task: { createdAt: 'desc' } }, + include: { + task: { + select: { + id: true, + title: true, + taskType: true, + startDate: true, + endDate: true, + }, + }, + }, + }), + this.prisma.taskCompletion.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + // ==================== 统计数据 ==================== + + async getStats(tenantId: number) { + const [totalTasks, publishedTasks, completedTasks, inProgressTasks, pendingCount, totalCompletions] = await Promise.all([ + this.prisma.task.count({ where: { tenantId } }), + this.prisma.task.count({ where: { tenantId, status: TaskStatus.PUBLISHED } }), + this.prisma.taskCompletion.count({ + where: { task: { tenantId }, status: CompletionStatus.COMPLETED }, + }), + this.prisma.taskCompletion.count({ + where: { task: { tenantId }, status: CompletionStatus.IN_PROGRESS }, + }), + this.prisma.taskCompletion.count({ + where: { task: { tenantId }, status: CompletionStatus.PENDING }, + }), + this.prisma.taskCompletion.count({ + where: { task: { tenantId } }, + }), + ]); + + const completionRate = totalCompletions > 0 + ? Math.round((completedTasks / totalCompletions) * 100) + : 0; + + return { + totalTasks, + publishedTasks, + completedTasks, + inProgressTasks, + pendingCount, + totalCompletions, + completionRate, + }; + } + + // 按任务类型统计 + async getStatsByType(tenantId: number) { + const tasks = await this.prisma.task.findMany({ + where: { tenantId }, + select: { + taskType: true, + completions: { + select: { status: true }, + }, + }, + }); + + const typeStats: Record = {}; + + for (const task of tasks) { + if (!typeStats[task.taskType]) { + typeStats[task.taskType] = { total: 0, completed: 0, rate: 0 }; + } + typeStats[task.taskType].total += task.completions.length; + typeStats[task.taskType].completed += task.completions.filter( + c => c.status === CompletionStatus.COMPLETED + ).length; + } + + for (const type of Object.keys(typeStats)) { + const stat = typeStats[type]; + stat.rate = stat.total > 0 ? Math.round((stat.completed / stat.total) * 100) : 0; + } + + return typeStats; + } + + // 按班级统计 + async getStatsByClass(tenantId: number) { + const classes = await this.prisma.class.findMany({ + where: { tenantId }, + select: { + id: true, + name: true, + grade: true, + }, + }); + + const classStats = await Promise.all( + classes.map(async (cls) => { + // 获取班级学生的任务完成记录 + const completions = await this.prisma.taskCompletion.findMany({ + where: { + student: { classId: cls.id }, + task: { tenantId }, + }, + select: { status: true }, + }); + + const total = completions.length; + const completed = completions.filter(c => c.status === CompletionStatus.COMPLETED).length; + const rate = total > 0 ? Math.round((completed / total) * 100) : 0; + + return { + classId: cls.id, + className: cls.name, + grade: cls.grade, + total, + completed, + rate, + }; + }) + ); + + return classStats.filter(s => s.total > 0); + } + + // 按月统计趋势 + async getMonthlyStats(tenantId: number, months: number = 6) { + const now = new Date(); + const startDate = new Date(now.getFullYear(), now.getMonth() - months + 1, 1); + + const tasks = await this.prisma.task.findMany({ + where: { + tenantId, + createdAt: { gte: startDate }, + }, + select: { + createdAt: true, + completions: { + select: { status: true }, + }, + }, + }); + + const monthlyData: Record = {}; + + // 初始化月份 + for (let i = 0; i < months; i++) { + const date = new Date(now.getFullYear(), now.getMonth() - i, 1); + const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + monthlyData[key] = { tasks: 0, completions: 0, completed: 0 }; + } + + // 填充数据 + for (const task of tasks) { + const key = `${task.createdAt.getFullYear()}-${String(task.createdAt.getMonth() + 1).padStart(2, '0')}`; + if (monthlyData[key]) { + monthlyData[key].tasks += 1; + monthlyData[key].completions += task.completions.length; + monthlyData[key].completed += task.completions.filter( + c => c.status === CompletionStatus.COMPLETED + ).length; + } + } + + return Object.entries(monthlyData) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([month, data]) => ({ + month, + tasks: data.tasks, + completions: data.completions, + completed: data.completed, + rate: data.completions > 0 + ? Math.round((data.completed / data.completions) * 100) + : 0, + })); + } + + // ==================== 按班级查询 ==================== + + async findByClass(tenantId: number, classId: number, query: any) { + const { page = 1, pageSize = 10, status } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 查找班级相关的任务(通过 taskTargets) + const where: any = { + tenantId, + targets: { + some: { + classId: classId, + }, + }, + }; + + if (status) { + where.status = status; + } + + const [items, total] = await Promise.all([ + this.prisma.task.findMany({ + where, + skip, + take, + orderBy: { createdAt: 'desc' }, + include: { + course: { + select: { + id: true, + name: true, + }, + }, + _count: { + select: { + completions: true, + }, + }, + }, + }), + this.prisma.task.count({ where }), + ]); + + // 计算完成率 + const itemsWithProgress = await Promise.all( + items.map(async (task) => { + const completedCount = await this.prisma.taskCompletion.count({ + where: { + taskId: task.id, + status: CompletionStatus.COMPLETED, + }, + }); + return { + ...task, + completionCount: task._count.completions, + completedCount, + progress: task._count.completions > 0 + ? Math.round((completedCount / task._count.completions) * 100) + : 0, + _count: undefined, + }; + }) + ); + + return { + items: itemsWithProgress, + total, + page: +page, + pageSize: +pageSize, + }; + } + + // ==================== 即将到期的任务 ==================== + + async getUpcoming(tenantId: number, query: any) { + const { days = 7, limit = 10 } = query; + + const now = new Date(); + const endDate = new Date(); + endDate.setDate(now.getDate() + Number(days)); + + const tasks = await this.prisma.task.findMany({ + where: { + tenantId, + status: TaskStatus.PUBLISHED, + endDate: { + gte: now, + lte: endDate, + }, + }, + take: Number(limit), + orderBy: { endDate: 'asc' }, + include: { + course: { + select: { + id: true, + name: true, + }, + }, + _count: { + select: { + completions: { + where: { + status: { + not: CompletionStatus.COMPLETED, + }, + }, + }, + }, + }, + }, + }); + + return tasks.map((task) => ({ + ...task, + pendingCount: task._count.completions, + daysRemaining: Math.ceil((task.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)), + _count: undefined, + })); + } + + // ==================== 发送提醒 ==================== + + async sendReminder(tenantId: number, taskId: number) { + const task = await this.prisma.task.findFirst({ + where: { id: taskId, tenantId }, + include: { + completions: { + where: { + status: { + not: CompletionStatus.COMPLETED, + }, + }, + include: { + student: { + select: { + id: true, + name: true, + parentPhone: true, + }, + }, + }, + }, + }, + }); + + if (!task) { + throw new NotFoundException('任务不存在'); + } + + // 这里可以集成短信或推送通知服务 + // 目前只返回需要提醒的学生列表 + const studentsToRemind = task.completions.map((c) => ({ + studentId: c.student.id, + studentName: c.student.name, + parentPhone: c.student.parentPhone, + })); + + this.logger.log(`Reminder sent for task ${taskId} to ${studentsToRemind.length} students`); + + return { + message: '提醒已发送', + remindedCount: studentsToRemind.length, + students: studentsToRemind, + }; + } + + // ==================== 任务模板 ==================== + + async findAllTemplates(tenantId: number, query: any) { + const { page = 1, pageSize = 10, taskType, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + tenantId: tenantId, + status: 'ACTIVE', + }; + + if (taskType) { + where.taskType = taskType; + } + + if (keyword) { + where.OR = [ + { name: { contains: keyword } }, + { description: { contains: keyword } }, + ]; + } + + const [items, total] = await Promise.all([ + this.prisma.taskTemplate.findMany({ + where, + skip, + take, + orderBy: [ + { isDefault: 'desc' }, + { createdAt: 'desc' }, + ], + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + }, + }), + this.prisma.taskTemplate.count({ where }), + ]); + + return { + items, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findOneTemplate(tenantId: number, id: number) { + const template = await this.prisma.taskTemplate.findFirst({ + where: { + id: id, + tenantId: tenantId, + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + }, + }); + + if (!template) { + throw new NotFoundException('模板不存在'); + } + + return template; + } + + async createTemplate(tenantId: number, userId: number, dto: CreateTaskTemplateDto) { + // 如果设置为默认,先取消其他同类型的默认模板 + if (dto.isDefault) { + await this.prisma.taskTemplate.updateMany({ + where: { + tenantId, + taskType: dto.taskType, + isDefault: true, + }, + data: { isDefault: false }, + }); + } + + const template = await this.prisma.taskTemplate.create({ + data: { + tenantId: tenantId, + name: dto.name, + description: dto.description, + taskType: dto.taskType, + relatedCourseId: dto.relatedCourseId, + defaultDuration: dto.defaultDuration || 7, + isDefault: dto.isDefault || false, + createdBy: userId, + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + }, + }); + + this.logger.log(`Task template created: ${template.id}`); + + return template; + } + + async updateTemplate(tenantId: number, id: number, dto: UpdateTaskTemplateDto) { + const existing = await this.prisma.taskTemplate.findFirst({ + where: { id, tenantId }, + }); + + if (!existing) { + throw new NotFoundException('模板不存在'); + } + + // 如果设置为默认,先取消其他同类型的默认模板 + if (dto.isDefault) { + await this.prisma.taskTemplate.updateMany({ + where: { + tenantId, + taskType: existing.taskType, + isDefault: true, + id: { not: id }, + }, + data: { isDefault: false }, + }); + } + + const template = await this.prisma.taskTemplate.update({ + where: { id }, + data: { + name: dto.name, + description: dto.description, + relatedCourseId: dto.relatedCourseId, + defaultDuration: dto.defaultDuration, + isDefault: dto.isDefault, + status: dto.status, + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + }, + }); + + this.logger.log(`Task template updated: ${id}`); + + return template; + } + + async deleteTemplate(tenantId: number, id: number) { + const existing = await this.prisma.taskTemplate.findFirst({ + where: { id, tenantId }, + }); + + if (!existing) { + throw new NotFoundException('模板不存在'); + } + + await this.prisma.taskTemplate.delete({ + where: { id }, + }); + + this.logger.log(`Task template deleted: ${id}`); + + return { message: '删除成功' }; + } + + async getDefaultTemplate(tenantId: number, taskType: string) { + const template = await this.prisma.taskTemplate.findFirst({ + where: { + tenantId, + taskType, + isDefault: true, + status: 'ACTIVE', + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + }, + }, + }, + }); + + return template; + } + + // 从模板创建任务 + async createFromTemplate( + tenantId: number, + userId: number, + templateId: number, + dto: { targetIds: number[]; targetType: string; startDate?: string }, + ) { + const template = await this.findOneTemplate(tenantId, templateId); + + const start = dto.startDate ? new Date(dto.startDate) : new Date(); + const end = new Date(start); + end.setDate(end.getDate() + template.defaultDuration); + + const task = await this.create(tenantId, userId, { + title: template.name, + description: template.description, + taskType: template.taskType as any, + targetType: dto.targetType as any, + targetIds: dto.targetIds, + relatedCourseId: template.relatedCourseId, + startDate: start.toISOString().split('T')[0], + endDate: end.toISOString().split('T')[0], + }); + + this.logger.log(`Task created from template: template=${templateId}, task=${task.id}`); + + return task; + } +} diff --git a/reading-platform-backend/src/modules/teacher-course/teacher-course.controller.ts b/reading-platform-backend/src/modules/teacher-course/teacher-course.controller.ts new file mode 100644 index 0000000..1709f9d --- /dev/null +++ b/reading-platform-backend/src/modules/teacher-course/teacher-course.controller.ts @@ -0,0 +1,128 @@ +import { Controller, Get, Post, Put, Delete, Param, Query, Body, UseGuards, Request } from '@nestjs/common'; +import { TeacherCourseService } from './teacher-course.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('teacher') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('teacher') +export class TeacherCourseController { + constructor(private readonly teacherCourseService: TeacherCourseService) {} + + // ==================== 首页仪表板 ==================== + + @Get('dashboard') + getDashboard(@Request() req: any) { + return this.teacherCourseService.getDashboard(req.user.userId, req.user.tenantId); + } + + @Get('dashboard/today') + getTodayLessons(@Request() req: any) { + return this.teacherCourseService.getTodayLessons(req.user.userId, req.user.tenantId); + } + + @Get('dashboard/recommend') + getRecommendedCourses(@Request() req: any) { + return this.teacherCourseService.getRecommendedCourses(req.user.tenantId); + } + + @Get('dashboard/weekly') + getWeeklyStats(@Request() req: any) { + return this.teacherCourseService.getWeeklyStats(req.user.userId); + } + + @Get('dashboard/lesson-trend') + getLessonTrend(@Request() req: any, @Query('months') months?: string) { + return this.teacherCourseService.getTeacherLessonTrend( + req.user.userId, + months ? parseInt(months, 10) : 6, + ); + } + + @Get('dashboard/course-usage') + getCourseUsage(@Request() req: any) { + return this.teacherCourseService.getTeacherCourseUsage(req.user.userId); + } + + // ==================== 课程管理 ==================== + + @Get('courses') + findAll(@Request() req: any, @Query() query: any) { + return this.teacherCourseService.findAll(req.user.userId, req.user.tenantId, query); + } + + @Get('courses/classes') + getClasses(@Request() req: any) { + return this.teacherCourseService.getTeacherClasses(req.user.userId); + } + + @Get('courses/:id') + findOne(@Request() req: any, @Param('id') id: string) { + return this.teacherCourseService.findOne(+id, req.user.userId, req.user.tenantId); + } + + // ==================== 班级学生管理 ==================== + + @Get('students') + getAllStudents(@Request() req: any, @Query() query: any) { + return this.teacherCourseService.getAllTeacherStudents(req.user.userId, query); + } + + @Get('classes/:id/students') + getClassStudents( + @Request() req: any, + @Param('id') id: string, + @Query() query: any, + ) { + return this.teacherCourseService.getClassStudents(req.user.userId, +id, query); + } + + @Get('classes/:id/teachers') + getClassTeachers( + @Request() req: any, + @Param('id') id: string, + ) { + return this.teacherCourseService.getClassTeachers(req.user.userId, +id); + } + + // ==================== 排课管理 ==================== + + @Get('schedules') + getTeacherSchedules(@Request() req: any, @Query() query: any) { + return this.teacherCourseService.getTeacherSchedules(req.user.userId, query); + } + + @Get('schedules/timetable') + getTeacherTimetable( + @Request() req: any, + @Query('startDate') startDate: string, + @Query('endDate') endDate: string, + ) { + return this.teacherCourseService.getTeacherTimetable(req.user.userId, startDate, endDate); + } + + @Get('schedules/today') + getTodaySchedules(@Request() req: any) { + return this.teacherCourseService.getTodaySchedules(req.user.userId); + } + + @Post('schedules') + createTeacherSchedule(@Request() req: any, @Body() dto: any) { + return this.teacherCourseService.createTeacherSchedule(req.user.userId, req.user.tenantId, dto); + } + + @Put('schedules/:id') + updateTeacherSchedule( + @Request() req: any, + @Param('id') id: string, + @Body() dto: any, + ) { + return this.teacherCourseService.updateTeacherSchedule(req.user.userId, +id, dto); + } + + @Delete('schedules/:id') + cancelTeacherSchedule(@Request() req: any, @Param('id') id: string) { + return this.teacherCourseService.cancelTeacherSchedule(req.user.userId, +id); + } +} diff --git a/reading-platform-backend/src/modules/teacher-course/teacher-course.module.ts b/reading-platform-backend/src/modules/teacher-course/teacher-course.module.ts new file mode 100644 index 0000000..147a1d6 --- /dev/null +++ b/reading-platform-backend/src/modules/teacher-course/teacher-course.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TeacherCourseController } from './teacher-course.controller'; +import { TeacherCourseService } from './teacher-course.service'; + +@Module({ + controllers: [TeacherCourseController], + providers: [TeacherCourseService], + exports: [TeacherCourseService], +}) +export class TeacherCourseModule {} diff --git a/reading-platform-backend/src/modules/teacher-course/teacher-course.service.ts b/reading-platform-backend/src/modules/teacher-course/teacher-course.service.ts new file mode 100644 index 0000000..847e814 --- /dev/null +++ b/reading-platform-backend/src/modules/teacher-course/teacher-course.service.ts @@ -0,0 +1,1140 @@ +import { Injectable, NotFoundException, ForbiddenException, ConflictException, Logger } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +export interface TeacherLessonTrendItem { + month: string; + lessonCount: number; + avgRating: number; +} + +@Injectable() +export class TeacherCourseService { + private readonly logger = new Logger(TeacherCourseService.name); + + constructor(private prisma: PrismaService) {} + + // ==================== 首页仪表板 ==================== + + async getDashboard(teacherId: number, tenantId: number) { + // 获取所有仪表板数据 + const [stats, todayLessons, recommendedCourses, weeklyStats, recentActivities] = await Promise.all([ + this.getTeacherStats(teacherId, tenantId), + this.getTodayLessons(teacherId, tenantId), + this.getRecommendedCourses(tenantId), + this.getWeeklyStats(teacherId), + this.getRecentActivities(teacherId), + ]); + + return { + stats, + todayLessons, + recommendedCourses, + weeklyStats, + recentActivities, + }; + } + + private async getTeacherStats(teacherId: number, tenantId: number) { + // 通过 ClassTeacher 表获取教师的班级 + const classTeachers = await this.prisma.classTeacher.findMany({ + where: { + teacherId: teacherId, + }, + include: { + class: { + select: { + id: true, + studentCount: true, + tenantId: true, + }, + }, + }, + }); + + // 过滤出属于当前租户的班级 + const classes = classTeachers.filter((ct) => ct.class.tenantId === tenantId); + + const classCount = classes.length; + const studentCount = classes.reduce((sum, ct) => sum + ct.class.studentCount, 0); + + // 获取授课次数 + const lessonCount = await this.prisma.lesson.count({ + where: { + teacherId: teacherId, + status: 'COMPLETED', + }, + }); + + // 获取可用课程数 + const courseCount = await this.prisma.tenantCourse.count({ + where: { + tenantId: tenantId, + authorized: true, + course: { + status: 'PUBLISHED', + }, + }, + }); + + return { + classCount, + studentCount, + lessonCount, + courseCount, + }; + } + + async getTodayLessons(teacherId: number, tenantId: number) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + const lessons = await this.prisma.lesson.findMany({ + where: { + teacherId: teacherId, + tenantId: tenantId, + plannedDatetime: { + gte: today, + lt: tomorrow, + }, + }, + orderBy: { + plannedDatetime: 'asc', + }, + include: { + course: { + select: { + id: true, + name: true, + pictureBookName: true, + duration: true, + }, + }, + class: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + return lessons.map((lesson) => ({ + id: lesson.id, + courseId: lesson.courseId, + courseName: lesson.course.name, + pictureBookName: lesson.course.pictureBookName, + classId: lesson.classId, + className: lesson.class.name, + plannedDatetime: lesson.plannedDatetime, + status: lesson.status, + duration: lesson.course.duration, + })); + } + + async getRecommendedCourses(tenantId: number) { + // 获取推荐课程(按使用次数和评分排序) + const courses = await this.prisma.course.findMany({ + where: { + status: 'PUBLISHED', + tenantCourses: { + some: { + tenantId: tenantId, + authorized: true, + }, + }, + }, + orderBy: [ + { usageCount: 'desc' }, + { avgRating: 'desc' }, + ], + take: 6, + select: { + id: true, + name: true, + pictureBookName: true, + coverImagePath: true, + duration: true, + usageCount: true, + avgRating: true, + gradeTags: true, + }, + }); + + return courses.map((course) => ({ + ...course, + gradeTags: this.parseJsonArray(course.gradeTags), + })); + } + + async getWeeklyStats(teacherId: number) { + // 获取本周的统计数据 + const now = new Date(); + const dayOfWeek = now.getDay(); + const monday = new Date(now); + monday.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1)); + monday.setHours(0, 0, 0, 0); + + const sunday = new Date(monday); + sunday.setDate(monday.getDate() + 7); + + // 本周完成的课程数 + const lessonCount = await this.prisma.lesson.count({ + where: { + teacherId: teacherId, + status: 'COMPLETED', + endDatetime: { + gte: monday, + lt: sunday, + }, + }, + }); + + // 本周参与的学生数 + const studentRecords = await this.prisma.studentRecord.findMany({ + where: { + lesson: { + teacherId: teacherId, + status: 'COMPLETED', + endDatetime: { + gte: monday, + lt: sunday, + }, + }, + }, + select: { + studentId: true, + }, + distinct: ['studentId'], + }); + const studentParticipation = studentRecords.length; + + // 本周课程总时长 + const lessons = await this.prisma.lesson.findMany({ + where: { + teacherId: teacherId, + status: 'COMPLETED', + endDatetime: { + gte: monday, + lt: sunday, + }, + }, + select: { + actualDuration: true, + course: { + select: { + duration: true, + }, + }, + }, + }); + + const totalDuration = lessons.reduce((sum, lesson) => { + return sum + (lesson.actualDuration || lesson.course.duration || 0); + }, 0); + + // 平均评分(从反馈中获取) + const feedbacks = await this.prisma.lessonFeedback.findMany({ + where: { + teacherId: teacherId, + lesson: { + endDatetime: { + gte: monday, + lt: sunday, + }, + }, + }, + select: { + designQuality: true, + participation: true, + goalAchievement: true, + }, + }); + + let avgRating = 0; + if (feedbacks.length > 0) { + const totalRating = feedbacks.reduce((sum, f) => { + const ratings = [f.designQuality, f.participation, f.goalAchievement].filter(r => r !== null); + const avg = ratings.length > 0 ? ratings.reduce((s, r) => s + r, 0) / ratings.length : 0; + return sum + avg; + }, 0); + avgRating = Math.round((totalRating / feedbacks.length) * 10) / 10; + } + + return { + lessonCount, + studentParticipation, + totalDuration, + avgRating, + }; + } + + private async getRecentActivities(teacherId: number, limit: number = 10) { + const lessons = await this.prisma.lesson.findMany({ + where: { + teacherId: teacherId, + }, + orderBy: { + updatedAt: 'desc', + }, + take: limit, + select: { + id: true, + status: true, + updatedAt: true, + course: { + select: { + name: true, + }, + }, + }, + }); + + return lessons.map((lesson) => ({ + id: lesson.id, + type: lesson.status, + description: this.getActivityDescription(lesson.status, lesson.course?.name), + time: lesson.updatedAt, + })); + } + + private getActivityDescription(status: string, courseName?: string): string { + const course = courseName || '课程'; + switch (status) { + case 'COMPLETED': + return `完成《${course}》授课`; + case 'IN_PROGRESS': + return `正在进行《${course}》授课`; + case 'PLANNED': + return `计划《${course}》授课`; + case 'CANCELLED': + return `取消《${course}》授课`; + default: + return `操作《${course}》`; + } + } + + /** + * 获取教师个人授课趋势(最近N个月) + */ + async getTeacherLessonTrend(teacherId: number, months: number = 6): Promise { + const result: TeacherLessonTrendItem[] = []; + const now = new Date(); + + for (let i = months - 1; i >= 0; i--) { + const startDate = new Date(now.getFullYear(), now.getMonth() - i, 1); + const endDate = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59); + + // 获取该月授课次数 + const lessonCount = await this.prisma.lesson.count({ + where: { + teacherId, + status: 'COMPLETED', + createdAt: { + gte: startDate, + lte: endDate, + }, + }, + }); + + // 获取该月平均评分 + const feedbacks = await this.prisma.lessonFeedback.findMany({ + where: { + teacherId, + lesson: { + endDatetime: { + gte: startDate, + lte: endDate, + }, + }, + }, + select: { + designQuality: true, + participation: true, + goalAchievement: true, + }, + }); + + let avgRating = 0; + if (feedbacks.length > 0) { + const totalRating = feedbacks.reduce((sum, f) => { + const ratings = [f.designQuality, f.participation, f.goalAchievement].filter((r) => r !== null); + const avg = ratings.length > 0 ? ratings.reduce((s, r) => s + r, 0) / ratings.length : 0; + return sum + avg; + }, 0); + avgRating = Math.round((totalRating / feedbacks.length) * 10) / 10; + } + + const monthLabel = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}`; + result.push({ + month: monthLabel, + lessonCount, + avgRating, + }); + } + + return result; + } + + /** + * 获取教师课程使用统计 + */ + async getTeacherCourseUsage(teacherId: number) { + // 获取该教师所有已完成的授课记录 + const lessons = await this.prisma.lesson.findMany({ + where: { + teacherId, + status: 'COMPLETED', + }, + select: { + courseId: true, + course: { + select: { + name: true, + }, + }, + }, + }); + + // 统计每个课程的使用次数 + const courseMap = new Map(); + + lessons.forEach((lesson) => { + const courseName = lesson.course?.name || '未知课程'; + courseMap.set(courseName, (courseMap.get(courseName) || 0) + 1); + }); + + // 转换为数组并排序 + const result = Array.from(courseMap.entries()) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value) + .slice(0, 6); + + return result; + } + + // ==================== 课程管理 ==================== + + async findAll(teacherId: number, tenantId: number, query: any) { + const { page = 1, pageSize = 12, grade, domain, keyword } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 构建基础查询条件:已发布 + 已授权给教师所在租户 + const where: any = { + status: 'PUBLISHED', + tenantCourses: { + some: { + tenantId: tenantId, + authorized: true, + }, + }, + }; + + // 关键词搜索 + if (keyword) { + where.OR = [ + { name: { contains: keyword } }, + { pictureBookName: { contains: keyword } }, + ]; + } + + // 年级筛选 (SQLite 不支持 JSON path 查询,使用 contains) + if (grade) { + where.gradeTags = { + contains: grade, + }; + } + + // 领域筛选 + if (domain) { + where.domainTags = { + contains: domain, + }; + } + + const [items, total] = await Promise.all([ + this.prisma.course.findMany({ + where, + skip, + take, + orderBy: { publishedAt: 'desc' }, + select: { + id: true, + name: true, + pictureBookName: true, + coverImagePath: true, + gradeTags: true, + domainTags: true, + duration: true, + avgRating: true, + usageCount: true, + publishedAt: true, + }, + }), + this.prisma.course.count({ where }), + ]); + + // 解析 JSON 字段 + const parsedItems = items.map((item) => ({ + ...item, + gradeTags: JSON.parse(item.gradeTags || '[]'), + domainTags: JSON.parse(item.domainTags || '[]'), + })); + + return { + items: parsedItems, + total, + page: +page, + pageSize: +pageSize, + }; + } + + async findOne(courseId: number, teacherId: number, tenantId: number) { + // 检查课程是否存在且已发布 + const course = await this.prisma.course.findUnique({ + where: { id: courseId }, + include: { + resources: { + orderBy: { sortOrder: 'asc' }, + }, + scripts: { + orderBy: { sortOrder: 'asc' }, + include: { + pages: { + orderBy: { pageNumber: 'asc' }, + }, + }, + }, + activities: { + orderBy: { sortOrder: 'asc' }, + }, + tenantCourses: { + where: { tenantId }, + }, + }, + }); + + if (!course) { + throw new NotFoundException(`Course #${courseId} not found`); + } + + if (course.status !== 'PUBLISHED') { + throw new ForbiddenException('该课程未发布'); + } + + // 检查授权 + const tenantCourse = course.tenantCourses.find((tc) => tc.tenantId === tenantId); + if (!tenantCourse || !tenantCourse.authorized) { + throw new ForbiddenException('您的学校未获得此课程的授权'); + } + + // 解析 JSON 字段 + return { + ...course, + gradeTags: JSON.parse(course.gradeTags || '[]'), + domainTags: JSON.parse(course.domainTags || '[]'), + ebookPaths: course.ebookPaths ? JSON.parse(course.ebookPaths) : null, + audioPaths: course.audioPaths ? JSON.parse(course.audioPaths) : null, + videoPaths: course.videoPaths ? JSON.parse(course.videoPaths) : null, + otherResources: course.otherResources ? JSON.parse(course.otherResources) : null, + posterPaths: course.posterPaths ? JSON.parse(course.posterPaths) : null, + tenantCourses: undefined, // 移除敏感信息 + scripts: course.scripts.map((script) => ({ + ...script, + interactionPoints: script.interactionPoints ? JSON.parse(script.interactionPoints) : null, + resourceIds: script.resourceIds ? JSON.parse(script.resourceIds) : null, + pages: script.pages?.map((page) => ({ + ...page, + resourceIds: page.resourceIds ? JSON.parse(page.resourceIds) : null, + })), + })), + activities: course.activities.map((activity) => ({ + ...activity, + onlineMaterials: activity.onlineMaterials ? JSON.parse(activity.onlineMaterials) : null, + objectives: activity.objectives ? JSON.parse(activity.objectives) : null, + })), + }; + } + + async getTeacherClasses(teacherId: number) { + // 通过 ClassTeacher 表查询教师关联的班级 + const classTeachers = await this.prisma.classTeacher.findMany({ + where: { teacherId }, + include: { + class: { + select: { + id: true, + name: true, + grade: true, + studentCount: true, + lessonCount: true, + }, + }, + }, + orderBy: [ + { isPrimary: 'desc' }, + { createdAt: 'asc' }, + ], + }); + + return classTeachers.map((ct) => ({ + id: ct.class.id, + name: ct.class.name, + grade: ct.class.grade, + studentCount: ct.class.studentCount, + lessonCount: ct.class.lessonCount, + myRole: ct.role, // 我在该班级的角色 + isPrimary: ct.isPrimary, // 是否班主任 + })); + } + + async getClassTeachers(teacherId: number, classId: number) { + // 验证教师是否有权限查看该班级 + const teacherClass = await this.prisma.classTeacher.findFirst({ + where: { teacherId, classId }, + }); + + if (!teacherClass) { + throw new ForbiddenException('您没有权限查看该班级'); + } + + const classTeachers = await this.prisma.classTeacher.findMany({ + where: { classId }, + include: { + teacher: { + select: { + id: true, + name: true, + phone: true, + }, + }, + }, + orderBy: [ + { isPrimary: 'desc' }, + { createdAt: 'asc' }, + ], + }); + + return classTeachers.map((ct) => ({ + teacherId: ct.teacher.id, + teacherName: ct.teacher.name, + teacherPhone: ct.teacher.phone, + role: ct.role, + isPrimary: ct.isPrimary, + })); + } + + /** + * 获取教师所有班级的学生列表(跨班级) + */ + async getAllTeacherStudents(teacherId: number, query: any) { + const { page = 1, pageSize = 20, keyword } = query; + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 获取教师关联的所有班级ID + const classTeachers = await this.prisma.classTeacher.findMany({ + where: { teacherId }, + select: { classId: true }, + }); + + const classIds = classTeachers.map((ct) => ct.classId); + + if (classIds.length === 0) { + return { + items: [], + total: 0, + page: +page, + pageSize: +pageSize, + }; + } + + // 构建搜索条件 + const where: any = { + classId: { in: classIds }, + }; + + if (keyword) { + where.name = { contains: keyword }; + } + + // 查询学生列表 + const [students, total] = await Promise.all([ + this.prisma.student.findMany({ + where, + skip, + take, + select: { + id: true, + name: true, + gender: true, + birthDate: true, + classId: true, + parentName: true, + parentPhone: true, + createdAt: true, + class: { + select: { + id: true, + name: true, + grade: true, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.student.count({ where }), + ]); + + return { + items: students.map((student) => ({ + id: student.id, + name: student.name, + gender: student.gender, + birthDate: student.birthDate, + classId: student.classId, + parentName: student.parentName, + parentPhone: student.parentPhone, + createdAt: student.createdAt, + class: student.class, + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + /** + * 获取教师关联的所有班级ID + */ + async getTeacherClassIds(teacherId: number): Promise { + const classTeachers = await this.prisma.classTeacher.findMany({ + where: { teacherId }, + select: { classId: true }, + }); + return classTeachers.map((ct) => ct.classId); + } + + async getClassStudents(teacherId: number, classId: number, query: any) { + const { page = 1, pageSize = 20, keyword } = query; + const skip = (page - 1) * pageSize; + const take = +pageSize; + + // 验证教师是否关联该班级(通过 ClassTeacher 表) + const teacherClass = await this.prisma.classTeacher.findFirst({ + where: { + teacherId, + classId, + }, + include: { + class: { + select: { + id: true, + name: true, + grade: true, + studentCount: true, + lessonCount: true, + }, + }, + }, + }); + + if (!teacherClass) { + throw new ForbiddenException('您没有权限查看该班级或班级不存在'); + } + + const classEntity = teacherClass.class; + + // 构建搜索条件 + const where: any = { + classId: classId, + }; + + if (keyword) { + where.name = { contains: keyword }; + } + + // 查询学生列表 + const [students, total] = await Promise.all([ + this.prisma.student.findMany({ + where, + skip, + take, + select: { + id: true, + name: true, + gender: true, + birthDate: true, + parentName: true, + parentPhone: true, + lessonCount: true, + readingCount: true, + createdAt: true, + }, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.student.count({ where }), + ]); + + return { + items: students.map(student => ({ + id: student.id, + name: student.name, + gender: student.gender, + birthDate: student.birthDate, + parentName: student.parentName, + parentPhone: student.parentPhone, + lessonCount: student.lessonCount, + readingCount: student.readingCount, + createdAt: student.createdAt, + })), + total, + page: +page, + pageSize: +pageSize, + class: classEntity, + }; + } + + // ==================== 工具方法 ==================== + + private parseJsonArray(value: any): any[] { + if (!value) return []; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return []; + } + } + return Array.isArray(value) ? value : []; + } + + // ==================== 排课管理 ==================== + + async getTeacherSchedules(teacherId: number, query: any) { + const { page = 1, pageSize = 20, startDate, endDate, status } = query; + + const skip = (page - 1) * pageSize; + const take = +pageSize; + + const where: any = { + teacherId, + status: status || 'ACTIVE', + }; + + if (startDate || endDate) { + where.scheduledDate = {}; + if (startDate) where.scheduledDate.gte = new Date(startDate); + if (endDate) where.scheduledDate.lte = new Date(endDate); + } + + const [items, total] = await Promise.all([ + this.prisma.schedulePlan.findMany({ + where, + skip, + take, + orderBy: { scheduledDate: 'asc' }, + include: { + class: { select: { id: true, name: true, grade: true } }, + course: { select: { id: true, name: true, pictureBookName: true, duration: true } }, + lessons: { + where: { teacherId }, + select: { id: true, status: true }, + take: 1, + }, + }, + }), + this.prisma.schedulePlan.count({ where }), + ]); + + return { + items: items.map((item) => ({ + ...item, + className: item.class.name, + courseName: item.course.name, + hasLesson: item.lessons.length > 0, + lessonId: item.lessons[0]?.id, + lessonStatus: item.lessons[0]?.status, + })), + total, + page: +page, + pageSize: +pageSize, + }; + } + + async getTeacherTimetable(teacherId: number, startDate: string, endDate: string) { + const schedules = await this.prisma.schedulePlan.findMany({ + where: { + teacherId, + status: 'ACTIVE', + scheduledDate: { + gte: new Date(startDate), + lte: new Date(endDate), + }, + }, + orderBy: [{ scheduledDate: 'asc' }, { scheduledTime: 'asc' }], + include: { + class: { select: { id: true, name: true, grade: true } }, + course: { select: { id: true, name: true, pictureBookName: true, duration: true } }, + lessons: { + where: { teacherId }, + select: { id: true, status: true }, + take: 1, + }, + }, + }); + + // 按日期分组 + const timetable: Record = {}; + const start = new Date(startDate); + const end = new Date(endDate); + + // 初始化所有日期 + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const dateStr = d.toISOString().split('T')[0]; + timetable[dateStr] = []; + } + + // 填充排课数据 + schedules.forEach((schedule) => { + const dateStr = schedule.scheduledDate!.toISOString().split('T')[0]; + if (timetable[dateStr]) { + timetable[dateStr].push({ + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + hasLesson: schedule.lessons.length > 0, + lessonId: schedule.lessons[0]?.id, + lessonStatus: schedule.lessons[0]?.status, + }); + } + }); + + return Object.entries(timetable).map(([date, items]) => ({ + date, + weekDay: new Date(date).getDay(), + schedules: items, + })); + } + + async getTodaySchedules(teacherId: number) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + const schedules = await this.prisma.schedulePlan.findMany({ + where: { + teacherId, + status: 'ACTIVE', + scheduledDate: { + gte: today, + lt: tomorrow, + }, + }, + orderBy: { scheduledTime: 'asc' }, + include: { + class: { select: { id: true, name: true } }, + course: { select: { id: true, name: true, pictureBookName: true, duration: true } }, + lessons: { + where: { teacherId }, + select: { id: true, status: true }, + take: 1, + }, + }, + }); + + return schedules.map((schedule) => ({ + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + hasLesson: schedule.lessons.length > 0, + lessonId: schedule.lessons[0]?.id, + lessonStatus: schedule.lessons[0]?.status, + })); + } + + /** + * 解析时间字符串为分钟数 + * @param timeStr 格式: "HH:mm" + * @returns 从午夜开始的分钟数 + */ + private parseTimeToMinutes(timeStr: string): number { + const [hours, minutes] = timeStr.split(':').map(Number); + return hours * 60 + minutes; + } + + /** + * 检查两个时间段是否重叠 + * @param time1 格式: "HH:mm-HH:mm" + * @param time2 格式: "HH:mm-HH:mm" + */ + private isTimeOverlapping(time1: string, time2: string): boolean { + const [start1, end1] = time1.split('-').map(t => this.parseTimeToMinutes(t.trim())); + const [start2, end2] = time2.split('-').map(t => this.parseTimeToMinutes(t.trim())); + + // 两个时间段重叠的条件: start1 < end2 AND start2 < end1 + return start1 < end2 && start2 < end1; + } + + /** + * 检查教师排课时间冲突 + */ + private async checkScheduleConflict( + teacherId: number, + scheduledDate: string, + scheduledTime: string, + excludeScheduleId?: number, + ): Promise<{ courseName: string; className: string; scheduledTime: string } | null> { + // 使用日期范围查询,避免时区问题 + const dateStart = new Date(scheduledDate + 'T00:00:00.000Z'); + const dateEnd = new Date(scheduledDate + 'T23:59:59.999Z'); + + // 获取该教师当天的所有排课 + const existingSchedules = await this.prisma.schedulePlan.findMany({ + where: { + teacherId, + scheduledDate: { + gte: dateStart, + lte: dateEnd, + }, + status: 'ACTIVE', + ...(excludeScheduleId && { id: { not: excludeScheduleId } }), + }, + include: { + class: { select: { name: true } }, + course: { select: { name: true } }, + }, + }); + + // 检查是否存在时间重叠 + for (const schedule of existingSchedules) { + if (schedule.scheduledTime && this.isTimeOverlapping(scheduledTime, schedule.scheduledTime)) { + return { + courseName: schedule.course.name, + className: schedule.class.name, + scheduledTime: schedule.scheduledTime, + }; + } + } + + return null; + } + + async createTeacherSchedule(teacherId: number, tenantId: number, dto: any) { + // 验证班级存在且教师有权限 + const classTeacher = await this.prisma.classTeacher.findFirst({ + where: { teacherId, classId: dto.classId }, + }); + + if (!classTeacher) { + throw new ForbiddenException('您没有权限在此班级排课'); + } + + // 验证课程存在且已授权 + const tenantCourse = await this.prisma.tenantCourse.findFirst({ + where: { tenantId, courseId: dto.courseId, authorized: true }, + }); + + if (!tenantCourse) { + throw new ForbiddenException('该课程未授权或不存在'); + } + + // 检查教师时间冲突 + if (dto.scheduledDate && dto.scheduledTime) { + const conflict = await this.checkScheduleConflict(teacherId, dto.scheduledDate, dto.scheduledTime); + if (conflict) { + throw new ConflictException( + `时间冲突:您在 ${conflict.scheduledTime} 已有排课「${conflict.courseName}」(${conflict.className}),请选择其他时间段` + ); + } + } + + const schedule = await this.prisma.schedulePlan.create({ + data: { + tenantId, + classId: dto.classId, + courseId: dto.courseId, + teacherId, + scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : null, + scheduledTime: dto.scheduledTime, + weekDay: dto.weekDay, + repeatType: dto.repeatType || 'NONE', + repeatEndDate: dto.repeatEndDate ? new Date(dto.repeatEndDate) : null, + source: 'TEACHER', + createdBy: teacherId, + note: dto.note, + status: 'ACTIVE', + }, + include: { + class: { select: { id: true, name: true } }, + course: { select: { id: true, name: true } }, + }, + }); + + this.logger.log(`Teacher schedule created: ${schedule.id} by teacher ${teacherId}`); + + return { + ...schedule, + className: schedule.class.name, + courseName: schedule.course.name, + }; + } + + async updateTeacherSchedule(teacherId: number, id: number, dto: any) { + const schedule = await this.prisma.schedulePlan.findFirst({ + where: { id, teacherId }, + }); + + if (!schedule) { + throw new NotFoundException('排课计划不存在或无权限'); + } + + const updated = await this.prisma.schedulePlan.update({ + where: { id }, + data: { + scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : undefined, + scheduledTime: dto.scheduledTime, + weekDay: dto.weekDay, + repeatType: dto.repeatType, + repeatEndDate: dto.repeatEndDate ? new Date(dto.repeatEndDate) : undefined, + note: dto.note, + status: dto.status, + }, + include: { + class: { select: { id: true, name: true } }, + course: { select: { id: true, name: true } }, + }, + }); + + this.logger.log(`Teacher schedule updated: ${id}`); + + return { + ...updated, + className: updated.class.name, + courseName: updated.course.name, + }; + } + + async cancelTeacherSchedule(teacherId: number, id: number) { + const schedule = await this.prisma.schedulePlan.findFirst({ + where: { id, teacherId }, + }); + + if (!schedule) { + throw new NotFoundException('排课计划不存在或无权限'); + } + + await this.prisma.schedulePlan.update({ + where: { id }, + data: { status: 'CANCELLED' }, + }); + + this.logger.log(`Teacher schedule cancelled: ${id}`); + + return { message: '取消成功' }; + } +} diff --git a/reading-platform-backend/src/modules/tenant/dto/tenant.dto.ts b/reading-platform-backend/src/modules/tenant/dto/tenant.dto.ts new file mode 100644 index 0000000..4072c77 --- /dev/null +++ b/reading-platform-backend/src/modules/tenant/dto/tenant.dto.ts @@ -0,0 +1,158 @@ +import { + IsString, + IsNotEmpty, + IsOptional, + IsInt, + IsDateString, + Matches, + Min, + Max, +} from 'class-validator'; + +export class TenantQueryDto { + @IsOptional() + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @IsInt() + @Min(1) + @Max(100) + pageSize?: number = 10; + + @IsOptional() + @IsString() + keyword?: string; + + @IsOptional() + @IsString() + status?: string; + + @IsOptional() + @IsString() + packageType?: string; +} + +export class CreateTenantDto { + @IsString() + @IsNotEmpty({ message: '学校名称不能为空' }) + name: string; + + @IsString() + @IsNotEmpty({ message: '登录账号不能为空' }) + @Matches(/^[a-zA-Z][a-zA-Z0-9_]{3,19}$/, { + message: '登录账号必须以字母开头,4-20位字母、数字或下划线', + }) + loginAccount: string; + + @IsOptional() + @IsString() + @Matches(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{6,20}$/, { + message: '密码至少6位,需包含字母和数字', + }) + password?: string; + + @IsOptional() + @IsString() + address?: string; + + @IsOptional() + @IsString() + contactPerson?: string; + + @IsOptional() + @IsString() + @Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }) + contactPhone?: string; + + @IsOptional() + @IsString() + packageType?: string = 'STANDARD'; + + @IsOptional() + @IsInt() + @Min(1) + teacherQuota?: number = 20; + + @IsOptional() + @IsInt() + @Min(1) + studentQuota?: number = 200; + + @IsOptional() + @IsDateString() + startDate?: string; + + @IsOptional() + @IsDateString() + expireDate?: string; +} + +export class UpdateTenantDto { + @IsOptional() + @IsString() + @IsNotEmpty({ message: '学校名称不能为空' }) + name?: string; + + @IsOptional() + @IsString() + address?: string; + + @IsOptional() + @IsString() + contactPerson?: string; + + @IsOptional() + @IsString() + @Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' }) + contactPhone?: string; + + @IsOptional() + @IsString() + packageType?: string; + + @IsOptional() + @IsInt() + @Min(1) + teacherQuota?: number; + + @IsOptional() + @IsInt() + @Min(1) + studentQuota?: number; + + @IsOptional() + @IsDateString() + startDate?: string; + + @IsOptional() + @IsDateString() + expireDate?: string; + + @IsOptional() + @IsString() + status?: string; +} + +export class UpdateTenantQuotaDto { + @IsOptional() + @IsString() + packageType?: string; + + @IsOptional() + @IsInt() + @Min(1) + teacherQuota?: number; + + @IsOptional() + @IsInt() + @Min(1) + studentQuota?: number; +} + +export class UpdateTenantStatusDto { + @IsString() + @IsNotEmpty({ message: '状态不能为空' }) + status: string; +} diff --git a/reading-platform-backend/src/modules/tenant/tenant.controller.js b/reading-platform-backend/src/modules/tenant/tenant.controller.js new file mode 100644 index 0000000..a2519d9 --- /dev/null +++ b/reading-platform-backend/src/modules/tenant/tenant.controller.js @@ -0,0 +1,96 @@ +"use strict"; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TenantController = void 0; +var common_1 = require("@nestjs/common"); +var jwt_auth_guard_1 = require("../common/guards/jwt-auth.guard"); +var TenantController = function () { + var _classDecorators = [(0, common_1.Controller)('tenants'), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard)]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var _instanceExtraInitializers = []; + var _findAll_decorators; + var _findOne_decorators; + var _create_decorators; + var _update_decorators; + var _remove_decorators; + var TenantController = _classThis = /** @class */ (function () { + function TenantController_1(tenantService) { + this.tenantService = (__runInitializers(this, _instanceExtraInitializers), tenantService); + } + TenantController_1.prototype.findAll = function () { + return this.tenantService.findAll(); + }; + TenantController_1.prototype.findOne = function (id) { + return this.tenantService.findOne(+id); + }; + TenantController_1.prototype.create = function (createTenantDto) { + return this.tenantService.create(createTenantDto); + }; + TenantController_1.prototype.update = function (id, updateTenantDto) { + return this.tenantService.update(+id, updateTenantDto); + }; + TenantController_1.prototype.remove = function (id) { + return this.tenantService.remove(+id); + }; + return TenantController_1; + }()); + __setFunctionName(_classThis, "TenantController"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + _findAll_decorators = [(0, common_1.Get)()]; + _findOne_decorators = [(0, common_1.Get)(':id')]; + _create_decorators = [(0, common_1.Post)()]; + _update_decorators = [(0, common_1.Put)(':id')]; + _remove_decorators = [(0, common_1.Delete)(':id')]; + __esDecorate(_classThis, null, _findAll_decorators, { kind: "method", name: "findAll", static: false, private: false, access: { has: function (obj) { return "findAll" in obj; }, get: function (obj) { return obj.findAll; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(_classThis, null, _findOne_decorators, { kind: "method", name: "findOne", static: false, private: false, access: { has: function (obj) { return "findOne" in obj; }, get: function (obj) { return obj.findOne; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(_classThis, null, _create_decorators, { kind: "method", name: "create", static: false, private: false, access: { has: function (obj) { return "create" in obj; }, get: function (obj) { return obj.create; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(_classThis, null, _update_decorators, { kind: "method", name: "update", static: false, private: false, access: { has: function (obj) { return "update" in obj; }, get: function (obj) { return obj.update; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(_classThis, null, _remove_decorators, { kind: "method", name: "remove", static: false, private: false, access: { has: function (obj) { return "remove" in obj; }, get: function (obj) { return obj.remove; } }, metadata: _metadata }, null, _instanceExtraInitializers); + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + TenantController = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return TenantController = _classThis; +}(); +exports.TenantController = TenantController; diff --git a/reading-platform-backend/src/modules/tenant/tenant.controller.ts b/reading-platform-backend/src/modules/tenant/tenant.controller.ts new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/reading-platform-backend/src/modules/tenant/tenant.controller.ts @@ -0,0 +1,74 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + UseGuards, + Query, +} from '@nestjs/common'; +import { TenantService } from './tenant.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; +import { + TenantQueryDto, + CreateTenantDto, + UpdateTenantDto, + UpdateTenantQuotaDto, + UpdateTenantStatusDto, +} from './dto/tenant.dto'; + +@Controller('admin/tenants') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class TenantController { + constructor(private readonly tenantService: TenantService) {} + + @Get() + findAll(@Query() query: TenantQueryDto) { + return this.tenantService.findAllPaginated(query); + } + + @Get('stats') + getStats() { + return this.tenantService.getStats(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.tenantService.findOne(+id); + } + + @Post() + create(@Body() createTenantDto: CreateTenantDto) { + return this.tenantService.create(createTenantDto); + } + + @Put(':id') + update(@Param('id') id: string, @Body() updateTenantDto: UpdateTenantDto) { + return this.tenantService.update(+id, updateTenantDto); + } + + @Put(':id/quota') + updateQuota(@Param('id') id: string, @Body() dto: UpdateTenantQuotaDto) { + return this.tenantService.updateQuota(+id, dto); + } + + @Put(':id/status') + updateStatus(@Param('id') id: string, @Body() dto: UpdateTenantStatusDto) { + return this.tenantService.updateStatus(+id, dto); + } + + @Post(':id/reset-password') + resetPassword(@Param('id') id: string) { + return this.tenantService.resetPassword(+id); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.tenantService.remove(+id); + } +} diff --git a/reading-platform-backend/src/modules/tenant/tenant.module.js b/reading-platform-backend/src/modules/tenant/tenant.module.js new file mode 100644 index 0000000..ffd58b6 --- /dev/null +++ b/reading-platform-backend/src/modules/tenant/tenant.module.js @@ -0,0 +1,71 @@ +"use strict"; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TenantModule = void 0; +var common_1 = require("@nestjs/common"); +var tenant_service_1 = require("./tenant.service"); +var tenant_controller_1 = require("./tenant.controller"); +var prisma_module_1 = require("../../database/prisma.module"); +var TenantModule = function () { + var _classDecorators = [(0, common_1.Module)({ + imports: [prisma_module_1.PrismaModule], + controllers: [tenant_controller_1.TenantController], + providers: [tenant_service_1.TenantService], + exports: [tenant_service_1.TenantService], + })]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var TenantModule = _classThis = /** @class */ (function () { + function TenantModule_1() { + } + return TenantModule_1; + }()); + __setFunctionName(_classThis, "TenantModule"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + TenantModule = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return TenantModule = _classThis; +}(); +exports.TenantModule = TenantModule; diff --git a/reading-platform-backend/src/modules/tenant/tenant.module.ts b/reading-platform-backend/src/modules/tenant/tenant.module.ts new file mode 100644 index 0000000..499b7e0 --- /dev/null +++ b/reading-platform-backend/src/modules/tenant/tenant.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TenantService } from './tenant.service'; +import { TenantController } from './tenant.controller'; +import { PrismaModule } from '../../database/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [TenantController], + providers: [TenantService], + exports: [TenantService], +}) +export class TenantModule {} diff --git a/reading-platform-backend/src/modules/tenant/tenant.service.js b/reading-platform-backend/src/modules/tenant/tenant.service.js new file mode 100644 index 0000000..dacd70a --- /dev/null +++ b/reading-platform-backend/src/modules/tenant/tenant.service.js @@ -0,0 +1,192 @@ +"use strict"; +var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { + function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } + var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; + var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; + var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); + var _, done = false; + for (var i = decorators.length - 1; i >= 0; i--) { + var context = {}; + for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; + for (var p in contextIn.access) context.access[p] = contextIn.access[p]; + context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; + var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); + if (kind === "accessor") { + if (result === void 0) continue; + if (result === null || typeof result !== "object") throw new TypeError("Object expected"); + if (_ = accept(result.get)) descriptor.get = _; + if (_ = accept(result.set)) descriptor.set = _; + if (_ = accept(result.init)) initializers.unshift(_); + } + else if (_ = accept(result)) { + if (kind === "field") initializers.unshift(_); + else descriptor[key] = _; + } + } + if (target) Object.defineProperty(target, contextIn.name, descriptor); + done = true; +}; +var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { + var useValue = arguments.length > 2; + for (var i = 0; i < initializers.length; i++) { + value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); + } + return useValue ? value : void 0; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { + if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; + return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TenantService = void 0; +var common_1 = require("@nestjs/common"); +var TenantService = function () { + var _classDecorators = [(0, common_1.Injectable)()]; + var _classDescriptor; + var _classExtraInitializers = []; + var _classThis; + var TenantService = _classThis = /** @class */ (function () { + function TenantService_1(prisma) { + this.prisma = prisma; + } + TenantService_1.prototype.findAll = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.prisma.tenant.findMany({ + select: { + id: true, + name: true, + address: true, + contactPerson: true, + contactPhone: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + teacherCount: true, + studentCount: true, + startDate: true, + expireDate: true, + status: true, + createdAt: true, + }, + orderBy: { createdAt: 'desc' }, + })]; + }); + }); + }; + TenantService_1.prototype.findOne = function (id) { + return __awaiter(this, void 0, void 0, function () { + var tenant; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.prisma.tenant.findUnique({ + where: { id: id }, + include: { + teachers: { + select: { + id: true, + name: true, + phone: true, + email: true, + status: true, + lessonCount: true, + }, + }, + students: { + select: { + id: true, + name: true, + classId: true, + gender: true, + readingCount: true, + }, + }, + }, + })]; + case 1: + tenant = _a.sent(); + if (!tenant) { + throw new common_1.NotFoundException("Tenant #".concat(id, " not found")); + } + return [2 /*return*/, tenant]; + } + }); + }); + }; + TenantService_1.prototype.create = function (createTenantDto) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.prisma.tenant.create({ + data: createTenantDto, + })]; + }); + }); + }; + TenantService_1.prototype.update = function (id, updateTenantDto) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.prisma.tenant.update({ + where: { id: id }, + data: updateTenantDto, + })]; + }); + }); + }; + TenantService_1.prototype.remove = function (id) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.prisma.tenant.delete({ + where: { id: id }, + })]; + }); + }); + }; + return TenantService_1; + }()); + __setFunctionName(_classThis, "TenantService"); + (function () { + var _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; + __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); + TenantService = _classThis = _classDescriptor.value; + if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); + __runInitializers(_classThis, _classExtraInitializers); + })(); + return TenantService = _classThis; +}(); +exports.TenantService = TenantService; diff --git a/reading-platform-backend/src/modules/tenant/tenant.service.ts b/reading-platform-backend/src/modules/tenant/tenant.service.ts new file mode 100644 index 0000000..1d5006d --- /dev/null +++ b/reading-platform-backend/src/modules/tenant/tenant.service.ts @@ -0,0 +1,422 @@ +import { + Injectable, + NotFoundException, + ConflictException, + BadRequestException, +} from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; +import * as bcrypt from 'bcrypt'; +import { + TenantQueryDto, + CreateTenantDto, + UpdateTenantDto, + UpdateTenantQuotaDto, + UpdateTenantStatusDto, +} from './dto/tenant.dto'; + +@Injectable() +export class TenantService { + constructor(private prisma: PrismaService) {} + + async findAllPaginated(query: TenantQueryDto) { + const { page = 1, pageSize = 10, keyword, status, packageType } = query; + const skip = (page - 1) * pageSize; + + const where: any = {}; + + if (keyword) { + where.OR = [ + { name: { contains: keyword } }, + { loginAccount: { contains: keyword } }, + { contactPerson: { contains: keyword } }, + { contactPhone: { contains: keyword } }, + ]; + } + + if (status) { + where.status = status; + } + + if (packageType) { + where.packageType = packageType; + } + + const [items, total] = await Promise.all([ + this.prisma.tenant.findMany({ + where, + select: { + id: true, + name: true, + loginAccount: true, + address: true, + contactPerson: true, + contactPhone: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + teacherCount: true, + studentCount: true, + startDate: true, + expireDate: true, + status: true, + createdAt: true, + }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }), + this.prisma.tenant.count({ where }), + ]); + + return { + items, + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize), + }; + } + + async findAll() { + return this.prisma.tenant.findMany({ + select: { + id: true, + name: true, + address: true, + contactPerson: true, + contactPhone: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + teacherCount: true, + studentCount: true, + startDate: true, + expireDate: true, + status: true, + createdAt: true, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + async findOne(id: number) { + const tenant = await this.prisma.tenant.findUnique({ + where: { id }, + select: { + id: true, + name: true, + loginAccount: true, + address: true, + contactPerson: true, + contactPhone: true, + logoUrl: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + storageQuota: true, + teacherCount: true, + studentCount: true, + storageUsed: true, + startDate: true, + expireDate: true, + status: true, + createdAt: true, + updatedAt: true, + teachers: { + select: { + id: true, + name: true, + phone: true, + email: true, + status: true, + lessonCount: true, + }, + take: 10, + orderBy: { createdAt: 'desc' }, + }, + students: { + select: { + id: true, + name: true, + classId: true, + gender: true, + readingCount: true, + }, + take: 10, + orderBy: { createdAt: 'desc' }, + }, + classes: { + select: { + id: true, + name: true, + grade: true, + studentCount: true, + }, + take: 10, + }, + _count: { + select: { + teachers: true, + students: true, + classes: true, + lessons: true, + }, + }, + }, + }); + + if (!tenant) { + throw new NotFoundException(`租户 #${id} 不存在`); + } + + // Convert BigInt to string for JSON serialization + return { + ...tenant, + storageQuota: tenant.storageQuota?.toString() || '0', + storageUsed: tenant.storageUsed?.toString() || '0', + }; + } + + async create(dto: CreateTenantDto) { + // Check if login account already exists + const existing = await this.prisma.tenant.findUnique({ + where: { loginAccount: dto.loginAccount }, + }); + + if (existing) { + throw new ConflictException('登录账号已存在'); + } + + // Check if teacher login account exists too + const existingTeacher = await this.prisma.teacher.findUnique({ + where: { loginAccount: dto.loginAccount }, + }); + + if (existingTeacher) { + throw new ConflictException('该账号已被教师使用'); + } + + // Hash password + const defaultPassword = dto.password || '123456'; + const passwordHash = await bcrypt.hash(defaultPassword, 10); + + // Set default dates if not provided + const startDate = dto.startDate || new Date().toISOString().split('T')[0]; + const expireDate = + dto.expireDate || + new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + const tenant = await this.prisma.tenant.create({ + data: { + name: dto.name, + loginAccount: dto.loginAccount, + passwordHash, + address: dto.address, + contactPerson: dto.contactPerson, + contactPhone: dto.contactPhone, + packageType: dto.packageType || 'STANDARD', + teacherQuota: dto.teacherQuota || 20, + studentQuota: dto.studentQuota || 200, + startDate, + expireDate, + status: 'ACTIVE', + }, + select: { + id: true, + name: true, + loginAccount: true, + address: true, + contactPerson: true, + contactPhone: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + startDate: true, + expireDate: true, + status: true, + createdAt: true, + }, + }); + + return { + ...tenant, + tempPassword: dto.password || '123456', + }; + } + + async update(id: number, dto: UpdateTenantDto) { + // Check if tenant exists + const existing = await this.prisma.tenant.findUnique({ + where: { id }, + }); + + if (!existing) { + throw new NotFoundException(`租户 #${id} 不存在`); + } + + return this.prisma.tenant.update({ + where: { id }, + data: { + name: dto.name, + address: dto.address, + contactPerson: dto.contactPerson, + contactPhone: dto.contactPhone, + packageType: dto.packageType, + teacherQuota: dto.teacherQuota, + studentQuota: dto.studentQuota, + startDate: dto.startDate, + expireDate: dto.expireDate, + status: dto.status, + }, + select: { + id: true, + name: true, + loginAccount: true, + address: true, + contactPerson: true, + contactPhone: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + startDate: true, + expireDate: true, + status: true, + createdAt: true, + updatedAt: true, + }, + }); + } + + async updateQuota(id: number, dto: UpdateTenantQuotaDto) { + // Check if tenant exists + const existing = await this.prisma.tenant.findUnique({ + where: { id }, + }); + + if (!existing) { + throw new NotFoundException(`租户 #${id} 不存在`); + } + + // Validate quota not less than current usage + if (dto.teacherQuota !== undefined && dto.teacherQuota < existing.teacherCount) { + throw new BadRequestException( + `教师配额不能小于当前已用数量 (${existing.teacherCount})`, + ); + } + + if (dto.studentQuota !== undefined && dto.studentQuota < existing.studentCount) { + throw new BadRequestException( + `学生配额不能小于当前已用数量 (${existing.studentCount})`, + ); + } + + return this.prisma.tenant.update({ + where: { id }, + data: { + packageType: dto.packageType, + teacherQuota: dto.teacherQuota, + studentQuota: dto.studentQuota, + }, + select: { + id: true, + name: true, + packageType: true, + teacherQuota: true, + studentQuota: true, + teacherCount: true, + studentCount: true, + }, + }); + } + + async updateStatus(id: number, dto: UpdateTenantStatusDto) { + // Check if tenant exists + const existing = await this.prisma.tenant.findUnique({ + where: { id }, + }); + + if (!existing) { + throw new NotFoundException(`租户 #${id} 不存在`); + } + + return this.prisma.tenant.update({ + where: { id }, + data: { + status: dto.status, + }, + select: { + id: true, + name: true, + status: true, + }, + }); + } + + async resetPassword(id: number) { + // Check if tenant exists + const existing = await this.prisma.tenant.findUnique({ + where: { id }, + }); + + if (!existing) { + throw new NotFoundException(`租户 #${id} 不存在`); + } + + // Generate random password + const tempPassword = Math.random().toString(36).slice(-8); + const passwordHash = await bcrypt.hash(tempPassword, 10); + + await this.prisma.tenant.update({ + where: { id }, + data: { + passwordHash, + }, + }); + + return { + tempPassword, + }; + } + + async remove(id: number) { + // Check if tenant exists + const existing = await this.prisma.tenant.findUnique({ + where: { id }, + }); + + if (!existing) { + throw new NotFoundException(`租户 #${id} 不存在`); + } + + await this.prisma.tenant.delete({ + where: { id }, + }); + + return { success: true }; + } + + async getStats() { + const [totalCount, activeCount, expiredCount] = await Promise.all([ + this.prisma.tenant.count(), + this.prisma.tenant.count({ where: { status: 'ACTIVE' } }), + this.prisma.tenant.count({ where: { status: { not: 'ACTIVE' } } }), + ]); + + const packageDistribution = await this.prisma.tenant.groupBy({ + by: ['packageType'], + _count: { + id: true, + }, + }); + + return { + totalCount, + activeCount, + expiredCount, + packageDistribution: packageDistribution.map((item) => ({ + packageType: item.packageType, + count: item._count.id, + })), + }; + } +} diff --git a/reading-platform-backend/src/modules/theme/theme.controller.ts b/reading-platform-backend/src/modules/theme/theme.controller.ts new file mode 100644 index 0000000..8e6f521 --- /dev/null +++ b/reading-platform-backend/src/modules/theme/theme.controller.ts @@ -0,0 +1,58 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + ParseIntPipe, + UseGuards, +} from '@nestjs/common'; +import { ThemeService } from './theme.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../common/guards/roles.guard'; +import { Roles } from '../common/decorators/roles.decorator'; + +@Controller('admin/themes') +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('admin') +export class ThemeController { + constructor(private readonly themeService: ThemeService) {} + + @Get() + async findAll() { + return this.themeService.findAll(); + } + + @Get(':id') + async findOne(@Param('id', ParseIntPipe) id: number) { + return this.themeService.findOne(id); + } + + @Post() + async create( + @Body() body: { name: string; description?: string; sortOrder?: number }, + ) { + return this.themeService.create(body); + } + + @Put(':id') + async update( + @Param('id', ParseIntPipe) id: number, + @Body() + body: { name?: string; description?: string; sortOrder?: number; status?: string }, + ) { + return this.themeService.update(id, body); + } + + @Delete(':id') + async remove(@Param('id', ParseIntPipe) id: number) { + return this.themeService.remove(id); + } + + @Put('reorder') + async reorder(@Body() body: { ids: number[] }) { + return this.themeService.reorder(body.ids); + } +} diff --git a/reading-platform-backend/src/modules/theme/theme.module.ts b/reading-platform-backend/src/modules/theme/theme.module.ts new file mode 100644 index 0000000..1c773bb --- /dev/null +++ b/reading-platform-backend/src/modules/theme/theme.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ThemeController } from './theme.controller'; +import { ThemeService } from './theme.service'; + +@Module({ + controllers: [ThemeController], + providers: [ThemeService], + exports: [ThemeService], +}) +export class ThemeModule {} diff --git a/reading-platform-backend/src/modules/theme/theme.service.ts b/reading-platform-backend/src/modules/theme/theme.service.ts new file mode 100644 index 0000000..fd46c48 --- /dev/null +++ b/reading-platform-backend/src/modules/theme/theme.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../database/prisma.service'; + +@Injectable() +export class ThemeService { + constructor(private prisma: PrismaService) {} + + async findAll() { + return this.prisma.theme.findMany({ + orderBy: { sortOrder: 'asc' }, + }); + } + + async findOne(id: number) { + return this.prisma.theme.findUnique({ + where: { id }, + include: { + courses: { + where: { status: 'PUBLISHED' }, + select: { id: true, name: true, coverImagePath: true }, + take: 5, + }, + }, + }); + } + + async create(data: { name: string; description?: string; sortOrder?: number }) { + const maxSortOrder = await this.prisma.theme.aggregate({ + _max: { sortOrder: true }, + }); + + return this.prisma.theme.create({ + data: { + name: data.name, + description: data.description, + sortOrder: data.sortOrder ?? (maxSortOrder._max.sortOrder || 0) + 1, + }, + }); + } + + async update(id: number, data: { name?: string; description?: string; sortOrder?: number; status?: string }) { + return this.prisma.theme.update({ + where: { id }, + data, + }); + } + + async remove(id: number) { + // 检查是否有关联课程 + const courseCount = await this.prisma.course.count({ + where: { themeId: id }, + }); + + if (courseCount > 0) { + throw new Error(`该主题下有 ${courseCount} 个课程包,无法删除`); + } + + return this.prisma.theme.delete({ + where: { id }, + }); + } + + async reorder(ids: number[]) { + const updates = ids.map((id, index) => + this.prisma.theme.update({ + where: { id }, + data: { sortOrder: index + 1 }, + }), + ); + + return Promise.all(updates); + } +} diff --git a/reading-platform-backend/start-backend.sh b/reading-platform-backend/start-backend.sh new file mode 100644 index 0000000..21ef2f8 --- /dev/null +++ b/reading-platform-backend/start-backend.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# 后端启动脚本 +# 确保从正确目录启动 + +cd "$(dirname "$0")" + +echo "🚀 正在启动后端服务..." +echo "📂 工作目录: $(pwd)" + +# 检查 node_modules 是否存在 +if [ ! -d "node_modules" ]; then + echo "📦 正在安装依赖..." + npm install +fi + +# 启动后端 +npm run start:dev diff --git a/reading-platform-backend/tsconfig.build.json b/reading-platform-backend/tsconfig.build.json new file mode 100644 index 0000000..02070b3 --- /dev/null +++ b/reading-platform-backend/tsconfig.build.json @@ -0,0 +1,10 @@ +# 后端 NestJS 配置 +module.exports = { + extends: '../tsconfig.json', + compilerOptions: { + declaration: false, + sourceMap: true, + }, + include: ['src/**/*'], + exclude: ['node_modules', 'test', 'dist'], +}; diff --git a/reading-platform-backend/tsconfig.json b/reading-platform-backend/tsconfig.json new file mode 100644 index 0000000..5607d84 --- /dev/null +++ b/reading-platform-backend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "useDefineForClassFields": false + }, + "include": ["src/**/*", "prisma/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/reading-platform-frontend/.env.development b/reading-platform-frontend/.env.development new file mode 100644 index 0000000..41071c4 --- /dev/null +++ b/reading-platform-frontend/.env.development @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=http://localhost:3000/api/v1 +VITE_APP_TITLE=幼儿阅读教学服务平台 +VITE_SERVER_BASE_URL=http://localhost:3000 diff --git a/reading-platform-frontend/index.html b/reading-platform-frontend/index.html new file mode 100644 index 0000000..eb354c7 --- /dev/null +++ b/reading-platform-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 幼儿阅读教学服务平台 + + +
+ + + diff --git a/reading-platform-frontend/package-lock.json b/reading-platform-frontend/package-lock.json new file mode 100644 index 0000000..4acc361 --- /dev/null +++ b/reading-platform-frontend/package-lock.json @@ -0,0 +1,3912 @@ +{ + "name": "reading-platform-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reading-platform-frontend", + "version": "1.0.0", + "dependencies": { + "@ant-design/icons-vue": "^7.0.1", + "@fullcalendar/core": "^6.1.20", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", + "@fullcalendar/vue3": "^6.1.20", + "ant-design-vue": "^4.1.2", + "axios": "^1.6.7", + "dayjs": "^1.11.10", + "echarts": "^6.0.0", + "lodash-es": "^4.17.21", + "lucide-vue-next": "^0.575.0", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@types/lodash-es": "^4.17.12", + "@types/node": "^20.11.28", + "@vitejs/plugin-vue": "^5.0.4", + "@vue/tsconfig": "^0.5.1", + "sass-embedded": "^1.97.3", + "typescript": "~5.4.0", + "unplugin-auto-import": "^0.17.5", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.1.6", + "vite-plugin-compression": "^0.5.1", + "vue-tsc": "^2.0.6" + } + }, + "node_modules/@ant-design/colors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz", + "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/icons-vue": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz", + "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^6.0.0", + "@ant-design/icons-svg": "^4.2.1" + }, + "peerDependencies": { + "vue": ">=3.0.3" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fullcalendar/core": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", + "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz", + "integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz", + "integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz", + "integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==", + "license": "MIT", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/vue3": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.20.tgz", + "integrity": "sha512-8qg6pS27II9QBwFkkJC+7SfflMpWqOe7i3ii5ODq9KpLAjwQAd/zjfq8RvKR1Yryoh5UmMCmvRbMB7i4RGtqog==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20", + "vue": "^3.0.11" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@simonwep/pickr": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.2.tgz", + "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==", + "license": "MIT", + "dependencies": { + "core-js": "^3.15.1", + "nanopop": "^2.1.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "vue": "3.5.28" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz", + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ant-design-vue": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-4.2.6.tgz", + "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^6.0.0", + "@ant-design/icons-vue": "^7.0.0", + "@babel/runtime": "^7.10.5", + "@ctrl/tinycolor": "^3.5.0", + "@emotion/hash": "^0.9.0", + "@emotion/unitless": "^0.8.0", + "@simonwep/pickr": "~1.8.0", + "array-tree-filter": "^2.1.0", + "async-validator": "^4.0.0", + "csstype": "^3.1.1", + "dayjs": "^1.10.5", + "dom-align": "^1.12.1", + "dom-scroll-into-view": "^2.0.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.15", + "resize-observer-polyfill": "^1.5.1", + "scroll-into-view-if-needed": "^2.2.25", + "shallow-equal": "^1.0.0", + "stylis": "^4.1.3", + "throttle-debounce": "^5.0.0", + "vue-types": "^3.0.0", + "warning": "^4.0.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design-vue" + }, + "peerDependencies": { + "vue": ">=3.2.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/array-tree-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", + "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==", + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-align": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz", + "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==", + "license": "MIT" + }, + "node_modules/dom-scroll-into-view": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz", + "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loose-envify/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lucide-vue-next": { + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.575.0.tgz", + "integrity": "sha512-UHzA3cYMCgBLyGay5R9IQaidwV0NLocx7cIBnFt8vJ9Xhl6IM/oKD0fUhoCUuouFta15SX1rLXVoko9s3TzWMA==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanopop": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.4.2.tgz", + "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.97.3.tgz", + "integrity": "sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.97.3", + "sass-embedded-android-arm": "1.97.3", + "sass-embedded-android-arm64": "1.97.3", + "sass-embedded-android-riscv64": "1.97.3", + "sass-embedded-android-x64": "1.97.3", + "sass-embedded-darwin-arm64": "1.97.3", + "sass-embedded-darwin-x64": "1.97.3", + "sass-embedded-linux-arm": "1.97.3", + "sass-embedded-linux-arm64": "1.97.3", + "sass-embedded-linux-musl-arm": "1.97.3", + "sass-embedded-linux-musl-arm64": "1.97.3", + "sass-embedded-linux-musl-riscv64": "1.97.3", + "sass-embedded-linux-musl-x64": "1.97.3", + "sass-embedded-linux-riscv64": "1.97.3", + "sass-embedded-linux-x64": "1.97.3", + "sass-embedded-unknown-all": "1.97.3", + "sass-embedded-win32-arm64": "1.97.3", + "sass-embedded-win32-x64": "1.97.3" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.97.3.tgz", + "integrity": "sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.97.3" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.97.3.tgz", + "integrity": "sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.97.3.tgz", + "integrity": "sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.97.3.tgz", + "integrity": "sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.97.3.tgz", + "integrity": "sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.97.3.tgz", + "integrity": "sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.97.3.tgz", + "integrity": "sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.97.3.tgz", + "integrity": "sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.97.3.tgz", + "integrity": "sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.97.3.tgz", + "integrity": "sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.97.3.tgz", + "integrity": "sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.97.3.tgz", + "integrity": "sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.97.3.tgz", + "integrity": "sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.97.3.tgz", + "integrity": "sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.3.tgz", + "integrity": "sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.97.3.tgz", + "integrity": "sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.97.3" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.97.3.tgz", + "integrity": "sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.97.3.tgz", + "integrity": "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^1.0.20" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/unplugin-auto-import/-/unplugin-auto-import-0.17.8.tgz", + "integrity": "sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.10", + "minimatch": "^9.0.4", + "unimport": "^3.7.2", + "unplugin": "^1.11.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.26.0.tgz", + "integrity": "sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.4", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.3", + "minimatch": "^9.0.3", + "resolve": "^1.22.4", + "unplugin": "^1.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-compression": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz", + "integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "debug": "^4.3.3", + "fs-extra": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-3.0.2.tgz", + "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==", + "license": "MIT", + "dependencies": { + "is-plain-object": "3.0.1" + }, + "engines": { + "node": ">=10.15.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + } + } +} diff --git a/reading-platform-frontend/package.json b/reading-platform-frontend/package.json new file mode 100644 index 0000000..d490b31 --- /dev/null +++ b/reading-platform-frontend/package.json @@ -0,0 +1,42 @@ +{ + "name": "reading-platform-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + }, + "dependencies": { + "@ant-design/icons-vue": "^7.0.1", + "@fullcalendar/core": "^6.1.20", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", + "@fullcalendar/vue3": "^6.1.20", + "ant-design-vue": "^4.1.2", + "axios": "^1.6.7", + "dayjs": "^1.11.10", + "echarts": "^6.0.0", + "lodash-es": "^4.17.21", + "lucide-vue-next": "^0.575.0", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@types/lodash-es": "^4.17.12", + "@types/node": "^20.11.28", + "@vitejs/plugin-vue": "^5.0.4", + "@vue/tsconfig": "^0.5.1", + "sass-embedded": "^1.97.3", + "typescript": "~5.4.0", + "unplugin-auto-import": "^0.17.5", + "unplugin-vue-components": "^0.26.0", + "vite": "^5.1.6", + "vite-plugin-compression": "^0.5.1", + "vue-tsc": "^2.0.6" + } +} diff --git a/reading-platform-frontend/public/logo.png b/reading-platform-frontend/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..456046e315f931694d091a8ad2f1ad7d4cf2337a GIT binary patch literal 135769 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%0A?Mf4>vT`63y<{>ix&7`aNQ{Dvp|^ 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 >n#HP$tKL|x>Xl&uxJCsQ-(RsM+p4` zAvT_&pSGQQ?b%k-1t6!S$lY#KG232eCrSsUwTpyq+D=czx+Ql_wt z=UGJI5C+R2>?mxES*vBkMJda@f8JE`(CV`_{H`}H?)^k!Q2O~2W}OP{Z|{abqFlx5 z?F+S9kO1NZm-~7$7$9Yn5vD3yMK33?Gvm^BYAc`emN7F0kdub(QMICaHDN{6NCgJW;*bIhAxs*f9H@~znR?UgST@ROT1o*4KRkI+J`0Q|;A9Z_ zFToRXgY|4ROjB71^>uG;2wvfB)3;Vv&Q*syrBm^o9I0;eAjljRu>AE{lfJaFQrrFI zOW@`1vcLNle#2-8=B~Yjoi!E#qx%|0UGI|`ZM^Hw$+X6z3TF3I3ykW{pwqLuRzmGOGx*8C6E?n2bi}+A z{CNXX)iQNrQVz1?x@E72sVsUWf0~oPN(E>YDg*o++l?^jZh%%Vx|e0PGe06vbu!^_ z`O_Mok^n&?!Vz==M|qp%+R^mHjso7V#BK{%x>v8Dmur!5U5Lcc6J0CReGo14Fwv_R zGx)qqc=!7+bN+fy{R6-G;8_D@7^m^bG!#kxS+tVQD=a_!yh+1T?h^TP8WO-m$WgwsVSPIB~+dD%VPE~==IA>}8ctvAJ zs*eFo7fJD3tY2n5!)nAXD&P}7%jx+|8Q1CtJNgZhs}>s^A$HEdbA6*N+B@c zlRM3Tr@{OyKY6f@!{ZEzzR5}PoK4S)i1e65g+{aob+2K@2GH_Xl_vhHB{K=0P5i3B zo`<#}C+BQ}CDD~bLAGz?v!Bdo?#9xt-K!jrBdbwi5nw)@S0pA%{6kV(pnUJxho*6+ zT6@n_<3f|M(~BB6*dXYNt|9A*-W>y*D_B?;z*3L>R&CucXNg(-`6dzr-%#^qnT|4h zphT)FV}cSo#j+4tVp1>4xe*;Z581f@6L;K4g6fb|paA{O^1`%(oV7tCG09-E3lm;z z&UarlagkU7Fxwf^*(5#J#Z@=H|cGMW1!RESKg|oVRWxeQIeDof?`hBikE?j&{qZ>hq`M zoc&NJ1NQ*JE`TRZvg&!-ZpsHVsSg}As~shAl4t8n^E}F&-{wZ^QU}U2aE~f5P(o?d zBIfYjL7$gNUps7Q{~iAIZ3x--P`gDx4PSE#4M=|Zef&YFw)R3cWXi0b@H&nhS$!*I zq2~caC^L5HN1mM z^)7MtxY2wd*R#vdNXCri@-}BUr9nm|c7vrDvyms49|~=v>gHX_) zV;B>W7_IbPMi|9@zGD$9c3T+9yc4gc;Q0(eE$5>oi~g?Mh04gW_H?WLM%s@#C6$GH zdpqV%JOVQ}Iu&+}>SQ9^_JpZ6>=)+AXVUTbbu*lkdxIu|EPA6zt9XRMrq6c{=*oh#9kS;3BfZ$Ejj*!i@75I78A7e8GMJh@BlA;JBDituyBqy4q$VVe>2r-l7`=5OFZw7&q<u zXS&ycC!toAszG92kaet~E^<(%tFh-5hqQssPPIcd?WsTw^_c|fbX+l=nnuISjYK|G ze#qTHyv)XJW~c1pm^zpR6WX8KjbQ8jz}J9$SrL7iyUT|xCm`Bf{X!_7fL zqr+CpqD-wVc{0w-#k*dC3Mb9?vhxVsqmxzMF8;i|(D4*-NdR{njH3Te3I(ZyG!u#QQ<6Oq?it$eiHb}h1&sb} zNepVOvICgtD~ixoleF)V+q9pE=u!|J%02DW8^mUed3%fKkU5;^jKReEDw{Bzhpgw)(u{F#~f?Po9WQ|m7AzkBcGO| zKKycuBIl`8eMgv(#hj_W8aPJ|r^38HLC)d?%uI(93oQ6qkzxvYM$?)~fHr3p77|3x z)1~*OM7P1vl37!uPvoAJvew((&f{)UW)gyS`ZVb_-m=J9*UV<;zP0!dhv~>Sl8?;) zEt&{+5q(e^F=MAK4oN8%`gk0BdulD6`o4Sq%&uhc``v$yzb{4(mA0P$Vth1zeWFvQ zMW%Zuw`s-E#3VKU;J_{0r!ULBQA!w$!2G!->mz2l@f_MLw;~vlx@V#3?f@haK@Fi_ z6^;y2|FR%@-9tCYD3Gx&*>|0$e`H#f&h7EV7=1 zh);R_-^PY8F);|tVQ>RUmN@8A5Gj-z7v+pdWH_@JQG`_4 z4KZ)3)hCCvg*G$mwii=er=IW}K=n~rDv-yS@Bfss{?>J2drev48?NJeD^D@iE`9gf zyW1?aA=e7$s4oX!jcSL#oN#?oorV0&^LRNIhNX<$TznYsAqoQj`1m-e_GMXAd^fLl z=ZC4&sm{3pPY@))HV3)}%R=}>^$iU8!~Kw~Hq*hy7>*b7M(bJ|eM3jQHd(HnPsXEI z!78^q9!g`Pk^V4*81sf8`h5tezbJ6|rz}v7+T#)|B$BbcRaiCzA7#*UW*K5)`&+Y- zZ*x76Ox(0-x8O?IJ%Un(5Ku-NpB?W32;w_LJCLi2g!)a3MC$P-6U&aTCZ@NL>zNHmZ z%hkU>{pcd*8L0HC#JK?zk8NO$3Mg#sw?C;hWT%V54w*=0DNO>=f@Wk&^VH94n#Bq& z=IM#AZC_1wm}{INky4c0h6IK9+Lfxg0~p5QzNg}^#8<|C%Uktuq73K2sOGHVFy1Ud zFVke+W^cIZ{^D98(km+zLJ(%9tUHv`UBVAgj0- zZ96qFVHZgeAqO8MLlid8La3K8U9LC-O@|OA5iUp*;j6J_BhplJxyktGRI|(y7DZT% zn{cTN(?NEKZS24t+G+}7ocd-`sTXYI{6_|5J{DY*GN6P4@u$!Yij-687Y!*(X9>ah z{|ek6vBq$(dI>O0x$Pbt)JT6^dsnH0bG;>L@jm=VuHw9iKt>0V66)fEH%-u8&{MhoK|ZbTEi z#!5m;c$HiZcZ`Du&@5$EWX+?aG_7e7g~BMGa7kuI{tUH6xr<^~=S}>LqJ$tiBSaNuMp= z8$u@^G{AwT)4Qe+djYRKM4NrD{f1JOyA}2?TjR3}hq6~E?$n!n^>#<8N) z$X960JX6Xh1Hc&Es(8j~Bl{dlOlWH-{?q_bTAu&>3~vgr3X@4^TtV>MmzRz6+JlIz zV=mFY=@{#YczX)vpGntlbBeFU|4U@X3-Z?y3zBB~pX3~-(L~v@og&<$lUtan?C8(NP4*Avnq2Nxl)MQRLz!>C;dAk7885{nN+{yre`z4s4i{kpdML z3|)uITl<0J(nbFp;9sxa840>|F3=>}joM1fe8JSf{*FF;}(p1j%hHz-=x+oZ1o zNr~G}N%A&k0<#DWqPGv5pWr;dX<1n6wztBxI338tntx?j`B0xVrXP+~3}s77-YP8| zUO}DMNYN+cT(DAWKL#=N-h@MB%kt_fHb#L_4JGW90d*>Wi9R%LZqou5m_N93o{XS z_v>_{@fNPNW(FT1{|@x{&@)x{R()e*qgd?Gj^uR`JeA9E{W%emtqvCppEzW^{2I$d z#d=xL;$c?*hX6cwp(~VCj&p5HW+&!9`K)t1ovKs5lwchIeE;uB1;Wi{eYoC0>mw=h_LJg+{r$c$ec9e!B_lXZe;esu^OMs6Of#^8S7a}O z_Nn{3A-$`%^p`DKVJdFJyv~K~ke7nXF7QGs@kEK6KWpc4&wsMywt^fTbT^$BTU-ZDM6So=;v6f+8sv!=ZFDVs{nE3v;dUkfLQHTK0R0Dsj(hL;y3qH<6E`&` z?Pz+H9_?8VcZ@$SG~KQ*Z%&xb?mnz@L9~)@7ZVGG9B+x^@7k+^YU!18N2J|I%Z@Hj?#92%P)fHXcOv;Bw zz^p`9BsxR_3T6oj2^VMxFlx^@GUnfP) zlZtQN$MA10b1=^h=lhl1q%lMjes}DB`JJ4Oysn}ml?>w#d7>W5xLh=hwEL6g` zva-@$f8Q?5SfWMd+qHOc0h6*BKk>ak@cm(viuWz(Ex6L{t4dggdxOUXgQSS?+0&MX zfz4RH4_c$W>_^KUX+FRu+qIHRUYdz6^vk`3;eSSg0TPOLOrn1pjeWn!x!UNHYMTK| zCJ*|37cb~trhR3I3fC0!N{48{HUZoK zGngI<*vU-3vQdu{SO%xwG5Y?S+Q3DU6(fFIvE3@DLqzgj=HofW@?n=Qe9>VhqtL>O7FbEu?ikS1{Dk7R8l}pTl9{D$DT6SySLCl>*^9n{OG>c z5g0XFX6@bIQ_S}m43&hRvAeWtPdoOGeUARpZ(ZYH1MEgn@BsIpca$bQ;6cMa?Hv>{< zF20Iu$=)HzYB5jjmdDaddDOK1HUD_;LBsxlJ{{?YJBE*vNsTMJw3JR6iED9A^uAeq ze#ku2d26@pc0ukgd^Lff3;2!&c8$@FiulIbBRLy8BBNSp(HsH!pSB{6Y?xQx+MaoGqdYO0ONb#i zK1NaI%~>Y;gmB1@1P2oaNvuxp+qBIu-{pwNDW1XU<_ z{?lc69e>*l6*#g#j1`ORvFJJ1dXdtGn=kT{*l$z(R?-IR*r73J#P1@&MGLKX_~oF_ z1pLd~MXe5Wdl>+f!mfir=@`?U!E?hm2v}svxlFp#oo^%dq#c?&WZhlGPE~4*aP=z4 z0>TdyEeu5tph}>9P_D(1xc#Ny^0#h9Ee93Ri&$DHvTkT#1S=4HVe9vA!7Cs5NsNzjB80D7!{cD5ISb8-Te!__7 z$8cs4VY#^93^BJ6#1xFxqr7pfk z^IbuGTVFWb+y|t(Fq6WH3_cBG2#p&>mvF+%;ugniq4vNBvENXzD)<}JZGRCr^ed)& z-?iWXI*J>B4QPJ>E+;FXAEUc)(ob05hUEs_iY(Kb**xw{?QgjV*4f6T)4CQ)Ahm6P zv99EVUa#U&e+>sw2a9RmlJJ?< zqo<~}^`?zl{&VQ61>-4b7%<=q&w`8WtEl(Wr&9rI)2DTxTyw#k=KT6K2ZnB`X&|cH zdhnfW@OtLs@tx<3W?NUx>+dp5p)E4Q(Oz>&I(dkKH=Ah%w_!8Nwa_H&w;0fHKpp_Z-*y({?;l2N@a3X+{%<$MXU`EkA z*=Uerj{f-DIH{6=1|rRK-N9 znpR}9>*QgrNH+`}8g$M!ihb;HiZaa}3VpBW@x!Qi7&2WjHNSkU+gx$qtGN1_WQ$$}BA?cE-Dwr7xS z{LVcR<=(rLZ48LaBpa6QD+JRYH#}~GBdCjRkW2hw$ZQcK()kDB6tozKLbVi6Z-Wd!aP3ru-b5+OUvZxQ;s+UuK*=0 zEI1G{gRJ-o_XI! z6JrmTkfB1Z0oEJ1iebVJLXr^x;OTGlkqy)N@$Iwlv~oaGUg{HXd0d{aSloJFAI98o z9CQ0*cQioAeW4YfwK$6MuvRMJXrcd~jorSFhq3p|mCLSIKsOrkz5*HnkQlU<`hOn>@mKbY zLb2eJj8skTf~eR4oIC(y{4g#3ej?=lc(SO6!<^-rc&+`sBRr^JD*@E+u8E(AM*&r# zbR5sOKU`S8KI&D__$M{4n{;LDC~MVrcB z^DNgF%3xn``V(t*0Rn+gaKrWf(9?`m20~ecexDq5o6PIlssnkN+nC0mF(efnWI2_r zN{oT1!*UwWVawSAvhQugJ-hf{Coka3B9{xu1mSt{yqfn^Ezn7atvxj07o5fbqmO&Y zFVfl^3JDQa(h#CrZIhn#xnk8k#VvoUShp!(yq`~{UBRL@HMiasI1C3tImr6qzJ|jB z07}4S;ALQn!>B6)2nl!~P%^X7U(NzaAJR4xQHmS=K_S~YyO-FA}Z&z8R9aVo({2f0fLhp$q3 zWUmnfV&Y+8#&32IWAxqsH8>a;<+YmyR>_=)+-c)W!lVl&G^8s`pL6GW(?(BMGd)q@CaCA<;6mUps{Tt`2OX{|>pv?Y!m%w=VYnqNINnE+VeEs< zu#LME`x`~}z&K8wm1kWaFsT!|5#m25O9|73%8cfV zwd-kOM9%i?pzvukQ{6cu@hykvjWP=CyKi}?R#GK3GQZoui(5?hV2b z${L01qh2B6fvJ1`Mlwy<4BxdI_N*X~D_({ua@g`H9im*t=p;Y9FAanW0#@^Ns2h*< z0_l$Okpof9J_qCD_60LDmSAjbi@z(Ff zcf+`9{tk>Ka4Wz$E)JV65_%vs{ee`$DwWN(6g{rco;`E4W=h3XQhB)>W+_ZQz@)+9QKAT z*$q2K;@d~HN~Lu6eL~UGgTuqQ|M@;uVhAfqYzmEtQCR069-r*M3~K@-Mb%EP7-Axy zhJ5*qob^Z1S%InS_*rUfRn37_7(a)-Y0>XC$w=^KL@q04zt^i#VGo2!4Kaejg^mjn z62y@6pPJj~;GZy4%>55t3%u|V#K4}>0LcI+qRn5pT^nvuv-KQ}+vv7XAs<>;t6t+y z=x}VL=J!_DMUqBn3$6Fxhy;w z4Zd)3r3PUv%o~#1(NPJW%SIV}yv2OB1-Ol-rGQGYXG4C2imU7QcqF9Z7@BPw0n-H1 zvHu--WAa^|o5{a$u``Pr948j1-T1uTlzA?H=JXs)MSwWMjpjJvJe+t7*Tv2EDGsK+ z)Z}XqF1N#(V8@g7N}KbPpNFe&YPWD?SJYmJ@gCAIpRrSh!C41$1PC4l`l((=lNx(W z&OJ2S?ta`{6LbEaKKQ8RS$K2m8n(`r7!L)cO<^Cpa}w>f#Ldn zp))vsX;m55%?yr?>@QPbgEbRISUe06JkcQ^X!Y5)?-91XquTG~vB$o57xwNpOqGxN zEx7#CYzZ7~+Na-iMB7}y`Is{;akvz&qoVIC92nT4Ak=W8Xqj0h`P2S2@Z~?g$GHQ# z)lRqSAu)axo&>OKzq)59?t;QADW$}?-p;qK>KWD9h424>3oa|T&8FH%%WW`8w4Z^Z zi5p!qyeF)ozqV*gN2?z(QBiwku$Qd%Q3o*&Ru$O227wwfsm&C`JBh%rXkOYoLShL( zbH_+0{Ngu&AAf9zJmgagwn)(UtSvZoZ?wYSZhN?@CmM3+#&hZM*W17F<3G<BLfe_a@M%w7Y*jtp>x)rM_w0J4mO8$L%&IJq1F?4k0#? zrw$@SE|G9;Q@?!(#$IJ^GM&4&szWXnA<=Jo9qEB~C3|&pe7p+=!_P=51~Cvg@}M&c zEBYnf-BTt?6{29A2zE>eM|;miEIKUKYdO0(XuBu+`sREPC!JS&;!i=M((=lgi)$^O zFIxVjHkV1kGh^IvwvQkg4kHVHJ}-WrF~fGPS^5UY6O#Ma0--7LAA$JnFTc;$ zNxF4L#q8MNU^bjANCDb3q!CHOE5aO&!Am_m!nRQ|dFC2HY&O&M6Si%hoDZtF*H_i&C`OHdyx*~Ot3 zw_wRy;w-Ad0tG$80nz+J^Th_C=2$;gf52V9a0U*{FOV<$;|W6*>d~ef79P7U=qKR1 zZUlqiGX#bjh0LYpSrmS!=(ImX1UvaNM~AR$o`4T_83L#7Pk|KJ1!%hnKN&xah5SFF zWOv666vimQ#hksewA2WGj36ZASp@Qm9pUItI_1)D|7^&eEY4fp$L*E6#h&2V_of@J=PGbo||?UjKPUg(!BHz(gXw>U(G5awF=Nce33#@S4Jig9O7sS>q3=-qAzq1d|LhEE{JSDC=EJBwqi0nDzU9 z2Bf?NnU;p|crGg)Bh|C8+XpGLnwb{Y#nOqjjdGTSxU1h26H{p|h9d-CTkw*AK)e7< ztOzq24yF=J1Ss5tqeJK;3%CvY?xlv?#j{QQ+3<Ke_>e4an!|Lprp#z_&$5 z%%P|JSrrtIPouDdDV=g&!h)Dpbycj!2&ajZ!Qu?bumRs)(9r07tDyh7>x|S7h|Z5f z5-pe3*9T({1R2^E0aUN0HE)Cj3lT_m-UDX{{2Z3dY#0fpN#q^cd~xjr44IN3$lvQw z^#Tp(5IObCc8h~d5xWW#WthqGuk8PVg-_J-VE9a2)ZJh$o_^)G94UE`G z7}yFDp8)raTGkotk$cwm& zybHQ@ZbG0VPsGR{%>VpL78PefMacR9(q^v1y#t7?@;$hGz;T8=e2{v;9NmaSi||}* z^B|pN5zu>pD%LMoeB2c+y@}W`kPHV6;fD%*(Il6O5 z-PVWmrUc!zdwvG&^R4U=A4hVy|2lmrw{UTAah@i*{3!;}&eOm?i_FVJS$%E~n`mVY z!}`~JB-QaXi?85mc)*f#m5u$+Hhc_)OHa)MXFgCjBzSH2!OZ7z_d=5Ft?@!<+<3BlojG>3te(Zbh0&Z7=LvBGmR?L=HTiKKDXq-ted_4haVvfa3DrVe4~S#%ajuHxUsypgNLUv&8ECJ^ z1mpb@(m44aII;$|$*SX(FVP+5hLmpZQV*!3K%j97VT~-XMFzmRApFcYy}qXfYhCdj z48gWL$hrWb;*IZbDGA2iI}qarDPQi+v0cl-&R9_GP`@R)s`yp_QmoMr>xfge?T{{{ zc(vVl!bT1so2w*v!FBc5%j4qT^Zp5~5mbT=9puC&8{n;g{}s||&l;Fy{c4YWCn77Z z4m;Ary{&oF???|r2hPQ=>O}w~2LGlnP-o+6=lt6AAwr)b^|^cWBP+L^9^pMma>$H= z4~8fFCK*fnKTfcH{u*(iJb3|Mz_#Ynv)O76FRV?6J3C_7l6=wGKkDUQWZdRt;}y9N zl$XI7r1j%H!OQ%*?!`p;`c#n0xHR6*i<;|ewR9R7%hcmx?O*M|7*|5o{9AOr-yGaSU-huJm>{B6VabRz7zMe^60@QS6;J@>sG*Wo{uz7 z%Ya1U^FO%_yuh=qgV@@89^G@I)#l{Ps(LVKAv0VCsV4z)Pz$ZM4^L#4CNY!Qn#6H> zdQHAMj*M(6PR`Hg&sIemHh=#Y4e4>@q&Xg(7T;#tDF8kmSQwEg2)&$uid$xkI(L_Y zw<``uqG}#m_$kJj=&xY}6=cYJ?S~1f4cNi;m`p09b?BE7=JOLGW;(hi@ z5CWgJnR-x*j}$Np_Q;d#%eR*-2#;5fzNY5f*(oH=9$FlE9a zDw)>Ko|}?+czoaM-!WV@PbDCo#0^dY3s1akav(W;;khCGQ%_^_>FfYLlYVgZ&!)WW z?MKS8UQamdw3-zhnajp-7x!XU$rv8qc!XWP?wM8PaGZLc>-!GRn8@fR<{60IhLHaO zx%we<1j(4un0j@6UY$9xwYWi0AjB#AJ-otGF?YH_w8TOV;w5p&rapx31|OKdR-uq# z1}T*WE;0;$&7R*VZ+BmsNGI9P;~%UiO(Qp+h+1JT$P!pR`$$Wj(4_W4i$M`6%ePd%` zh;aS5;e55h0ltGIZ1ws(f$L5!SELxFxM?2xG_}TB70k3oy40%zr+`3Kp0MrBK|T>E z1Rp|-1K{0nnc?hA5O^MGbBU|-L;@Z-M<5<%-z#zRtuFOz(EUc4m(7}qJk?6(%=HN0 zrn`0<`&3dvk{OqiGK@xa1*w~cn+H+FLB^mmD;%8}zwh^^&xy!vhu50p@L!JMe~f-& z69F^<4aB^r*Mlr`3&syMOl(DlCkG1@91UM6ls zwp~Ax{0^+R(C}C0*Ham0kI89XHk%lXgxFAkLpFJCMtuLz1UXXSWF zez0WI`ap2qJ+ik;YHXkUo6?U6aj^addq1A~lNRxpw1KjL(W_XI9lQg((Ea)ODda*- zD33uv7Wm3m0*R?s6mDkDZB>b)0K-E|vkT38yy|sBTSJ*1tF{drrlqY?0Bd>JWzG-(H9^iQl2(+BY;z_^xqZ3FIgp?}13utc7l|%Fl9;QnnQ&_yo>M19u~XI-9+d$yKkTi(O>^9~oSxF%i?% z5(Jzq0|2oKGyL@kxpECf_+b(ouDuPY9+ul6@SbuN182~>NF-kR!KAzW89>wRwWO~b zs5{Wx3tc@giGo{lqWYfq)Xp^-t8r!{ulCK&^6g+8sv4fblbK-(0vy9MvB-hlL;ilR zNwUWBIgwPe)Cn)t9Me=T_)7;)8gch##Kj8ByXcC`+mOlZ;LmeLcqd*#&?A_-;5|!l zggLirl5vvi=v3}xi!U+~6AARY9@V~wl=Q(W|3c*nhymKhy4xjJY<64f)c32@AwI?J z2~a{>@HUan$4b*SjBAQZ?{^s6wEg(O`ze4>G))IfWYHZugC24Z6eP0 z$I#smUeVLG)AbbpX8rvEqM6$Bc28h-M`9ZPp3Tms=`fb1-LCucn-aV+0aMr^TQM7zG z2?6f#W;=sLY8i>!M0#8>F+OW`>Sl@T*9lvPL3WXGTR06k_YfmFAhJ+W=?1&UDJkNe z^n@+Y1S06v$7AlJ%7FeDKOI->lo4H`trF_&<-3t_gCQ#U#1-bW{|+o9j2L!^R*qee$0F`xR)?e{n9p>LLdJ_wB|$(_7@^(vuN-pbjfQwbpxK2*Z)194)6 zFsdG;$2NmKcn==Uw*V5*>@1?&XIsM%K@ZS#%wxAu3deA{sd|i;AoB^&!`Ff zSzDmeor8mp4qPnu+qp}L8U_Q^OGn#%my6y1+C6AbfBtZ|Q6>hlEx_mK)StOaGLXb^ z$>_j+5!d87MEV(OyK!hhy!h?UkXocW1^g6z*zTg{r89WVPqQR@rs$5L45f$mmA=il z5#hjA^}{2?Hv|(FIsWxm@6M6XTzJ4fgI`2?r)n^bSj>EVk)Z$v)=*CMPgoJ`PykfC zhkzwq1{EYA`~xDDRX#WtV%I=r7I9xi$s0_sAan$-YYx=nVw3P{aBN%lmH(;+1``~h z8;3pZnC`X}4Qbtd0%4$DJ-YQDsUYiF# znfiwwI^6NdVz~;`93yh+l)jj_hwzqw5iYH*33`UlKYlNfBmIgc+5cqj06h83kA_&um;k;sNIr7EYFalQ;$?XIRfpCM9Lsxg%qSzJKx{9 zN6R!`ms23R5*Tg^t3T4GgHSe`u-_vYPKa+BJ}L0>7{>7M_Sxq-F?v3BeNbB^%@QO~ zHDn{-fOdCxS7H3?xJk}%_3gv(H}BHSz-t7b(Ppr+M1q>kMf`E|yVvyi-z<8XA*a$$ z=CbGy`n1t_gCpdof*ZsXa!-**32s^V2l8{kNWR_~ma^%0;}i7(OeHBC8Y`9>m1BXN zzXjj^nwQ67cJ)Kp>2V0932j#N@m;Zm$d@33qFTb{-he2YPZ*B=jC&JEdCv-@KMnyh zLj{8?nC&}+cRMw=`L+8**=|$%e3^HJ!dl?(V2de*L>>f1fZYNIHr;xtfZs%BPl#tO z)m7el^OjRTG|*!V0>YM&ko?0QdoTZ6N*|2ZN6U*mK7e0g`;|{qiA-u`E9aek_%tFf zGDxpMYAo%W)z8MCNCG$u7=}fg_omGvhbuHqQf@r^K=O9}y4$KNP(MsD9Z186?=dhX zrbB93CEEOt-DrwVk8k#@kB2Do`)Cj$KrsR<06Q`xFLitsl+1Y$HtLcBSvCK~*9Ct3 zaKYcdU1z!Kh=AfWS;L3*s;Q)BBL+% zP#MhhY%sYRb=gQjudZ`}A81eOvCDjrlg64Hqj zooIX}&rwMZ0{BKGGOKDKCe;ky{(B#hxC|K`9jXf3KYlRJjYFC^w9Q{bev2fQ;dmAK z^gRbMfHlK?w9E=gLsmCj`=p;rJm2nsr%_CphoZyA7S$a$HRPg%Th1tZ@(~kBa0~03 z@X=JR27o&j%Jmz(B!CEmje4qzYZ{!q;aooETRE?18HyZ4NZK94<1hKJYbs%p>rEbE zeW>jtEcZQ=ba`UK72lP>XNG5iPK|)@rA zgx;`gA}!!sfHuw`M=a>`Fh~eo#)%C{$#|{Yi!A$VROIf`a)KZyw3Qi=#Uo{}aIXlI z7&l?nf$ww;qG^!yD`ebQ{HJQN2?2061lVD3VKt|%s`Xsm$0;gf#9t!Do@s5D`9wcqwB^R1UXk07=WCNu`OB_^Z`1+h~9C&RGlKCqS%JNA{#st%0h(w7g-Z$jYG&qlCy zLLgrRJP_01BM6uiaRY@p(j)E&m&1QKZ&2UH_WgnSBws_NhotIG1~@j_k(C#T1q4@R z+n3_{&~lq$@Q!3uRt^`;(L}ZPm^SYgZM})oso&N7ddi#|5Y0kFnU(4=smK=YL!~p# zSH2gUFE=cv-&b9m&wJJUbH;;r1<^Ofh22~Mqt^HwD~cxd+Qcq5Z6#89)a&K&MXL@o zR-AqQA0(Hybq#^m6~F_WQ$P>_QHHN=ia;$e3EAQCu{PR#RqV&G)$z%s=Q1#1^-a>5 z%aW!NwA|^#`TjaiH~qT_ztn>i%=Z z=c!#~XAtk7+_-1gl+r6J{R$2^L0Tb`pS6lBaE@bQzzwQf`QrKqF1=a)W{0MTqA|2ka0Nh`?EiJUZA6KitqQ@+~7NY6)%mbqDZ{s{r$DQ9|B1K62q8HznSv|K} zspwR%J;iHzAB%l6A2Z-jaU5f4+-!e@0>`^hB&YIV2Uvu!VVfUDScfuRew(#e&Ns z(-|c>MN4j0N*&sosXqPl>yNRI`MrTHCT8XH*d=o!a(tO5tV+)y z@*kWIG0*RPyt_D^5%?*g|NK=1Ot;{B1n^Jh81GC~>bL6pJL=M1ooDd;iZijQR~I#rJ(Fe2T^s=$(8 zZ%lg|ANS#3l(?l}#yXg3L^f)7HX)S6K>cYZSr>q4x-C}x^o08XD#Jpkt(BO)qTw$+ z?K@`?0oSW5-P&#y*Yk~=LGsg2iUm^$=4Yq<_OhBD0fNdw!U3Sp*%W+4K6XTvN;J~{ zJ6aS~5uAD#t;qgtlJk2Iu-GuIO+FoKQJuRKfnU3M>0@#mw#FwQn`s8ve^XI&5f{bs zPYMx9gvn(gZmPF}fWUB{L&JR2{Ja0f7eFycM|OJjbKhWbch|)_@ zTOS2zBfgV3di=x2dl_PNd9Lk#^`gNTm;Wia!^yuTs4`syAet(Br|ngi)L4cP7(`$;+G9cb^qJ-}t`~=cn^dxn`2kNMbm#AIdra0c z2<5N<)9#+{{$dMj2Eb%6w3_X~bO}RTQG%^8?|#x#rKeBRIHjWSjK8j7`DX}u%$5wd z(q8s(G}geeQajKnQ;5#kUpQ!`U>OZo4fs^@-sfR^DTQGD>&u>Ucvrq(W^n32%bxU; zK5g%+`1c2knwNU?OZnYYR_8CJlTd$lk{wk^PF3aTb6zZ5e-wergu`IiDls^$8vz$1pHozCA5CAn=r@02rYnz^c)IPUdg@yF^i}s&`nihVV?}ddbp0Gpdfca$@mJZ``aV@>?tT`_F&p zR?Y4rXCGXOXE2msLzq&?t5fMUhO^iSM<@Kyvrkubz2{OvE!x4XKmuR=UtkBM( za*@zIysUC=*#6*+1}~TlVGZb?oV=V(4!;0*r%Fg+pFX_ndBRu9Oy8B@d+;>*88lZ5 zeIyPnr0iZXNkbQ3Um?5~fpgohfE|alJPD9yH{j@gKfu2p03$$Rf{Qh${%*ha+9A$# zE?57-DXLbSrbn3?)vPK}8%ucNMzEdmuG|Xd-8LBlq+vy^a~+r2hL-}JhfjFf{7F_{ z9g;JQW5_|X>g@fxh_W32Yy>6mzI}ryZvwC4ed`WIwT3ORyrDt~g`h$8?C?DPt^e?; z)o_TgnjHmx1{od^FdA6A@=kO9q|w-dE-YLcI)I@3{W2+(m-!v=4CLe6?8o3!>VCOf0OLCM_a)}N zRvxdb;34Ck)X+~!3NPEQ{o$?YK6YSMNjlLGzlc^cDM#z@oPMl7&~S70o>`ppd^_(8 zl08r&Aoql-HLMsAVKab!UXxe&uVdY=#SIf%aN>%6y+5zuJ8SAHgwdcs;wU+4$sTY;J#jv=D4VbvHsJ5RnKivBF}Vd=RHu@FRiW)APT862E#tb ziSN#-F}I8u1O3S=ZY5Ogeb+I-WVsc_No44|t>IM1^|~zWj_qiB+2;eAztdG>~OgJXm^*a-{PyGaOsiQpZau)HUy!tD-4bKpWgA8I z80^JCowYf4_PY^9?Zp>vd(U3zmc-pIKCz!~D%)srqzC2OV0r3uh#E!ODG;;JOGXp( z5WgqE+6MBgmnI`{Sfpk=%SxXfy78n>z3CzHoG;9L@Ltt&%VqoroA4E07-*{#Wu?rvk@Y!F-RU4eVUG@`pK|ncdL1$ zzK&JMoorh};k3do;Eu=$kpWBVZAi0)K*t;{Zk1ku=vrXw4My^H5MBz)_}Bo3c*-@o zVNu$RecdRktcZzt*0Hm4S{^fWWMB9+*nk08FBF5fo;Q!{zc7~n=rC9%W8Un2;KK)k z9)4!7@zrMuHh-`0(LS}asrS67NFQHf!1l#JEcl?s3LQk=MJT_T^o0vYr@y%Z1FU~p zbxwh2;jde02H1Wlw8d)oFO652{wyWt8WelXx%pk2BVTs#5Ri>lwXz{35c4;refmPh z3-Qap-B1bDz^8RL#$RjW+~9%0SKJHls?ICyA0Yng*PRulhYtalfbv3s5-o!=DiPKw&ANb9w^X8zV9_HM*?Rddjc9t>N{ z-9xDabd=MmcAB?P)}I}JpL*jZS3A>JfN2JczlF@`Z4kFZMt+(b>~s#@eC-K_j|UfNK*iSavD+>N9e%@$PqV#jP#;) z+ZHa~-)xUZUdq-jj;__n^T(Wd*NOhx5J8P0N+bxIpp7uynr%H?;PHMdwo$7IST@SK ztXU=ZUZ(L;-%n{Z)r7I=w*`~)e?keJZVJ9cWseiKwbY@K6b6>dI3I@NNhF1{kU^gh|lUBk4;E>d$-5kF0>JO1ujU@>*A-tNmbCDi;NI1f-76R*cE zA)Slo&^(&wfBBvQ1raXj{LDzen37{kK`~9ee6-qM5S`P{*G6U?_V)FJ8KzFY%T#$B z#<_Rh#^H#d{<`c-){nu9p5VGh9Ot#< z@87HS6`FNt$$^aRoK)eAF{u#~MJS?+NW-qk=A9Dui%ZnJy*W^)aTSpVxdF_Jls%e4 z{A&)5_#FXh2W$}@4RzI~ZH@3`SGighLW7~rd-3rZsa`v-qKV3;y(yxH&>0#GFi#$& z-RQFm=@x2QN@N=A9a>#@QIa&g!;@Me^y|?rYdglLPU@PijJVFqoANg0pG34mLX$*% z7md6-bi~)ah@lnL9)jy+o?YJq9C$)kJbCJ_}v;c zHe>jHSD^Q=0vXCS5iuyzfCmPCWnEl*88Pp^RYO#0;e=#uVf8!9r!T*S#(Ad7Z8%FH zry;NsGs6w%La9)@|{T|aq?yS4!b_NqTr~5NxRb0mfPfBN&8oA_6I1mY1Lp~ zo4zHTl1s8n&z%4}!A*YgEBhzYgD(oTzW+>@Nqvp7mH_c5VvPDvNDN|P1Se-T@2K}O zaGgJBX~!uFKQhn^Xy{Vf2b{UrLwHi1N68(zJhmo z46D?Dr7~Ip#<&UMlZ`Vk)fS~qi#pU!2?=btH$B6vak+NZgI;(RQr=F;Q>|qafi!d@ zN~+($uSmXp09VO8IZ;+)@0HQ^RZ>!;K79n{?ZYUW69JnO!XZ!T2acWgNI|wE{6Tm} zl!hW_cH6u-So6mXAZp@b$35pROoqT-T4DhA9NdDiumd+&4+a$8+LSS#p#XqDYE5@x z!g^(3kX(J)n&-P*yW=Zhm=9=(0=w@jvVUWoO1n@!fYA$-9``82mVt%#Z!(@R1F_}x z%fZg@pN{n{_FHOU_rVb9ybj%vdFRmQnG5_bQDE~sFYC%Q9fN&|6J7D!b7TN3gpQHsh_nJ|3|O_U0Wnc2L);9 z{?#K4-Z8LBT)L0Y-N-SK2GHs8-tg05kO}47In=i*w4RDSEikOs!0rQ47<|W87=MlC z*ITEao{M8FL2~zKVx$sNF3Z`BANI$XqT)&2wuY<&|At^>zZ#d}Rrmgx{%Oyr^1M)- zY=`TtZNcKNuQ6|4rTx@D>)Y;YyJRatJGRM3fKo%g7|bRh(T6GvBZPq(#(2dX-3Q9y zH+zI`st{LXQ-3MAMW&6`y}Pvi<#8%waunb10}x^FNJcmY(>sNnujn-7Pr1xpQaN~V z_Qy+V?d0zTIz@CqNdA|^$WHjMgc==N_A5{jAR(hUerknl!Qiq?I~iYm1wfdc9eNMt z8E!9s0G$}G5XG1!$IVg!Z`!?nkibg+_Z`92^9!g-Vf0Oq9k_@WfgUh%Zuxvk;C&cv z$E8#6^UDJO9I)=$P0AmE+vd|5#*)$NKeE_;@Qy->%H(nFPVWF1)qN)a*hxL{zPY)5 z2Zm_z*!Q;}g=_1wS`$Wn9o6`#U{ZM8-Ulwa`HW0wB^PIo6=fu1_s+Z45=c&hcLv~w zVt0nI(S_&F%B2=5!9EZI`5^g#?_(og5Gg3Y0(H?T7W{O{!uEHMq{|$*No7B+Xz$W^ zNB+i(R`?O;{xF8~0bxaLj|&6sohoJNNV~vniV>Krz!)(JOYI!gmGdB3fH0;RnFxB> z%?-+Vt&19hA_JK;oWV9e3#mpNNZ-a8!pTB3XR`oW+#gU^O-Wt$h0dOBx7$@u5)Xj9 zw$J))d}L9xp+2RYJHQ$c8~RaglA8B0eAz`yvJSF5-lP;HgIKlg?d{Z%m%XC+FhCN) zMOFwqF|c^b?%P$)E1IaX{4=@-AAWKO06Oq)evH$)ZzlqA2RUk7ULVb7ZqeK2nR2*FL!?EHHP$3nXwL= z%Mh5VHLm}7?3Wlt-rS6Jz_8^0bAX+{gNH%g8yvETKWPwP%P-KoMCbvTq|%U{TCKXm z8GFznGg}q?Hmz&_b${J27xPL|MxCDnSB+CQV78}vp6bdf2~E2&_{*Y0-0xV9=d3Qw z-J6Om?pH|LI#oVbu50T8yq3ri#AtJcGidLdW|!*b1kvhJujt#!6K2<~WK5O>p7%Zk zD-;Z-uI&b;?~Wr)rSMN)&i2WWK0&xYWTafNhZgdjOAr&j6#6gS)t?eKXKpjF5c3Up zvQ7Yg&)zG$o8Iro8!NLMeIp-u#vaQ+QZ5K5*vZMDG)xpRq!)3*z&DoX2BZP9?Q=pJO+GV{(8Lpy`eCe;Xwi=OvmG!0!;o4%c-eH!iKd5`c}l}4s9r%McNfv z&GdTHW`WPm@kaVjjRQ z)LA^1Ia;8iG{_sTkqu*?&oWKBB5`U99CGBI@~b-l^CMr)8lwIYlgSo{(N+=FV=MA- zf(|6%xzZdDVU{4{)=Bo=ZxPOS%XlJo#zmy@l0f^;2$|f$j(?uTvnT0TrsN7?5fa(p zuTY8IFe8nyGSthoVKy1pPFivi#Iu%orzx|T?oQp|e*a(?6yV_Mym#*&G|M67qJ`Q< z#MwLUpU`lc04Z1TV#2yUcMlCxe4A5S3YNIrhv&P>h+f8d%&rpBd}aV<$v%hTRzyk< z`8uP!bVLoe6QRy%Aor*o^PkYL%hU3BmeNaQW!KSZe<`Au&ob_MfsQ2j0M037_s%?k z=?SKMAmdykFZM>|sYR{1YVTYlG;jgUZ!+Ar93W>isf`Y9rF2=Y>9}8qSqKa^{Mj>z z2!VZP8PG3wxBg^N$v#8Lq`FI&nCE@x-}!8V(py~$B!p1JAPP@c2(+{T6rP3iez^5Z z6&B&9hf_rw=K;#mavKH*uvWk}j_int^B*xrBZOk9!;G}7BJ~~RM+`Dj#AgoT+C9p0 zdlgm|-WoX=ka@ENu;wg?3$YR70Q~f{&>|8Wxj0C0Hce@TB7E;HKNYhnJ;?fqaE^hC zgAZC;5vwHq>k$*A%n-p+j6t>rA029@AaY)JCdKdaFT!U&Y|NDDe9>$0s`Ij`#*$T} zEc6A3TMz-oDTL`f?2P1&Q$(s5$ax*?!Q@=7-5)8vUIF1#DxuDxIb(j-r&^8WV_KSr zjoon;ijxEuv`&eXpUqf=n&a=lRGBSy?_MnEKP`VaOxjPTYK34@?-so*E}8I_$EFot zuA9N)Xb1*|5>Z2uBFVZCu;mYdED2b6pL|B<{H8_>qJYB$kPEg}5P^7X`ZNAOip}9q z_Ozc0*u|h1jm$7*nctQ46i!T^$e6<)VZju@bwnyRxEb zUPOHmWkeMwYdcn<(lGdc?Opd*&42talH8IsG*lXBks_K((U7RFXh>vLGLq;vHANej zNP7=0D#cApTP0e`r=k)KEe#F6k9YXt`zL(Q`8plPIjQ@;@B4bcp3m3g`54mvDydU} zb-)lF>1m<6;VnN2R<(54dvr0%Qx&O zE-hnhI}ktF?z>SZGiC$%ZOZWxcY@@&|W4wucZtL;Bb7xbW^?u|y z&|urSCXrmj5^+X0u0|(iO-Atdvq7oy)j>ZJlh{^<&w^Be$NsQl>pOFO%Dv)Zzi3mVq`*>oON3w0p)YR8s1KNY! zYnO`KRlQm=8`bNgdJ21Z;O~bKGcgKAL>m#aN+ex^tjmr;j1lLrx{UWFG3+JtB1e+? z1ZAZAf>vG-g~H^?%6o^B*GuZ;Pe96)LDME(?RPUB@{ABIex={0724!kc}CZIK(O_; z!lB8bq?}sm;rEiUsTQ|-tYH%D*0wRhHcs_TRwBCTV#J9N0;r zJ%dZRwro!cmGzmkfFs{DXp8(;-^eO}9zk>J6%e-eLFlg{Xbj5f;gqpwa)#BM0-d(SkvmOT#Z=zpAHB*f!0mqv zFz9h{PQ78(MUc_rO}3nZRwHQy5@y3Q0#)ib^NgsW6(*;df4Z2@cB&hL7)PgKinU)D z+umiqZAHT@lqk*(&k8BHj+2&*_7$`abFUsEnYSOA#XHF7zz(mkZl7vyNM?~oLicx7sNL9Rmz z1>bYMoj%`!vh>?=6UvJyXYV_U4P&8ev+gFFdnE3!jSBp_!{wB1tFywoG24XS7q@MC z{^m}$zVGf~86mIdw-4@4f3h{O-L2v17*nUMrfr)+rf-;4OQW~K(K4H72DhYEdFIEo zzWKG0Gq226gR;BZ*@@nw-FMg5hqhZq1k3?vyOCAdqTA~=@`*oFJ1~lVwAl6g`cZ?m>r|u0WNR;R z3?|F1+(k0XV^;iN?~(EGuv+$EhS`WJQLXB17M6x0l}1WjNC?MT3H#C>Uxq>770w4q z`BPWY#4}aQ1D^z$^CYU*q?1h3Pt3Zij=o@gDmOkPYWav$c&D=_q<>#J6AN%>@U~5J(P=$V(aGW zvq}%_i7Ec}d@Iv<#gh>`QNgF`(H|OZXnN+&zdrVAEoSo4SGniA>#F_E{4n%+lylWd z;KUFjWwg}UH_jxFdu!Bxb*9mJIhxv$$Lsne=KK}fMH=U3wc+czGdUsu>CWzm8E6P- z87o_xAU@_u_0i%}TWFn(&uQBg$VS^e;;sd7px0ESachw>=UR2IZS`9@xjqdDd}({v z**WA~xl!T$OI^j11>W)Q=1WGQe$P*&zjcn$VJmtp7dF0ZpZkoqfihW8Rz_AuoGMm1 zaE0%5!9rPic~P3EGq2PFXT38ceOKFvW1C<=;U};4R)ukSJERm39l;dw;jMcUlrvU& z@!d{-F1uY_wq0iAUN8*TyBY=XW_FK4+*rtD4shO7a;u(bU+0(hIQ+; zwf2y|Uw9N4DCiIYXI;?#k`;g#a~(M+uv*i0qg$D+UZmdWSGzeIQQk7!POZ^z!4djA zE|taI*I8`t8>qnF`037$i@h@ogU3VKrgoAuXLkiL%xTS-kIs*O5&kr5x?}OlAPwfN zMc*6iNY*z_jPB@MUy)V%O7+#jqW!iaJywf?na{mPFYmaUKgMzHv0{GryiCOW`qAc> zD~o?AN1T>-zHAm0eezpgB!0ci#px>}jva!`3zbrQZ3`@_^(9hk?hc3nF@w5EF4ogw zcLEP!*KXU5S?in@!R^J7c4T5U&pGO%9Bb?PUYi>(s{A37?ZISy&z$HLd7AOl)(N+E z*)n`=a?~LpO4$psO+zlfIs;NW9z7o46X=&e9!@EGW&7jCZsFK2*9{T}}B`xGB6 z+ng8a)^N^8K~25-SJzPcly!nZ^W*kGfvm2n5c!2by7IhJtsH!2i-I9WrIC8e%y%69 zCQ#HEnc=f*(9gZM#xD4B`G7<5X{Y*CL6ujshI)M?DZT~IA4rvg;x;H5)R_LXLqkDr zmp}-a2i*%?2?j_5{qrm{yPyB=ZgRZ%lr-JE*}c*nSGf3*``blH)q}c)WzKNWRIem-1#~!-EoTRx{L&_+9t}_kDc?Z@-N+XU)VNn z7u+(e-0-|t&B$GVXG~!?`r`0lOhk5^51(pH# z$Gi^O_FXw=w5fs44_@7#Z5SE0D#_`*QUGkE)Ulv#=ex=Iizga6R19re$*Sf_;hx!C zGm8A8t1qAUUIAqVK~y%6lInOM+4R9;jS{0wGqXIJgf?eKhlr*#+CdgmJXxpRcaT_< zTb}xaX%|1b8rU`+QG3g)ztrfFM-pj5MSIQKxyqHwgGK8vYRxNE*16@g=j_qf%Wchc z6}QN6fG9U?;_Hl3ou3d`uiGg*}^c(WFrwYef9>-QB@rdGaoQ(mFY z#-PqtNl$wspK+Usju2|Q6Ml=a=iCq2u~`UPCuBz=i)$~#Z|8v9%wE}7VMj;&1cN0* z=-qg#20TtnJi=`1!*@au;M>ZFcH@boWdy_c(DI~KY0rUR4f&Z*ih5vE-95zjny_AYD?ks3o;?mI4xu3wo1C{ zZ@-DDm)S6zcy)EQ>vWmzm}h`W1-Ss&Nhr%huo~m(ud!7>IpJ!kxo7V2MB&$DWbZaac#V)7DVDr1TyZy2FOe}(vuRjZW z?D|-zSp`^|2s*DgMX(>>xBXKZ-$pWwm&_}Jdu}H=iQ5)?O)iZr&z0T|BNvn=;`oHo z3Lj7mQ14|0w+c=qQ|<3tCs+6tzSGj&0nRkcrl8lz^6-3Hf&)GX_OMnbsdF+|E(jfA zSXzWB%&R6#VrFhqNiX*)xFu8h$nBm|TpYJgiGKR2J#C)LA;iA3B- z1MUgx*k@8-q;LWE&57XJ{-;tKgSj0Z>cF|oTzY`i4JKgHYFN2pO1ol<64(Yxawa1=&^P9&$@1b^>PP<1w&DeOR)J_ujBT2O{atkr=8*su3Sq`0cbg zy*J4QhV@aY@DG5Vf*s3ivROwgP2PYlX&JoLh()P!oQOp7Th7QOO|t6k{?Ns1CVGDR zo>(tJoe*mP`uVkWQ8#`$!M_5c2?0dSQsxO2lNj8YvTYidtn7U-)#Solfdx}d2|lWj zp3LiizdR)cDR_HxOyn#$NH)to0Mk1tjZB=u*>iuaxuJLhSfy8>MaQD0w7u-|g?qaY z4~qzR#w+f9SB`F!S5dux))7Hq&j^G`7M21P1!6!avqBnC$QT91oB)`J;}0ArfuYjL z=$=E2u|&f3;#fo`di|;^I876It)GA{8(@Q4&u`*DN5{^zf1kRM4-3gl<_g=IKhO0w zIDimNOU)Mng$^#6KB!Le@;mk2nFG50?lNusW^zTo$(Hcqi}9Jq>Ac@d_NQ^(>^rml zHq4UJRzUpc)@-Q#&Dg@Jf|93G7YzE&bdvq@^a~k z;z{t>;mMC9$b}Ev-6m?pPFMJIA)yB3-7m_CR6}t%L!QqI{}};Aac1vf3W7zwVWp3) z364pYE9gM=?Jf5eV=Bx3yQ}-WcsqBXFYyyLR>Jt>z=JKNkfH>G8)4WDn!$_o^aEG> z{au_qJj-~o_7jI^B~Jd>{kwkV+5L6JZcG-^ufP&~m25w3W($bSIs?v27B?2JEfOw4 zNc8iiK*>Y671i}ahXvc1H$`lmd?wOi;H}k9DG`=|XE@V=34zv##~qp*214+N-BE9a zhJ~-;J0%9oQ$olHA|BwLRT7HsKMl5sF{9bWvj}x^)q8!lAZYZM2`kPi#G8bm@@d?8 zs#&OokDcNQ3s|qx7)3#28kL-M(&zXD4US>1;$$tI4{`i5^K+0jzz#CE6_j>tfy6?n z3Y@5+lCh@gJP2}0H~1SA3_c)HkiiPZX%MLhjc>J%*}(0GaDjle6L_;IWQGw`veq{x zqR9LTiBvgfCor zrrZ8Uy#tAh(awVsi-bNui#9<5+yON27N9>T6FNqzE%y2x0^n{G(2gdABCP>9Ij)lA zMU47QUO1%2jnWG(Wog|;-Q{2z|5JWsM0Cb+d(Jg|#0G#b=(lKKU(I4r9Rg~<8%Fb= z_imVDBL+vBf+FPYDW@#JUP~l<_9}eYF)?VD#kci*gl-}o%T&RgnRH~T<1C$Z6#oT~ zO@Qz{Zlq}wK~x~tG{Pr&oxHD`xVc5?l$Xqre@lHWp%chBinT~rBEHqk7D|C< zdoIIlj>za!nWz?pXy&Hsbo*%{X}uSU4`b?8&Lx+4snLM4nPEv+sH}o}A2tG;f%eMq zNs76W+o85Wg<#uaEM9;GZgpFq_Ym`V3)gW?tQF9!hzO;10gonz9lk?uxY6-V!}@2F zG?WHl9tbS7?NlYZ%|<4UfBnN);-j%#vy5|PJvwXSG#$T9 zqAcRWLs`XR1535l#D`5}-8x*qJ_r?X?=8{b5`wRMI#`hrL$;&WWtl-4b}ry0o;Xkm?Hv61^+}`1DMF>+>qeqC4Cp} zRMjw(8Q`!3sEnS4>NZN4*%M4{!aL&#xZ#A{5c|YOZ%P)PToFYRV$}=D`5LpXlOQ$_ zqlWkhvDyPzx|MaBPDTm*hFq%ThU&x35N|v$8o8(d*!>E2RHAJsT)}hdq+dc}FCO*s ztRLf1NiJXv_&P2fmV;$-;bXyC0zf6KLD1D$VxI$tla6qrbx>MO2V@>wBbLl&vB0ub zso7d^qjEArX%@Fr@eBBVM}wh;nXx3dl!zK6R8713+wsm&VWvkqr!df`{`bX8NRQ=j z;}|QUQ)r-D=x_d3$>3wBQN~gD@kXWhDX?NGtm;p1Ij{9IZWNky34Zk(_ZevrIug z*NArPT_g2u^s+cFmmQS<&j&odmz)ycYvM?^aaCJ3?lT~oVxOcKk+lgbhF>>0+0bb6 zFuI|n<+g5)2>vD3PvSAqLxV6B@k79uk^R2zS)E4?q*b|Do*E>0zki6*O+4(5L}58W z1+rH`+`bttf8H7ECRL;1!}!k>k(0H`@*hjP=4d7p7`FEKvh3*y^$ z1eAw&W{(#Wykdm(RQ`4QuCX++ZL@qw%nP5!pVs*~VaDGsb@!$eXZdm0_O1|P&c4ox zYbg|2mSeH87e~g_e#txKRx4NTK7kQPpFK)_#Ptpv3Wz6MzTD(P58X0o5B-FgIGF$L zM?K6ELY%eQ(T3H6PR=qQ<{F6=keQJJ-yL7gWWp&Ls_D*Zu;-JD9=T;pGNWR1|q(h90> zxCq1>T4p+T{)`N{F~G&xuA|7nNIH(K3fsxEOUCebpps!OS%S&taoS|$sNDT!o}RV7 zXvAN{$Cn!)W7pJW+r0Vj8z|*CR+4PKrS`~Y=Gk58EbTuH%0DDOx=OittZ{vRzuV-b z-A{Gha|nSKxg%qdO1PLo<65rV{qvCKeGXv462pf2h3pKLpW&KLJjYC{bFPMqk5;Ew zuc(^lT&->TbmrOb>CRvczQwYk1(Fq|bg!u5f;$YiaR5N73RLIF8>TFPk@gzQn-tKz zV^cbW^Nv7=6l*Z{Mhwm1XHj9VKbyqsqk5>L63snF&aUAxdtG)w@qFdImK2kl-T6P{ zk4I-XIQi0b_$Rc@d}P-4e^0n+6D=Mz_2ppp+0qUp7Xka#Fo-*UBJ1^TUsq?_%>DFj z_R_Uqi@g@-;mIGTvvFC*PK@Lm)D$T^P;qSpUg;_$)d_OF8r8garSOIA)W`LtFM^y<^GztWAgtHL{xm4Eg zkCu5L_iL%yI1{YhC7D)T6>#2;Cb@iK!(3*UMh)Vz@0tL#z++%>OHZ)=W;}IyF!tp+l>jJndHojE#m# zw=204vB`XLsi))&SNPC_hZY{kfg=9L z$aMHQRC}Ldyjja|hi}1`g`3FV1#1(BKBE?pTAJn!xLN@ zSj^l0Vgd$X_JzY_aiR+`VL+?1DD$&VSl=t zK03>m*B(R9)}}kZ%1h=N0w0zN(2Mr|*FVc>RI&d4jMzc%gp>5%f?LTNI!;o^3ks{l z$;26a5K8}jW5Q+VGjYU9JCeRf*a@U2U})de_=*so(R~JqWvd%5TF&V@j^_eW#TlKi$Tm*c^75T8JY%;zQweR$8eZCFfGV zt#(Fa#l3+fz#SZ4aKLZg`wvm!dBuAF_yby#>eR34j{GjGsfLQw=g}vG{k2v|@(CET z`J^Ia*Xc-B$hYD`~y9Q_(74wSm8;&@8W8XYHl~Y)OKD-GsJTJ zJy#O1`&Q(P)0HLkhv*O1!JBt!i@~=v0{*VyB}7N_fBnoqcnqJZ_1b^P6Sn+Zrhgz7 ze#p${?^`2k!B&mG7yAD%^Zz&P|IkXi^?MCRgw3F;_QnD_{MoO0XkVtrNw5C_J|$*p literal 0 HcmV?d00001 diff --git a/reading-platform-frontend/src/App.vue b/reading-platform-frontend/src/App.vue new file mode 100644 index 0000000..b34dbbc --- /dev/null +++ b/reading-platform-frontend/src/App.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/reading-platform-frontend/src/api/admin.ts b/reading-platform-frontend/src/api/admin.ts new file mode 100644 index 0000000..32ec169 --- /dev/null +++ b/reading-platform-frontend/src/api/admin.ts @@ -0,0 +1,215 @@ +import { http } from './index'; + +// ==================== 类型定义 ==================== + +export interface TenantQueryParams { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; + packageType?: string; +} + +export interface Tenant { + id: number; + name: string; + loginAccount: string; + address?: string; + contactPerson?: string; + contactPhone?: string; + logoUrl?: string; + packageType: string; + teacherQuota: number; + studentQuota: number; + storageQuota?: number; + teacherCount: number; + studentCount: number; + storageUsed?: number; + startDate: string; + expireDate: string; + status: string; + createdAt: string; + updatedAt?: string; +} + +export interface TenantDetail extends Tenant { + teachers?: Array<{ + id: number; + name: string; + phone: string; + email?: string; + status: string; + lessonCount: number; + }>; + students?: Array<{ + id: number; + name: string; + classId: number; + gender?: string; + readingCount: number; + }>; + classes?: Array<{ + id: number; + name: string; + grade: string; + studentCount: number; + }>; + _count?: { + teachers: number; + students: number; + classes: number; + lessons: number; + }; +} + +export interface CreateTenantDto { + name: string; + loginAccount: string; + password?: string; + address?: string; + contactPerson?: string; + contactPhone?: string; + packageType?: string; + teacherQuota?: number; + studentQuota?: number; + startDate?: string; + expireDate?: string; +} + +export interface UpdateTenantDto { + name?: string; + address?: string; + contactPerson?: string; + contactPhone?: string; + packageType?: string; + teacherQuota?: number; + studentQuota?: number; + startDate?: string; + expireDate?: string; + status?: string; +} + +export interface UpdateTenantQuotaDto { + packageType?: string; + teacherQuota?: number; + studentQuota?: number; +} + +export interface AdminStats { + tenantCount: number; + activeTenantCount: number; + courseCount: number; + publishedCourseCount: number; + studentCount: number; + teacherCount: number; + lessonCount: number; + monthlyLessons: number; +} + +export interface TrendData { + month: string; + tenantCount: number; + lessonCount: number; + studentCount: number; +} + +export interface ActiveTenant { + id: number; + name: string; + lessonCount: number; + teacherCount: number; + studentCount: number; +} + +export interface PopularCourse { + id: number; + name: string; + usageCount: number; + teacherCount: number; +} + +export interface AdminSettings { + // Basic settings + systemName?: string; + systemDesc?: string; + contactPhone?: string; + contactEmail?: string; + systemLogo?: string; + + // Security settings + passwordStrength?: string; + maxLoginAttempts?: number; + tokenExpire?: string; + forceHttps?: boolean; + + // Notification settings + emailEnabled?: boolean; + smtpHost?: string; + smtpPort?: number; + smtpUser?: string; + smtpPassword?: string; + fromEmail?: string; + smsEnabled?: boolean; + + // Storage settings + storageType?: string; + maxFileSize?: number; + allowedTypes?: string; + + // Tenant defaults + defaultTeacherQuota?: number; + defaultStudentQuota?: number; + enableAutoExpire?: boolean; + notifyBeforeDays?: number; +} + +// ==================== 租户管理 ==================== + +export const getTenants = (params: TenantQueryParams) => + http.get<{ items: Tenant[]; total: number; page: number; pageSize: number; totalPages: number }>( + '/admin/tenants', + { params } + ); + +export const getTenant = (id: number) => + http.get(`/admin/tenants/${id}`); + +export const createTenant = (data: CreateTenantDto) => + http.post('/admin/tenants', data); + +export const updateTenant = (id: number, data: UpdateTenantDto) => + http.put(`/admin/tenants/${id}`, data); + +export const updateTenantQuota = (id: number, data: UpdateTenantQuotaDto) => + http.put(`/admin/tenants/${id}/quota`, data); + +export const updateTenantStatus = (id: number, status: string) => + http.put<{ id: number; name: string; status: string }>(`/admin/tenants/${id}/status`, { status }); + +export const resetTenantPassword = (id: number) => + http.post<{ tempPassword: string }>(`/admin/tenants/${id}/reset-password`); + +export const deleteTenant = (id: number) => + http.delete<{ success: boolean }>(`/admin/tenants/${id}`); + +// ==================== 统计数据 ==================== + +export const getAdminStats = () => + http.get('/admin/stats'); + +export const getTrendData = () => + http.get('/admin/stats/trend'); + +export const getActiveTenants = (limit?: number) => + http.get('/admin/stats/tenants/active', { params: { limit } }); + +export const getPopularCourses = (limit?: number) => + http.get('/admin/stats/courses/popular', { params: { limit } }); + +// ==================== 系统设置 ==================== + +export const getAdminSettings = () => + http.get('/admin/settings'); + +export const updateAdminSettings = (data: Record) => + http.put('/admin/settings', data); diff --git a/reading-platform-frontend/src/api/auth.ts b/reading-platform-frontend/src/api/auth.ts new file mode 100644 index 0000000..9f2c983 --- /dev/null +++ b/reading-platform-frontend/src/api/auth.ts @@ -0,0 +1,51 @@ +import { http } from './index'; + +export interface LoginParams { + account: string; + password: string; + role: string; +} + +export interface LoginResponse { + token: string; + user: { + id: number; + name: string; + role: 'admin' | 'school' | 'teacher'; + tenantId?: number; + tenantName?: string; + email?: string; + phone?: string; + }; +} + +export interface UserProfile { + id: number; + name: string; + role: 'admin' | 'school' | 'teacher'; + tenantId?: number; + tenantName?: string; + email?: string; + phone?: string; + avatar?: string; +} + +// 登录 +export function login(params: LoginParams): Promise { + return http.post('/auth/login', params); +} + +// 登出 +export function logout(): Promise { + return http.post('/auth/logout'); +} + +// 刷新Token +export function refreshToken(): Promise<{ token: string }> { + return http.post('/auth/refresh'); +} + +// 获取当前用户信息 +export function getProfile(): Promise { + return http.get('/auth/profile'); +} diff --git a/reading-platform-frontend/src/api/course.ts b/reading-platform-frontend/src/api/course.ts new file mode 100644 index 0000000..6c156d2 --- /dev/null +++ b/reading-platform-frontend/src/api/course.ts @@ -0,0 +1,211 @@ +import { http } from './index'; + +export interface CourseQueryParams { + page?: number; + pageSize?: number; + grade?: string; + status?: string; + keyword?: string; +} + +export interface Course { + id: number; + name: string; + description?: string; + pictureBookName?: string; + grades: string[]; + status: string; + version: string; + usageCount: number; + teacherCount: number; + avgRating: number; + createdAt: Date; + updatedAt: Date; + submittedAt?: Date; + reviewedAt?: Date; + reviewComment?: string; + // 新增字段 + themeId?: number; + theme?: { id: number; name: string }; + coreContent?: string; + coverImagePath?: string; + domainTags?: string[]; + gradeTags?: string[]; + duration?: number; + // 课程介绍字段 + introSummary?: string; + introHighlights?: string; + introGoals?: string; + introSchedule?: string; + introKeyPoints?: string; + introMethods?: string; + introEvaluation?: string; + introNotes?: string; + // 排课计划参考 + scheduleRefData?: string; + // 环创建设 + environmentConstruction?: string; + // 关联课程 + courseLessons?: CourseLesson[]; +} + +export interface CourseLesson { + id: number; + courseId: number; + lessonType: string; + name: string; + description?: string; + duration: number; + videoPath?: string; + videoName?: string; + pptPath?: string; + pptName?: string; + pdfPath?: string; + pdfName?: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + assessmentData?: string; + useTemplate: boolean; + sortOrder: number; + steps?: LessonStep[]; +} + +export interface LessonStep { + id: number; + lessonId: number; + name: string; + content?: string; + duration: number; + objective?: string; + resourceIds?: string; + sortOrder: number; +} + +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; + warnings: ValidationWarning[]; +} + +export interface ValidationError { + field: string; + message: string; + code: string; +} + +export interface ValidationWarning { + field: string; + message: string; + code: string; +} + +// 获取课程包列表 +export function getCourses(params: CourseQueryParams): Promise<{ + items: Course[]; + total: number; + page: number; + pageSize: number; +}> { + return http.get('/courses', { params }); +} + +// 获取审核列表 +export function getReviewList(params: CourseQueryParams): Promise<{ + items: Course[]; + total: number; + page: number; + pageSize: number; +}> { + return http.get('/courses/review', { params }); +} + +// 获取课程包详情 +export function getCourse(id: number): Promise { + return http.get(`/courses/${id}`); +} + +// 创建课程包 +export function createCourse(data: any): Promise { + return http.post('/courses', data); +} + +// 更新课程包 +export function updateCourse(id: number, data: any): Promise { + return http.put(`/courses/${id}`, data); +} + +// 删除课程包 +export function deleteCourse(id: number): Promise { + return http.delete(`/courses/${id}`); +} + +// 验证课程完整性 +export function validateCourse(id: number): Promise { + return http.get(`/courses/${id}/validate`); +} + +// 提交审核 +export function submitCourse(id: number, copyrightConfirmed: boolean): Promise { + return http.post(`/courses/${id}/submit`, { copyrightConfirmed }); +} + +// 撤销审核 +export function withdrawCourse(id: number): Promise { + return http.post(`/courses/${id}/withdraw`); +} + +// 审核通过 +export function approveCourse(id: number, data: { checklist?: any; comment?: string }): Promise { + return http.post(`/courses/${id}/approve`, data); +} + +// 审核驳回 +export function rejectCourse(id: number, data: { checklist?: any; comment: string }): Promise { + return http.post(`/courses/${id}/reject`, data); +} + +// 直接发布(超级管理员) +export function directPublishCourse(id: number, skipValidation?: boolean): Promise { + return http.post(`/courses/${id}/direct-publish`, { skipValidation }); +} + +// 发布课程包(兼容旧API) +export function publishCourse(id: number): Promise { + return http.post(`/courses/${id}/publish`); +} + +// 下架课程包 +export function unpublishCourse(id: number): Promise { + return http.post(`/courses/${id}/unpublish`); +} + +// 重新发布 +export function republishCourse(id: number): Promise { + return http.post(`/courses/${id}/republish`); +} + +// 获取课程包统计数据 +export function getCourseStats(id: number): Promise { + return http.get(`/courses/${id}/stats`); +} + +// 获取版本历史 +export function getCourseVersions(id: number): Promise { + return http.get(`/courses/${id}/versions`); +} + +// 课程状态映射 +export const COURSE_STATUS_MAP: Record = { + DRAFT: { label: '草稿', color: 'default' }, + PENDING: { label: '审核中', color: 'processing' }, + REJECTED: { label: '已驳回', color: 'error' }, + PUBLISHED: { label: '已发布', color: 'success' }, + ARCHIVED: { label: '已下架', color: 'warning' }, +}; + +// 获取状态显示信息 +export function getCourseStatusInfo(status: string) { + return COURSE_STATUS_MAP[status] || { label: status, color: 'default' }; +} diff --git a/reading-platform-frontend/src/api/file.ts b/reading-platform-frontend/src/api/file.ts new file mode 100644 index 0000000..4cada95 --- /dev/null +++ b/reading-platform-frontend/src/api/file.ts @@ -0,0 +1,134 @@ +import axios from 'axios'; + +const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1'; + +export interface UploadResult { + success: boolean; + filePath: string; + fileName: string; + originalName: string; + fileSize: number; + mimeType: string; +} + +export interface DeleteResult { + success: boolean; + message: string; +} + +/** + * 文件上传 API + */ +export const fileApi = { + /** + * 上传文件 + */ + uploadFile: async ( + file: File, + type: 'cover' | 'ebook' | 'audio' | 'video' | 'ppt' | 'poster' | 'document' | 'other', + courseId?: number, + ): Promise => { + const formData = new FormData(); + formData.append('file', file); + formData.append('type', type); + if (courseId) { + formData.append('courseId', courseId.toString()); + } + + const response = await axios.post( + `${API_BASE}/files/upload`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + // 添加上传进度回调 + onUploadProgress: (progressEvent) => { + if (progressEvent.total) { + const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); + console.log(`Upload progress: ${percentCompleted}%`); + } + }, + }, + ); + + return response.data; + }, + + /** + * 删除文件 + */ + deleteFile: async (filePath: string): Promise => { + const response = await axios.delete(`${API_BASE}/files/delete`, { + data: { filePath }, + }); + return response.data; + }, + + /** + * 获取文件URL + */ + getFileUrl: (filePath: string): string => { + // filePath 格式: /uploads/courses/covers/xxx.png + // 直接返回相对路径,由 nginx 或后端静态服务处理 + return filePath; + }, +}; + +/** + * 文件类型常量 + */ +export const FILE_TYPES = { + COVER: 'cover', + EBOOK: 'ebook', + AUDIO: 'audio', + VIDEO: 'video', + PPT: 'ppt', + POSTER: 'poster', + DOCUMENT: 'document', + OTHER: 'other', +} as const; + +/** + * 文件大小限制(字节) + */ +export const FILE_SIZE_LIMITS = { + COVER: 10 * 1024 * 1024, // 10MB + EBOOK: 300 * 1024 * 1024, // 300MB + AUDIO: 300 * 1024 * 1024, // 300MB + VIDEO: 300 * 1024 * 1024, // 300MB + PPT: 300 * 1024 * 1024, // 300MB + POSTER: 10 * 1024 * 1024, // 10MB + DOCUMENT: 300 * 1024 * 1024, // 300MB + OTHER: 300 * 1024 * 1024, // 300MB +} as const; + +/** + * 每个分类最大文件数量 + */ +export const MAX_FILE_COUNT = 15; + +/** + * 文件类型验证 + */ +export const validateFileType = ( + file: File, + type: keyof typeof FILE_SIZE_LIMITS, +): { valid: boolean; error?: string } => { + const maxSize = FILE_SIZE_LIMITS[type]; + + if (file.size > maxSize) { + const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(0); + return { + valid: false, + error: `文件大小超过限制,最大允许 ${maxSizeMB}MB`, + }; + } + + return { valid: true }; +}; + +// 导出便捷函数 +export const uploadFile = fileApi.uploadFile; +export const deleteFile = fileApi.deleteFile; +export const getFileUrl = fileApi.getFileUrl; diff --git a/reading-platform-frontend/src/api/growth.ts b/reading-platform-frontend/src/api/growth.ts new file mode 100644 index 0000000..c506b18 --- /dev/null +++ b/reading-platform-frontend/src/api/growth.ts @@ -0,0 +1,130 @@ +import { http } from './index'; + +// ==================== 类型定义 ==================== + +export type RecordType = 'STUDENT' | 'CLASS'; + +export interface GrowthRecord { + id: number; + tenantId: number; + studentId: number; + classId?: number; + recordType: RecordType; + title: string; + content?: string; + images: string[]; + recordDate: string; + createdBy: number; + createdAt: string; + updatedAt: string; + student?: { + id: number; + name: string; + gender?: string; + }; + class?: { + id: number; + name: string; + grade?: string; + }; +} + +export interface CreateGrowthRecordDto { + studentId: number; + classId?: number; + recordType: RecordType; + title: string; + content?: string; + images?: string[]; + recordDate: string; +} + +export interface UpdateGrowthRecordDto { + title?: string; + content?: string; + images?: string[]; + recordDate?: string; +} + +// ==================== 学校端 API ==================== + +export const getGrowthRecords = (params?: { + page?: number; + pageSize?: number; + studentId?: number; + classId?: number; + recordType?: RecordType; + keyword?: string; +}) => + http.get<{ items: GrowthRecord[]; total: number; page: number; pageSize: number }>( + '/school/growth-records', + { params } + ); + +export const getGrowthRecord = (id: number) => + http.get(`/school/growth-records/${id}`); + +export const createGrowthRecord = (data: CreateGrowthRecordDto) => + http.post('/school/growth-records', data); + +export const updateGrowthRecord = (id: number, data: UpdateGrowthRecordDto) => + http.put(`/school/growth-records/${id}`, data); + +export const deleteGrowthRecord = (id: number) => + http.delete(`/school/growth-records/${id}`); + +export const getStudentGrowthRecords = (studentId: number, params?: { + page?: number; + pageSize?: number; +}) => + http.get<{ items: GrowthRecord[]; total: number; page: number; pageSize: number }>( + `/school/students/${studentId}/growth-records`, + { params } + ); + +export const getClassGrowthRecords = (classId: number, params?: { + page?: number; + pageSize?: number; + recordDate?: string; +}) => + http.get<{ items: GrowthRecord[]; total: number; page: number; pageSize: number }>( + `/school/classes/${classId}/growth-records`, + { params } + ); + +// ==================== 教师端 API ==================== + +export const getTeacherGrowthRecords = (params?: { + page?: number; + pageSize?: number; + studentId?: number; + classId?: number; + recordType?: RecordType; + keyword?: string; +}) => + http.get<{ items: GrowthRecord[]; total: number; page: number; pageSize: number }>( + '/teacher/growth-records', + { params } + ); + +export const getTeacherGrowthRecord = (id: number) => + http.get(`/teacher/growth-records/${id}`); + +export const createTeacherGrowthRecord = (data: CreateGrowthRecordDto) => + http.post('/teacher/growth-records', data); + +export const updateTeacherGrowthRecord = (id: number, data: UpdateGrowthRecordDto) => + http.put(`/teacher/growth-records/${id}`, data); + +export const deleteTeacherGrowthRecord = (id: number) => + http.delete(`/teacher/growth-records/${id}`); + +export const getTeacherClassGrowthRecords = (classId: number, params?: { + page?: number; + pageSize?: number; + recordDate?: string; +}) => + http.get<{ items: GrowthRecord[]; total: number; page: number; pageSize: number }>( + `/teacher/classes/${classId}/growth-records`, + { params } + ); diff --git a/reading-platform-frontend/src/api/index.ts b/reading-platform-frontend/src/api/index.ts new file mode 100644 index 0000000..badbbc9 --- /dev/null +++ b/reading-platform-frontend/src/api/index.ts @@ -0,0 +1,94 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import { message } from 'ant-design-vue'; + +// 创建axios实例 +const request: AxiosInstance = axios.create({ + baseURL: '/api/v1', // 使用 /api/v1,代理会保留完整路径 + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// 请求拦截器 +request.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// 响应拦截器 +request.interceptors.response.use( + (response: AxiosResponse) => { + // 直接返回响应数据 + return response.data; + }, + (error) => { + const { response } = error; + + if (response) { + const { status, data } = response; + + switch (status) { + case 401: + message.error('登录已过期,请重新登录'); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + localStorage.removeItem('role'); + localStorage.removeItem('name'); + window.location.href = '/login'; + break; + case 403: + message.error('没有权限访问'); + break; + case 404: + message.error('请求的资源不存在'); + break; + case 500: + message.error('服务器错误'); + break; + default: + message.error(data?.message || '请求失败'); + } + } else if (error.code === 'ECONNABORTED') { + message.error('请求超时'); + } else { + message.error('网络错误'); + } + + return Promise.reject(error); + } +); + +// 导出请求方法 +export default request; + +// 通用请求方法 +export const http = { + get(url: string, config?: AxiosRequestConfig): Promise { + return request.get(url, config); + }, + + post(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return request.post(url, data, config); + }, + + put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return request.put(url, data, config); + }, + + delete(url: string, config?: AxiosRequestConfig): Promise { + return request.delete(url, config); + }, + + patch(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return request.patch(url, data, config); + }, +}; diff --git a/reading-platform-frontend/src/api/lesson.ts b/reading-platform-frontend/src/api/lesson.ts new file mode 100644 index 0000000..ba32027 --- /dev/null +++ b/reading-platform-frontend/src/api/lesson.ts @@ -0,0 +1,159 @@ +import { http } from './index'; + +// ==================== 课程类型 ==================== + +export interface CourseLesson { + id: number; + courseId: number; + lessonType: string; + name: string; + description?: string; + duration: number; + videoPath?: string; + videoName?: string; + pptPath?: string; + pptName?: string; + pdfPath?: string; + pdfName?: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + assessmentData?: string; + useTemplate: boolean; + sortOrder: number; + steps?: LessonStep[]; +} + +export interface LessonStep { + id: number; + lessonId: number; + name: string; + content?: string; + duration: number; + objective?: string; + resourceIds?: string; + sortOrder: number; + stepResources?: StepResource[]; +} + +export interface StepResource { + id: number; + stepId: number; + resourceId: number; + sortOrder: number; + resource: CourseResource; +} + +export interface CourseResource { + id: number; + courseId: number; + resourceType: string; + resourceName: string; + fileUrl: string; + fileSize?: number; + mimeType?: string; +} + +export interface CreateLessonData { + lessonType: string; + name: string; + description?: string; + duration?: number; + videoPath?: string; + videoName?: string; + pptPath?: string; + pptName?: string; + pdfPath?: string; + pdfName?: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + assessmentData?: string; + useTemplate?: boolean; +} + +export interface CreateStepData { + name: string; + content?: string; + duration?: number; + objective?: string; + resourceIds?: number[]; +} + +// ==================== 超管端 API ==================== + +// 获取课程列表 +export function getLessonList(courseId: number) { + return http.get(`/admin/courses/${courseId}/lessons`); +} + +// 获取课程详情 +export function getLessonDetail(courseId: number, lessonId: number) { + return http.get(`/admin/courses/${courseId}/lessons/${lessonId}`); +} + +// 按类型获取课程 +export function getLessonByType(courseId: number, lessonType: string) { + return http.get(`/admin/courses/${courseId}/lessons/type/${lessonType}`); +} + +// 创建课程 +export function createLesson(courseId: number, data: CreateLessonData) { + return http.post(`/admin/courses/${courseId}/lessons`, data); +} + +// 更新课程 +export function updateLesson(lessonId: number, data: Partial) { + return http.put(`/admin/courses/0/lessons/${lessonId}`, data); +} + +// 删除课程 +export function deleteLesson(courseId: number, lessonId: number) { + return http.delete(`/admin/courses/${courseId}/lessons/${lessonId}`); +} + +// 重新排序课程 +export function reorderLessons(courseId: number, lessonIds: number[]) { + return http.put(`/admin/courses/${courseId}/lessons/reorder`, { lessonIds }); +} + +// ==================== 教学环节 API ==================== + +// 获取环节列表 +export function getStepList(courseId: number, lessonId: number) { + return http.get(`/admin/courses/${courseId}/lessons/${lessonId}/steps`); +} + +// 创建环节 +export function createStep(courseId: number, lessonId: number, data: CreateStepData) { + return http.post(`/admin/courses/${courseId}/lessons/${lessonId}/steps`, data); +} + +// 更新环节 +export function updateStep(stepId: number, data: Partial) { + return http.put(`/admin/courses/0/lessons/steps/${stepId}`, data); +} + +// 删除环节 +export function deleteStep(courseId: number, lessonId: number, stepId: number) { + return http.delete(`/admin/courses/${courseId}/lessons/steps/${stepId}`); +} + +// 重新排序环节 +export function reorderSteps(courseId: number, lessonId: number, stepIds: number[]) { + return http.put(`/admin/courses/${courseId}/lessons/${lessonId}/steps/reorder`, { stepIds }); +} + +// ==================== 教师端 API ==================== + +// 获取教师端课程列表 +export function getTeacherLessonList(courseId: number) { + return http.get(`/teacher/courses/${courseId}/lessons`); +} + +// 获取教师端课程详情 +export function getTeacherLessonDetail(courseId: number, lessonId: number) { + return http.get(`/teacher/courses/${courseId}/lessons/${lessonId}`); +} diff --git a/reading-platform-frontend/src/api/package.ts b/reading-platform-frontend/src/api/package.ts new file mode 100644 index 0000000..1a13047 --- /dev/null +++ b/reading-platform-frontend/src/api/package.ts @@ -0,0 +1,137 @@ +import { http } from './index'; + +// ==================== 套餐管理 ==================== + +export interface CoursePackage { + id: number; + name: string; + description?: string; + price: number; + discountPrice?: number; + discountType?: string; + gradeLevels: string[]; + status: string; + courseCount: number; + tenantCount: number; + createdAt: string; + publishedAt?: string; + courses?: PackageCourse[]; +} + +export interface PackageCourse { + packageId: number; + courseId: number; + gradeLevel: string; + sortOrder: number; + course: { + id: number; + name: string; + coverImagePath?: string; + duration?: number; + gradeTags?: string; + }; +} + +export interface PackageListParams { + status?: string; + page?: number; + pageSize?: number; +} + +export interface CreatePackageData { + name: string; + description?: string; + price: number; + discountPrice?: number; + discountType?: string; + gradeLevels: string[]; +} + +// 获取套餐列表 +export function getPackageList(params?: PackageListParams) { + return http.get('/admin/packages', { params }); +} + +// 获取套餐详情 +export function getPackageDetail(id: number) { + return http.get(`/admin/packages/${id}`); +} + +// 创建套餐 +export function createPackage(data: CreatePackageData) { + return http.post('/admin/packages', data); +} + +// 更新套餐 +export function updatePackage(id: number, data: Partial) { + return http.put(`/admin/packages/${id}`, data); +} + +// 删除套餐 +export function deletePackage(id: number) { + return http.delete(`/admin/packages/${id}`); +} + +// 设置套餐课程 +export function setPackageCourses( + packageId: number, + courses: { courseId: number; gradeLevel: string; sortOrder?: number }[], +) { + return http.put(`/admin/packages/${packageId}/courses`, { courses }); +} + +// 添加课程到套餐 +export function addCourseToPackage( + packageId: number, + data: { courseId: number; gradeLevel: string; sortOrder?: number }, +) { + return http.post(`/admin/packages/${packageId}/courses`, data); +} + +// 从套餐移除课程 +export function removeCourseFromPackage(packageId: number, courseId: number) { + return http.delete(`/admin/packages/${packageId}/courses/${courseId}`); +} + +// 提交审核 +export function submitPackage(id: number) { + return http.post(`/admin/packages/${id}/submit`); +} + +// 审核套餐 +export function reviewPackage(id: number, data: { approved: boolean; comment?: string }) { + return http.post(`/admin/packages/${id}/review`, data); +} + +// 发布套餐 +export function publishPackage(id: number) { + return http.post(`/admin/packages/${id}/publish`); +} + +// 下架套餐 +export function offlinePackage(id: number) { + return http.post(`/admin/packages/${id}/offline`); +} + +// ==================== 学校端套餐 ==================== + +export interface TenantPackage { + id: number; + tenantId: number; + packageId: number; + startDate: string; + endDate: string; + status: string; + pricePaid: number; + package: CoursePackage; +} + +// 获取学校已授权套餐 +export function getTenantPackages() { + return http.get('/school/packages'); +} + +// 续订套餐 +export function renewPackage(packageId: number, data: { endDate: string; pricePaid?: number }) { + return http.post(`/school/packages/${packageId}/renew`, data); +} diff --git a/reading-platform-frontend/src/api/parent.ts b/reading-platform-frontend/src/api/parent.ts new file mode 100644 index 0000000..c261006 --- /dev/null +++ b/reading-platform-frontend/src/api/parent.ts @@ -0,0 +1,152 @@ +import { http } from './index'; + +// ==================== 类型定义 ==================== + +export interface ChildInfo { + id: number; + name: string; + gender?: string; + birthDate?: string; + relationship: string; + class: { + id: number; + name: string; + grade: string; + }; + readingCount: number; + lessonCount: number; +} + +export interface ChildProfile extends ChildInfo { + stats: { + lessonRecords: number; + growthRecords: number; + taskCompletions: number; + }; +} + +export interface LessonRecord { + id: number; + lesson: { + id: number; + startDatetime: string; + endDatetime?: string; + actualDuration?: number; + course: { + id: number; + name: string; + pictureBookName?: string; + }; + }; + focus?: number; + participation?: number; + interest?: number; + understanding?: number; + notes?: string; + createdAt: string; +} + +export interface TaskWithCompletion { + id: number; + status: string; + completedAt?: string; + feedback?: string; + parentFeedback?: string; + task: { + id: number; + title: string; + description?: string; + taskType: string; + startDate: string; + endDate: string; + course?: { + id: number; + name: string; + }; + }; +} + +export interface GrowthRecord { + id: number; + title: string; + content?: string; + images: string[]; + recordDate: string; + recordType: string; + class?: { + id: number; + name: string; + }; + createdAt: string; +} + +export interface Notification { + id: number; + title: string; + content: string; + notificationType: string; + isRead: boolean; + readAt?: string; + createdAt: string; +} + +// ==================== 孩子信息 API ==================== + +export const getChildren = (): Promise => + http.get('/parent/children'); + +export const getChildProfile = (childId: number): Promise => + http.get(`/parent/children/${childId}`); + +// ==================== 阅读记录 API ==================== + +export const getChildLessons = ( + childId: number, + params?: { page?: number; pageSize?: number } +): Promise<{ items: LessonRecord[]; total: number; page: number; pageSize: number }> => + http.get(`/parent/children/${childId}/lessons`, { params }); + +// ==================== 任务 API ==================== + +export const getChildTasks = ( + childId: number, + params?: { page?: number; pageSize?: number; status?: string } +): Promise<{ items: TaskWithCompletion[]; total: number; page: number; pageSize: number }> => + http.get(`/parent/children/${childId}/tasks`, { params }); + +export const submitTaskFeedback = ( + childId: number, + taskId: number, + feedback: string +): Promise => + http.put(`/parent/children/${childId}/tasks/${taskId}/feedback`, { feedback }); + +// ==================== 成长档案 API ==================== + +export const getChildGrowthRecords = ( + childId: number, + params?: { page?: number; pageSize?: number } +): Promise<{ items: GrowthRecord[]; total: number; page: number; pageSize: number }> => + http.get(`/parent/children/${childId}/growth-records`, { params }); + +// ==================== 通知 API ==================== + +export const getNotifications = ( + params?: { page?: number; pageSize?: number; isRead?: boolean; notificationType?: string } +): Promise<{ + items: Notification[]; + total: number; + unreadCount: number; + page: number; + pageSize: number; +}> => + http.get('/parent/notifications', { params }); + +export const getUnreadCount = (): Promise => + http.get('/parent/notifications/unread-count'); + +export const markNotificationAsRead = (id: number): Promise => + http.put(`/parent/notifications/${id}/read`); + +export const markAllNotificationsAsRead = (): Promise => + http.put('/parent/notifications/read-all'); diff --git a/reading-platform-frontend/src/api/resource.ts b/reading-platform-frontend/src/api/resource.ts new file mode 100644 index 0000000..b1fd971 --- /dev/null +++ b/reading-platform-frontend/src/api/resource.ts @@ -0,0 +1,135 @@ +import { http } from './index'; + +// ==================== 类型定义 ==================== + +export type LibraryType = 'PICTURE_BOOK' | 'MATERIAL' | 'TEMPLATE'; +export type FileType = 'IMAGE' | 'PDF' | 'VIDEO' | 'AUDIO' | 'PPT' | 'OTHER'; + +export interface ResourceLibrary { + id: number; + name: string; + libraryType: LibraryType; + description?: string; + coverImage?: string; + createdBy: number; + status: string; + sortOrder: number; + itemCount: number; + createdAt: string; + updatedAt: string; +} + +export interface ResourceItem { + id: number; + libraryId: number; + title: string; + description?: string; + fileType: FileType; + filePath: string; + fileSize?: number; + tags: string[]; + sortOrder: number; + createdAt: string; + library?: { + id: number; + name: string; + libraryType: LibraryType; + }; +} + +export interface CreateLibraryDto { + name: string; + libraryType: LibraryType; + description?: string; + coverImage?: string; +} + +export interface UpdateLibraryDto { + name?: string; + description?: string; + coverImage?: string; + sortOrder?: number; +} + +export interface CreateResourceItemDto { + libraryId: number; + title: string; + description?: string; + fileType: FileType; + filePath: string; + fileSize?: number; + tags?: string[]; +} + +export interface UpdateResourceItemDto { + title?: string; + description?: string; + tags?: string[]; + sortOrder?: number; +} + +export interface ResourceStats { + totalLibraries: number; + totalItems: number; + itemsByType: Record; + itemsByLibraryType: Record; +} + +// ==================== 资源库管理 ==================== + +export const getLibraries = (params?: { + page?: number; + pageSize?: number; + libraryType?: LibraryType; + keyword?: string; +}) => + http.get<{ items: ResourceLibrary[]; total: number; page: number; pageSize: number }>( + '/admin/resources/libraries', + { params } + ); + +export const getLibrary = (id: number) => + http.get(`/admin/resources/libraries/${id}`); + +export const createLibrary = (data: CreateLibraryDto) => + http.post('/admin/resources/libraries', data); + +export const updateLibrary = (id: number, data: UpdateLibraryDto) => + http.put(`/admin/resources/libraries/${id}`, data); + +export const deleteLibrary = (id: number) => + http.delete(`/admin/resources/libraries/${id}`); + +// ==================== 资源项目管理 ==================== + +export const getResourceItems = (params?: { + page?: number; + pageSize?: number; + libraryId?: number; + fileType?: FileType; + keyword?: string; +}) => + http.get<{ items: ResourceItem[]; total: number; page: number; pageSize: number }>( + '/admin/resources/items', + { params } + ); + +export const getResourceItem = (id: number) => + http.get(`/admin/resources/items/${id}`); + +export const createResourceItem = (data: CreateResourceItemDto) => + http.post('/admin/resources/items', data); + +export const updateResourceItem = (id: number, data: UpdateResourceItemDto) => + http.put(`/admin/resources/items/${id}`, data); + +export const deleteResourceItem = (id: number) => + http.delete(`/admin/resources/items/${id}`); + +export const batchDeleteResourceItems = (ids: number[]) => + http.post<{ message: string }>('/admin/resources/items/batch-delete', { ids }); + +// ==================== 统计数据 ==================== + +export const getResourceStats = () => + http.get('/admin/resources/stats'); diff --git a/reading-platform-frontend/src/api/school-course.ts b/reading-platform-frontend/src/api/school-course.ts new file mode 100644 index 0000000..3b6e4d1 --- /dev/null +++ b/reading-platform-frontend/src/api/school-course.ts @@ -0,0 +1,178 @@ +import { http } from './index'; + +// ==================== 类型定义 ==================== + +export interface SchoolCourse { + id: number; + tenantId: number; + sourceCourseId: number; + name: string; + description?: string; + createdBy: number; + changesSummary?: string; + usageCount: number; + status: string; + createdAt: string; + updatedAt?: string; + sourceCourse?: { + id: number; + name: string; + coverImagePath?: string; + description?: string; + }; + lessons?: SchoolCourseLesson[]; +} + +export interface SchoolCourseLesson { + id: number; + schoolCourseId: number; + sourceLessonId: number; + lessonType: string; + objectives?: string; + preparation?: string; + extension?: string; + reflection?: string; + changeNote?: string; + stepsData?: string; +} + +export interface SchoolCourseReservation { + id: number; + schoolCourseId: number; + teacherId: number; + classId: number; + scheduledDate: string; + scheduledTime?: string; + status: string; + note?: string; + createdAt: string; +} + +export interface CreateSchoolCourseData { + sourceCourseId: number; + name: string; + description?: string; + changesSummary?: string; +} + +export interface UpdateSchoolCourseData { + name?: string; + description?: string; + changesSummary?: string; + status?: string; +} + +export interface CreateReservationData { + teacherId: number; + classId: number; + scheduledDate: string; + scheduledTime?: string; + note?: string; +} + +// ==================== 学校端 API ==================== + +// 获取校本课程包列表 +export function getSchoolCourseList() { + return http.get('/school/school-courses'); +} + +// 获取可创建校本课程包的源课程列表 +export function getSourceCourses() { + return http.get('/school/school-courses/source-courses'); +} + +// 获取校本课程包详情 +export function getSchoolCourseDetail(id: number) { + return http.get(`/school/school-courses/${id}`); +} + +// 创建校本课程包 +export function createSchoolCourse(data: CreateSchoolCourseData) { + return http.post('/school/school-courses', data); +} + +// 更新校本课程包 +export function updateSchoolCourse(id: number, data: UpdateSchoolCourseData) { + return http.put(`/school/school-courses/${id}`, data); +} + +// 删除校本课程包 +export function deleteSchoolCourse(id: number) { + return http.delete(`/school/school-courses/${id}`); +} + +// 获取校本课程列表 +export function getSchoolCourseLessons(schoolCourseId: number) { + return http.get(`/school/school-courses/${schoolCourseId}/lessons`); +} + +// 更新校本课程 +export function updateSchoolCourseLesson( + schoolCourseId: number, + lessonId: number, + data: Partial, +) { + return http.put(`/school/school-courses/${schoolCourseId}/lessons/${lessonId}`, data); +} + +// 获取预约列表 +export function getReservations(schoolCourseId: number) { + return http.get(`/school/school-courses/${schoolCourseId}/reservations`); +} + +// 创建预约 +export function createReservation(schoolCourseId: number, data: CreateReservationData) { + return http.post(`/school/school-courses/${schoolCourseId}/reservations`, data); +} + +// 取消预约 +export function cancelReservation(reservationId: number) { + return http.post(`/school/school-courses/reservations/${reservationId}/cancel`); +} + +// ==================== 教师端 API ==================== + +// 获取教师端校本课程包列表 +export function getTeacherSchoolCourseList() { + return http.get('/teacher/school-courses'); +} + +// 获取教师端可创建校本课程包的源课程列表 +export function getTeacherSourceCourses() { + return http.get('/teacher/school-courses/source-courses'); +} + +// 获取教师端校本课程包详情 +export function getTeacherSchoolCourseDetail(id: number) { + return http.get(`/teacher/school-courses/${id}`); +} + +// 创建教师端校本课程包 +export function createTeacherSchoolCourse(data: CreateSchoolCourseData) { + return http.post('/teacher/school-courses', data); +} + +// 更新教师端校本课程包 +export function updateTeacherSchoolCourse(id: number, data: UpdateSchoolCourseData) { + return http.put(`/teacher/school-courses/${id}`, data); +} + +// 删除教师端校本课程包 +export function deleteTeacherSchoolCourse(id: number) { + return http.delete(`/teacher/school-courses/${id}`); +} + +// 获取教师端校本课程列表 +export function getTeacherSchoolCourseLessons(schoolCourseId: number) { + return http.get(`/teacher/school-courses/${schoolCourseId}/lessons`); +} + +// 更新教师端校本课程 +export function updateTeacherSchoolCourseLesson( + schoolCourseId: number, + lessonId: number, + data: Partial, +) { + return http.put(`/teacher/school-courses/${schoolCourseId}/lessons/${lessonId}`, data); +} diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts new file mode 100644 index 0000000..a513d38 --- /dev/null +++ b/reading-platform-frontend/src/api/school.ts @@ -0,0 +1,1003 @@ +import { http } from './index'; + +// ==================== 类型定义 ==================== + +export interface TeacherQueryParams { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; +} + +export interface Teacher { + id: number; + name: string; + phone: string; + email?: string; + loginAccount: string; + status: string; + classIds: number[]; + classNames?: string | string[]; + lessonCount?: number; + createdAt: string; +} + +export interface CreateTeacherDto { + name: string; + phone: string; + email?: string; + loginAccount: string; + password?: string; + classIds?: number[]; +} + +export interface StudentQueryParams { + page?: number; + pageSize?: number; + classId?: number; + keyword?: string; +} + +export interface Student { + id: number; + name: string; + gender?: string; + birthDate?: string; + classId: number; + className?: string; + parentName?: string; + parentPhone?: string; + lessonCount?: number; + readingCount?: number; + avgScore?: number; + createdAt: string; +} + +export interface CreateStudentDto { + name: string; + gender?: string; + birthDate?: string; + classId: number; + parentName?: string; + parentPhone?: string; +} + +export interface ClassInfo { + id: number; + name: string; + grade: string; + teacherId?: number; + teacherName?: string; + studentCount: number; + lessonCount: number; + createdAt?: string; + teachers?: ClassTeacher[]; // 新增:教师团队 +} + +export interface ClassTeacher { + id: number; + teacherId: number; + teacherName: string; + teacherPhone?: string; + teacherEmail?: string; + role: 'MAIN' | 'ASSIST' | 'CARE'; + isPrimary: boolean; + createdAt?: string; +} + +export interface AddClassTeacherDto { + teacherId: number; + role: 'MAIN' | 'ASSIST' | 'CARE'; + isPrimary?: boolean; +} + +export interface UpdateClassTeacherDto { + role?: 'MAIN' | 'ASSIST' | 'CARE'; + isPrimary?: boolean; +} + +export interface TransferStudentDto { + toClassId: number; + reason?: string; +} + +export interface StudentClassHistory { + id: number; + fromClass: { id: number; name: string; grade: string } | null; + toClass: { id: number; name: string; grade: string }; + reason?: string; + operatedBy?: number; + createdAt: string; +} + +export interface CreateClassDto { + name: string; + grade: string; + teacherId?: number; +} + +export interface SchoolStats { + teacherCount: number; + studentCount: number; + classCount: number; + lessonCount: number; +} + +export interface PackageInfo { + packageType: string; + teacherQuota: number; + studentQuota: number; + storageQuota: number; + teacherCount: number; + studentCount: number; + storageUsed: number; + startDate: string; + expireDate: string; + status: string; +} + +export interface PackageUsage { + teacher: { + used: number; + quota: number; + percentage: number; + }; + student: { + used: number; + quota: number; + percentage: number; + }; + storage: { + used: number; + quota: number; + percentage: number; + }; +} + +// ==================== 教师管理 ==================== + +export const getTeachers = (params: TeacherQueryParams) => + http.get<{ items: Teacher[]; total: number; page: number; pageSize: number }>('/school/teachers', { params }); + +export const getTeacher = (id: number) => + http.get(`/school/teachers/${id}`); + +export const createTeacher = (data: CreateTeacherDto) => + http.post('/school/teachers', data); + +export const updateTeacher = (id: number, data: Partial) => + http.put(`/school/teachers/${id}`, data); + +export const deleteTeacher = (id: number) => + http.delete(`/school/teachers/${id}`); + +export const resetTeacherPassword = (id: number) => + http.post<{ tempPassword: string }>(`/school/teachers/${id}/reset-password`); + +// ==================== 学生管理 ==================== + +export const getStudents = (params: StudentQueryParams) => + http.get<{ items: Student[]; total: number; page: number; pageSize: number }>('/school/students', { params }); + +export const getStudent = (id: number) => + http.get(`/school/students/${id}`); + +export const createStudent = (data: CreateStudentDto) => + http.post('/school/students', data); + +export const updateStudent = (id: number, data: Partial) => + http.put(`/school/students/${id}`, data); + +export const deleteStudent = (id: number) => + http.delete(`/school/students/${id}`); + +// ==================== 学生批量导入 ==================== + +export interface ImportResult { + success: number; + failed: number; + errors: Array<{ row: number; message: string }>; +} + +export interface ImportTemplate { + headers: string[]; + example: string[]; + notes: string[]; +} + +export const getStudentImportTemplate = () => + http.get('/school/students/import/template'); + +export const importStudents = (file: File, defaultClassId?: number): Promise => { + const formData = new FormData(); + formData.append('file', file); + + const params = defaultClassId ? { defaultClassId } : {}; + return http.post('/school/students/import', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + params, + }); +}; + +// ==================== 班级管理 ==================== + +export const getClasses = () => + http.get('/school/classes'); + +export const getClass = (id: number) => + http.get(`/school/classes/${id}`); + +export const createClass = (data: CreateClassDto) => + http.post('/school/classes', data); + +export const updateClass = (id: number, data: Partial) => + http.put(`/school/classes/${id}`, data); + +export const deleteClass = (id: number) => + http.delete(`/school/classes/${id}`); + +export const getClassStudents = (classId: number, params?: { page?: number; pageSize?: number; keyword?: string }) => + http.get<{ items: Student[]; total: number; page: number; pageSize: number; class?: ClassInfo }>(`/school/classes/${classId}/students`, { params }); + +// ==================== 统计数据 ==================== + +export const getSchoolStats = () => + http.get('/school/stats'); + +export const getActiveTeachers = (limit?: number) => + http.get>('/school/stats/teachers', { params: { limit } }); + +export const getCourseUsageStats = () => + http.get>('/school/stats/courses'); + +export const getRecentActivities = (limit?: number) => + http.get>('/school/stats/activities', { params: { limit } }); + +// ==================== 套餐信息(旧API,保留兼容) ==================== + +export const getPackageInfo = () => + http.get('/school/package'); + +export const getPackageUsage = () => + http.get('/school/package/usage'); + +// ==================== 套餐管理(新API) ==================== + +export interface TenantPackage { + id: number; + tenantId: number; + packageId: number; + startDate: string; + endDate: string; + status: 'ACTIVE' | 'EXPIRED' | 'CANCELLED'; + pricePaid?: number; + createdAt: string; + package: { + id: number; + name: string; + description?: string; + price: number; + discountPrice?: number; + courseCount: number; + gradeLevels: string; + status: string; + courses: Array<{ + id: number; + packageId: number; + courseId: number; + gradeLevel: string; + course: { + id: number; + name: string; + coverImagePath?: string; + }; + }>; + }; +} + +export interface RenewPackageDto { + endDate: string; + pricePaid?: number; +} + +export const getTenantPackages = () => + http.get('/school/packages'); + +export const renewPackage = (packageId: number, data: RenewPackageDto) => + http.post(`/school/packages/${packageId}/renew`, data); + +// ==================== 系统设置 ==================== + +export interface SystemSettings { + id: number; + tenantId: number; + schoolName?: string; + schoolLogo?: string; + address?: string; + notifyOnLesson: boolean; + notifyOnTask: boolean; + notifyOnGrowth: boolean; + createdAt: string; + updatedAt: string; +} + +export interface UpdateSettingsDto { + schoolName?: string; + schoolLogo?: string; + address?: string; + notifyOnLesson?: boolean; + notifyOnTask?: boolean; + notifyOnGrowth?: boolean; +} + +export const getSettings = () => + http.get('/school/settings'); + +export const updateSettings = (data: UpdateSettingsDto) => + http.put('/school/settings', data); + +// ==================== 课程管理 ==================== + +export const getSchoolCourses = () => + http.get('/school/courses'); + +export const getSchoolCourse = (id: number) => + http.get(`/school/courses/${id}`); + +// ==================== 班级教师管理 ==================== + +export const getClassTeachers = (classId: number) => + http.get(`/school/classes/${classId}/teachers`); + +export const addClassTeacher = (classId: number, data: AddClassTeacherDto) => + http.post(`/school/classes/${classId}/teachers`, data); + +export const updateClassTeacher = (classId: number, teacherId: number, data: UpdateClassTeacherDto) => + http.put(`/school/classes/${classId}/teachers/${teacherId}`, data); + +export const removeClassTeacher = (classId: number, teacherId: number) => + http.delete<{ message: string }>(`/school/classes/${classId}/teachers/${teacherId}`); + +// ==================== 学生调班 ==================== + +export const transferStudent = (studentId: number, data: TransferStudentDto) => + http.post<{ message: string }>(`/school/students/${studentId}/transfer`, data); + +export const getStudentClassHistory = (studentId: number) => + http.get(`/school/students/${studentId}/history`); + +// ==================== 排课管理 ==================== + +export interface SchedulePlan { + id: number; + tenantId: number; + classId: number; + className: string; + courseId: number; + courseName: string; + teacherId?: number; + teacherName?: string; + scheduledDate?: string; + scheduledTime?: string; + weekDay?: number; + repeatType: 'NONE' | 'DAILY' | 'WEEKLY'; + repeatEndDate?: string; + source: 'SCHOOL' | 'TEACHER'; + status: 'ACTIVE' | 'CANCELLED'; + note?: string; + createdBy: number; + createdAt: string; + updatedAt: string; +} + +export interface CreateScheduleDto { + classId: number; + courseId: number; + teacherId?: number; + scheduledDate?: string; + scheduledTime?: string; + weekDay?: number; + repeatType: 'NONE' | 'DAILY' | 'WEEKLY'; + repeatEndDate?: string; + note?: string; +} + +export interface UpdateScheduleDto { + teacherId?: number; + scheduledDate?: string; + scheduledTime?: string; + weekDay?: number; + repeatType?: 'NONE' | 'DAILY' | 'WEEKLY'; + repeatEndDate?: string; + note?: string; + status?: string; +} + +export interface ScheduleQueryParams { + classId?: number; + teacherId?: number; + courseId?: number; + startDate?: string; + endDate?: string; + status?: string; + source?: string; + page?: number; + pageSize?: number; +} + +export interface TimetableItem { + date: string; + weekDay: number; + schedules: SchedulePlan[]; +} + +export interface TimetableQueryParams { + startDate: string; + endDate: string; + classId?: number; + teacherId?: number; +} + +export const getSchedules = (params?: ScheduleQueryParams) => + http.get<{ items: SchedulePlan[]; total: number; page: number; pageSize: number }>('/school/schedules', { params }); + +export const getSchedule = (id: number) => + http.get(`/school/schedules/${id}`); + +export const createSchedule = (data: CreateScheduleDto) => + http.post('/school/schedules', data); + +export const updateSchedule = (id: number, data: UpdateScheduleDto) => + http.put(`/school/schedules/${id}`, data); + +export const cancelSchedule = (id: number) => + http.delete<{ message: string }>(`/school/schedules/${id}`); + +export const getTimetable = (params: TimetableQueryParams) => + http.get('/school/schedules/timetable', { params }); + +export interface BatchScheduleItem { + classId: number; + courseId: number; + teacherId?: number; + scheduledDate: string; + scheduledTime?: string; + note?: string; +} + +export interface BatchCreateResult { + success: number; + failed: number; + results: SchedulePlan[]; + errors: Array<{ index: number; message: string }>; +} + +export const batchCreateSchedules = (schedules: BatchScheduleItem[]) => + http.post('/school/schedules/batch', { schedules }); + +// ==================== 趋势与分布统计 ==================== + +export interface LessonTrendItem { + month: string; + lessonCount: number; + studentCount: number; +} + +export interface CourseDistributionItem { + name: string; + value: number; +} + +export const getLessonTrend = (months?: number) => + http.get('/school/stats/lesson-trend', { params: { months } }); + +export const getCourseDistribution = () => + http.get('/school/stats/course-distribution'); + +// ==================== 数据导出 ==================== + +export const exportLessons = (startDate?: string, endDate?: string) => { + const params = new URLSearchParams(); + if (startDate) params.append('startDate', startDate); + if (endDate) params.append('endDate', endDate); + + const token = localStorage.getItem('token'); + const url = `/api/v1/school/export/lessons?${params.toString()}`; + + return fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + }, + }).then((res) => { + if (!res.ok) throw new Error('导出失败'); + return res.blob(); + }).then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `授课记录_${startDate || ''}_${endDate || ''}.xlsx`; + a.click(); + window.URL.revokeObjectURL(url); + }); +}; + +export const exportTeacherStats = (startDate?: string, endDate?: string) => { + const params = new URLSearchParams(); + if (startDate) params.append('startDate', startDate); + if (endDate) params.append('endDate', endDate); + + const token = localStorage.getItem('token'); + const url = `/api/v1/school/export/teacher-stats?${params.toString()}`; + + return fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + }, + }).then((res) => { + if (!res.ok) throw new Error('导出失败'); + return res.blob(); + }).then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `教师绩效统计_${startDate || ''}_${endDate || ''}.xlsx`; + a.click(); + window.URL.revokeObjectURL(url); + }); +}; + +export const exportStudentStats = (classId?: number) => { + const params = new URLSearchParams(); + if (classId) params.append('classId', String(classId)); + + const token = localStorage.getItem('token'); + const url = `/api/v1/school/export/student-stats?${params.toString()}`; + + return fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + }, + }).then((res) => { + if (!res.ok) throw new Error('导出失败'); + return res.blob(); + }).then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `学生统计.xlsx`; + a.click(); + window.URL.revokeObjectURL(url); + }); +}; + +// ==================== 排课模板 ==================== + +export interface ScheduleTemplate { + id: number; + tenantId: number; + name: string; + courseId: number; + courseName?: string; + classId?: number; + className?: string; + teacherId?: number; + teacherName?: string; + scheduledTime?: string; + weekDay?: number; + duration: number; + isDefault: boolean; + createdAt: string; + updatedAt: string; +} + +export interface CreateScheduleTemplateDto { + name: string; + courseId: number; + classId?: number; + teacherId?: number; + scheduledTime?: string; + weekDay?: number; + duration?: number; + isDefault?: boolean; +} + +export interface UpdateScheduleTemplateDto { + name?: string; + classId?: number; + teacherId?: number; + scheduledTime?: string; + weekDay?: number; + duration?: number; + isDefault?: boolean; +} + +export interface ApplyTemplateDto { + scheduledDate: string; + classId?: number; + teacherId?: number; +} + +export const getScheduleTemplates = (params?: { classId?: number; courseId?: number }) => + http.get('/school/schedule-templates', { params }); + +export const getScheduleTemplate = (id: number) => + http.get(`/school/schedule-templates/${id}`); + +export const createScheduleTemplate = (data: CreateScheduleTemplateDto) => + http.post('/school/schedule-templates', data); + +export const updateScheduleTemplate = (id: number, data: UpdateScheduleTemplateDto) => + http.put(`/school/schedule-templates/${id}`, data); + +export const deleteScheduleTemplate = (id: number) => + http.delete<{ message: string }>(`/school/schedule-templates/${id}`); + +export const applyScheduleTemplate = (id: number, data: ApplyTemplateDto) => + http.post(`/school/schedule-templates/${id}/apply`, data); + +// ==================== 操作日志 ==================== + +export interface OperationLog { + id: number; + tenantId: number; + userId: number; + userType: string; + action: string; + module: string; + description: string; + targetId: number | null; + oldValue: string | null; + newValue: string | null; + ipAddress: string | null; + createdAt: string; +} + +export interface OperationLogStats { + total: number; + modules: { name: string; count: number }[]; + actions: { name: string; count: number }[]; +} + +export const getOperationLogs = (params?: { + page?: number; + pageSize?: number; + module?: string; + action?: string; + startDate?: string; + endDate?: string; +}) => http.get<{ items: OperationLog[]; total: number; page: number; pageSize: number }>( + '/school/operation-logs', + { params } +); + +export const getOperationLogStats = (startDate?: string, endDate?: string) => + http.get('/school/operation-logs/stats', { + params: { startDate, endDate }, + }); + +export const getOperationLogById = (id: number) => + http.get(`/school/operation-logs/${id}`); + +// ==================== 任务模板 API ==================== + +export interface TaskTemplate { + id: number; + tenantId: number; + name: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + relatedCourseId?: number; + defaultDuration: number; + isDefault: boolean; + status: string; + createdBy: number; + createdAt: string; + updatedAt: string; + course?: { + id: number; + name: string; + pictureBookName?: string; + }; +} + +export interface CreateTaskTemplateDto { + name: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + relatedCourseId?: number; + defaultDuration?: number; + isDefault?: boolean; +} + +export interface UpdateTaskTemplateDto { + name?: string; + description?: string; + relatedCourseId?: number; + defaultDuration?: number; + isDefault?: boolean; + status?: string; +} + +export const getTaskTemplates = (params?: { + page?: number; + pageSize?: number; + taskType?: string; + keyword?: string; +}) => http.get<{ items: TaskTemplate[]; total: number; page: number; pageSize: number }>('/school/task-templates', { params }); + +export const getTaskTemplate = (id: number) => + http.get(`/school/task-templates/${id}`); + +export const getDefaultTaskTemplate = (taskType: string) => + http.get(`/school/task-templates/default/${taskType}`); + +export const createTaskTemplate = (data: CreateTaskTemplateDto) => + http.post('/school/task-templates', data); + +export const updateTaskTemplate = (id: number, data: UpdateTaskTemplateDto) => + http.put(`/school/task-templates/${id}`, data); + +export const deleteTaskTemplate = (id: number) => + http.delete<{ message: string }>(`/school/task-templates/${id}`); + +// ==================== 任务统计 API ==================== + +export interface TaskStats { + totalTasks: number; + publishedTasks: number; + completedTasks: number; + inProgressTasks: number; + pendingCount: number; + totalCompletions: number; + completionRate: number; +} + +export interface TaskStatsByType { + [key: string]: { + total: number; + completed: number; + rate: number; + }; +} + +export interface TaskStatsByClass { + classId: number; + className: string; + grade: string; + total: number; + completed: number; + rate: number; +} + +export interface MonthlyTaskStats { + month: string; + tasks: number; + completions: number; + completed: number; + rate: number; +} + +export const getTaskStats = () => + http.get('/school/tasks/stats'); + +export const getTaskStatsByType = () => + http.get('/school/tasks/stats/by-type'); + +export const getTaskStatsByClass = () => + http.get('/school/tasks/stats/by-class'); + +export const getMonthlyTaskStats = (months?: number) => + http.get('/school/tasks/stats/monthly', { params: { months } }); + +// ==================== 任务管理 API ==================== + +export interface SchoolTask { + id: number; + tenantId: number; + title: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + targetType: 'CLASS' | 'STUDENT'; + relatedCourseId?: number; + course?: { + id: number; + name: string; + }; + startDate: string; + endDate: string; + status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED'; + createdBy: number; + targetCount?: number; + completionCount?: number; + createdAt: string; + updatedAt: string; +} + +export interface TaskCompletion { + id: number; + taskId: number; + studentId: number; + studentName: string; + className: string; + status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED'; + completedAt?: string; + parentFeedback?: string; + rating?: number; +} + +export interface CreateSchoolTaskDto { + title: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + targetType: 'CLASS' | 'STUDENT'; + targetIds: number[]; + relatedCourseId?: number; + startDate: string; + endDate: string; +} + +export interface UpdateSchoolTaskDto { + title?: string; + description?: string; + taskType?: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + relatedCourseId?: number; + startDate?: string; + endDate?: string; + status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED'; +} + +export const getSchoolTasks = (params?: { + page?: number; + pageSize?: number; + status?: string; + taskType?: string; + keyword?: string; +}) => http.get<{ items: SchoolTask[]; total: number; page: number; pageSize: number }>('/school/tasks', { params }); + +export const getSchoolTask = (id: number) => + http.get(`/school/tasks/${id}`); + +export const createSchoolTask = (data: CreateSchoolTaskDto) => + http.post('/school/tasks', data); + +export const updateSchoolTask = (id: number, data: UpdateSchoolTaskDto) => + http.put(`/school/tasks/${id}`, data); + +export const deleteSchoolTask = (id: number) => + http.delete<{ message: string }>(`/school/tasks/${id}`); + +export const getSchoolTaskCompletions = (taskId: number) => + http.get(`/school/tasks/${taskId}/completions`); + +export const getSchoolClasses = () => + http.get('/school/classes'); + +// ==================== 数据报告 API ==================== + +export interface ReportOverview { + totalLessons: number; + activeTeacherCount: number; + usedCourseCount: number; + avgRating: number; +} + +export interface TeacherReport { + id: number; + name: string; + lessonCount: number; + courseCount: number; + feedbackCount: number; + avgRating: number; +} + +export interface CourseReport { + id: number; + name: string; + lessonCount: number; + teacherCount: number; + studentCount: number; + avgRating: number; +} + +export interface StudentReport { + id: number; + name: string; + className: string; + lessonCount: number; + avgFocus: number; + avgParticipation: number; +} + +export const getReportOverview = () => + http.get('/school/reports/overview'); + +export const getTeacherReports = () => + http.get('/school/reports/teachers'); + +export const getCourseReports = () => + http.get('/school/reports/courses'); + +export const getStudentReports = () => + http.get('/school/reports/students'); + +// ==================== 家长管理 ==================== + +export interface ParentQueryParams { + page?: number; + pageSize?: number; + keyword?: string; + status?: string; +} + +export interface ParentChild { + id: number; + name: string; + relationship: string; + class?: { + id: number; + name: string; + }; +} + +export interface Parent { + id: number; + name: string; + phone: string; + email?: string; + loginAccount: string; + status: string; + tenantId: number; + childrenCount: number; + children?: ParentChild[]; + createdAt: string; +} + +export interface CreateParentDto { + name: string; + phone: string; + email?: string; + loginAccount: string; + password?: string; +} + +export interface UpdateParentDto { + name?: string; + phone?: string; + email?: string; +} + +export interface AddChildDto { + studentId: number; + relationship: string; +} + +export const getParents = (params?: ParentQueryParams) => + http.get<{ items: Parent[]; total: number; page: number; pageSize: number }>('/school/parents', { params }); + +export const getParent = (id: number) => + http.get(`/school/parents/${id}`); + +export const createParent = (data: CreateParentDto) => + http.post('/school/parents', data); + +export const updateParent = (id: number, data: UpdateParentDto) => + http.put(`/school/parents/${id}`, data); + +export const deleteParent = (id: number) => + http.delete<{ message: string }>(`/school/parents/${id}`); + +export const resetParentPassword = (id: number) => + http.post<{ tempPassword: string }>(`/school/parents/${id}/reset-password`); + +export const getParentChildren = async (parentId: number): Promise => { + const parent = await http.get(`/school/parents/${parentId}`); + return parent.children || []; +}; + +export const addChildToParent = (parentId: number, data: AddChildDto) => + http.post(`/school/parents/${parentId}/children/${data.studentId}`, { relationship: data.relationship }); + +export const removeChildFromParent = (parentId: number, studentId: number) => + http.delete<{ message: string }>(`/school/parents/${parentId}/children/${studentId}`); diff --git a/reading-platform-frontend/src/api/task.ts b/reading-platform-frontend/src/api/task.ts new file mode 100644 index 0000000..78b8c2b --- /dev/null +++ b/reading-platform-frontend/src/api/task.ts @@ -0,0 +1,175 @@ +import { http } from './index'; + +// ==================== 类型定义 ==================== + +export type TaskType = 'READING' | 'ACTIVITY' | 'HOMEWORK'; +export type TargetType = 'CLASS' | 'STUDENT'; +export type TaskStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED'; +export type CompletionStatus = 'PENDING' | 'IN_PROGRESS' | 'COMPLETED'; + +export interface Task { + id: number; + tenantId: number; + title: string; + description?: string; + taskType: TaskType; + targetType: TargetType; + relatedCourseId?: number; + createdBy: number; + startDate: string; + endDate: string; + status: TaskStatus; + createdAt: string; + updatedAt: string; + course?: { + id: number; + name: string; + }; + targetCount?: number; + completionCount?: number; +} + +export interface TaskCompletion { + id: number; + taskId: number; + studentId: number; + status: CompletionStatus; + completedAt?: string; + feedback?: string; + parentFeedback?: string; + createdAt: string; + student?: { + id: number; + name: string; + gender?: string; + class?: { + id: number; + name: string; + }; + }; +} + +export interface CreateTaskDto { + title: string; + description?: string; + taskType: TaskType; + targetType: TargetType; + relatedCourseId?: number; + startDate: string; + endDate: string; + targetIds: number[]; +} + +export interface UpdateTaskDto { + title?: string; + description?: string; + startDate?: string; + endDate?: string; + status?: TaskStatus; + targetIds?: number[]; +} + +export interface UpdateCompletionDto { + status: CompletionStatus; + feedback?: string; + parentFeedback?: string; +} + +export interface TaskStats { + totalTasks: number; + publishedTasks: number; + completedTasks: number; + inProgressTasks: number; +} + +// ==================== 学校端 API ==================== + +export const getTasks = (params?: { + page?: number; + pageSize?: number; + status?: TaskStatus; + taskType?: TaskType; + keyword?: string; +}) => + http.get<{ items: Task[]; total: number; page: number; pageSize: number }>( + '/school/tasks', + { params } + ); + +export const getTask = (id: number) => + http.get(`/school/tasks/${id}`); + +export const getTaskCompletions = (taskId: number, params?: { + page?: number; + pageSize?: number; + status?: CompletionStatus; +}) => + http.get<{ items: TaskCompletion[]; total: number; page: number; pageSize: number }>( + `/school/tasks/${taskId}/completions`, + { params } + ); + +export const createTask = (data: CreateTaskDto) => + http.post('/school/tasks', data); + +export const updateTask = (id: number, data: UpdateTaskDto) => + http.put(`/school/tasks/${id}`, data); + +export const deleteTask = (id: number) => + http.delete(`/school/tasks/${id}`); + +export const updateTaskCompletion = ( + taskId: number, + studentId: number, + data: UpdateCompletionDto +) => + http.put(`/school/tasks/${taskId}/completions/${studentId}`, data); + +export const getTaskStats = () => + http.get('/school/tasks/stats'); + +// ==================== 教师端 API ==================== + +export const getTeacherTasks = (params?: { + page?: number; + pageSize?: number; + status?: TaskStatus; + taskType?: TaskType; + keyword?: string; +}) => + http.get<{ items: Task[]; total: number; page: number; pageSize: number }>( + '/teacher/tasks', + { params } + ); + +export const getTeacherTask = (id: number) => + http.get(`/teacher/tasks/${id}`); + +export const getTeacherTaskCompletions = (taskId: number, params?: { + page?: number; + pageSize?: number; + status?: CompletionStatus; +}) => + http.get<{ items: TaskCompletion[]; total: number; page: number; pageSize: number }>( + `/teacher/tasks/${taskId}/completions`, + { params } + ); + +export const createTeacherTask = (data: CreateTaskDto) => + http.post('/teacher/tasks', data); + +export const updateTeacherTask = (id: number, data: UpdateTaskDto) => + http.put(`/teacher/tasks/${id}`, data); + +export const deleteTeacherTask = (id: number) => + http.delete(`/teacher/tasks/${id}`); + +export const updateTeacherTaskCompletion = ( + taskId: number, + studentId: number, + data: UpdateCompletionDto +) => + http.put(`/teacher/tasks/${taskId}/completions/${studentId}`, data); + +export const getTeacherTaskStats = () => + http.get('/teacher/tasks/stats'); diff --git a/reading-platform-frontend/src/api/teacher.ts b/reading-platform-frontend/src/api/teacher.ts new file mode 100644 index 0000000..1d6edeb --- /dev/null +++ b/reading-platform-frontend/src/api/teacher.ts @@ -0,0 +1,660 @@ +import { http } from './index'; + +// ==================== 教师课程 API ==================== + +export interface TeacherCourseQueryParams { + page?: number; + pageSize?: number; + grade?: string; + keyword?: string; +} + +export interface TeacherCourse { + id: number; + name: string; + pictureBookName?: string; + coverImagePath?: string; + gradeTags: string[]; + domainTags: string[]; + duration: number; + avgRating: number; + usageCount: number; + publishedAt: string; +} + +// 教师班级信息(更新:新增角色字段) +export interface TeacherClass { + id: number; + name: string; + grade: string; + studentCount: number; + lessonCount: number; + myRole: 'MAIN' | 'ASSIST' | 'CARE'; // 我在该班级的角色 + isPrimary: boolean; // 是否班主任 +} + +// 班级教师信息 +export interface TeacherClassTeacher { + teacherId: number; + teacherName: string; + teacherPhone?: string; + role: 'MAIN' | 'ASSIST' | 'CARE'; + isPrimary: boolean; +} + +// 获取教师可用的课程列表 +export function getTeacherCourses(params: TeacherCourseQueryParams): Promise<{ + items: TeacherCourse[]; + total: number; + page: number; + pageSize: number; +}> { + return http.get('/teacher/courses', { params }); +} + +// 获取课程详情 +export function getTeacherCourse(id: number): Promise { + return http.get(`/teacher/courses/${id}`); +} + +// 获取教师的班级列表 +export function getTeacherClasses(): Promise { + return http.get('/teacher/courses/classes'); +} + +// 获取教师所有学生列表(跨班级) +export function getTeacherStudents(params?: { page?: number; pageSize?: number; keyword?: string }): Promise<{ + items: Array<{ + id: number; + name: string; + gender?: string; + birthDate?: string; + classId: number; + class?: { + id: number; + name: string; + grade: string; + }; + parentName?: string; + parentPhone?: string; + createdAt: string; + }>; + total: number; + page: number; + pageSize: number; +}> { + return http.get('/teacher/students', { params }); +} + +// 获取班级学生列表 +export function getTeacherClassStudents(classId: number, params?: { page?: number; pageSize?: number; keyword?: string }): Promise<{ + items: Array<{ + id: number; + name: string; + gender?: string; + birthDate?: string; + parentName?: string; + parentPhone?: string; + lessonCount?: number; + readingCount?: number; + createdAt: string; + }>; + total: number; + page: number; + pageSize: number; + class?: { + id: number; + name: string; + grade: string; + studentCount: number; + lessonCount: number; + }; +}> { + return http.get(`/teacher/classes/${classId}/students`, { params }); +} + +// 获取班级教师列表 +export function getClassTeachers(classId: number): Promise { + return http.get(`/teacher/classes/${classId}/teachers`); +} + +// ==================== 授课记录 API ==================== + +export interface CreateLessonDto { + courseId: number; + classId: number; + plannedDatetime?: string; +} + +export interface FinishLessonDto { + overallRating?: string; + participationRating?: string; + completionNote?: string; + actualDuration?: number; +} + +export interface StudentRecordDto { + focus?: number; + participation?: number; + interest?: number; + understanding?: number; + notes?: string; +} + +// 获取授课记录列表 +export function getLessons(params?: { + page?: number; + pageSize?: number; + status?: string; + courseId?: number; +}): Promise<{ + items: any[]; + total: number; + page: number; + pageSize: number; +}> { + return http.get('/teacher/lessons', { params }); +} + +// 获取单个授课记录详情 +export function getLesson(id: number): Promise { + return http.get(`/teacher/lessons/${id}`); +} + +// 创建授课记录(备课) +export function createLesson(data: CreateLessonDto): Promise { + return http.post('/teacher/lessons', data); +} + +// 开始上课 +export function startLesson(id: number): Promise { + return http.post(`/teacher/lessons/${id}/start`); +} + +// 结束上课 +export function finishLesson(id: number, data: FinishLessonDto): Promise { + return http.post(`/teacher/lessons/${id}/finish`, data); +} + +// 取消课程 +export function cancelLesson(id: number): Promise { + return http.post(`/teacher/lessons/${id}/cancel`); +} + +// 保存学生评价记录 +export function saveStudentRecord( + lessonId: number, + studentId: number, + data: StudentRecordDto +): Promise { + return http.post(`/teacher/lessons/${lessonId}/students/${studentId}/record`, data); +} + +// 获取课程所有学生记录 +export interface StudentWithRecord { + id: number; + name: string; + gender?: string; + record: { + id: number; + focus?: number; + participation?: number; + interest?: number; + understanding?: number; + notes?: string; + } | null; +} + +export interface StudentRecordsResponse { + lesson: { + id: number; + status: string; + className: string; + }; + students: StudentWithRecord[]; +} + +export function getStudentRecords(lessonId: number): Promise { + return http.get(`/teacher/lessons/${lessonId}/student-records`); +} + +// 批量保存学生评价记录 +export function batchSaveStudentRecords( + lessonId: number, + records: Array<{ studentId: number } & StudentRecordDto> +): Promise<{ count: number; records: any[] }> { + return http.post(`/teacher/lessons/${lessonId}/student-records/batch`, { records }); +} + +// ==================== 教师首页 API ==================== + +export interface DashboardData { + stats: { + classCount: number; + studentCount: number; + lessonCount: number; + courseCount: number; + }; + todayLessons: Array<{ + id: number; + courseId: number; + courseName: string; + pictureBookName?: string; + classId: number; + className: string; + plannedDatetime: string; + status: string; + duration: number; + }>; + recommendedCourses: Array<{ + id: number; + name: string; + pictureBookName?: string; + coverImagePath?: string; + duration: number; + usageCount: number; + avgRating: number; + gradeTags: string[]; + }>; + weeklyStats: { + lessonCount: number; + studentParticipation: number; + avgRating: number; + totalDuration: number; + }; + recentActivities: Array<{ + id: number; + type: string; + description: string; + time: string; + }>; +} + +export const getTeacherDashboard = () => + http.get('/teacher/dashboard'); + +export const getTodayLessons = () => + http.get('/teacher/dashboard/today'); + +export const getRecommendedCourses = () => + http.get('/teacher/dashboard/recommend'); + +export const getWeeklyStats = () => + http.get('/teacher/dashboard/weekly'); + +// ==================== 教师统计趋势 ==================== + +export interface TeacherLessonTrendItem { + month: string; + lessonCount: number; + avgRating: number; +} + +export interface TeacherCourseUsageItem { + name: string; + value: number; +} + +export const getTeacherLessonTrend = (months?: number) => + http.get('/teacher/dashboard/lesson-trend', { params: { months } }); + +export const getTeacherCourseUsage = () => + http.get('/teacher/dashboard/course-usage'); + +// ==================== 课程反馈 API ==================== + +export interface FeedbackDto { + designQuality?: number; + participation?: number; + goalAchievement?: number; + stepFeedbacks?: any; + pros?: string; + suggestions?: string; + activitiesDone?: any; +} + +export interface LessonFeedback { + id: number; + lessonId: number; + teacherId: number; + designQuality?: number; + participation?: number; + goalAchievement?: number; + stepFeedbacks?: any; + pros?: string; + suggestions?: string; + activitiesDone?: any; + createdAt: string; + updatedAt: string; + teacher?: { + id: number; + name: string; + }; + lesson?: { + id: number; + startDatetime?: string; + course: { + id: number; + name: string; + pictureBookName?: string; + }; + class: { + id: number; + name: string; + }; + }; +} + +// 提交课程反馈 +export function submitFeedback(lessonId: number, data: FeedbackDto): Promise { + return http.post(`/teacher/lessons/${lessonId}/feedback`, data); +} + +// 获取课程反馈 +export function getFeedback(lessonId: number): Promise { + return http.get(`/teacher/lessons/${lessonId}/feedback`); +} + +// ==================== 学校端反馈 API ==================== + +export interface FeedbackQueryParams { + page?: number; + pageSize?: number; + teacherId?: number; + courseId?: number; +} + +export interface FeedbackStats { + totalFeedbacks: number; + avgDesignQuality: number; + avgParticipation: number; + avgGoalAchievement: number; + courseStats: Record; +} + +// 获取学校端反馈列表 +export function getSchoolFeedbacks(params: FeedbackQueryParams): Promise<{ + items: LessonFeedback[]; + total: number; + page: number; + pageSize: number; +}> { + return http.get('/school/feedbacks', { params }); +} + +// 获取反馈统计 +export function getFeedbackStats(): Promise { + return http.get('/school/feedbacks/stats'); +} + +// 获取教师自己的反馈列表 +export function getTeacherFeedbacks(params: FeedbackQueryParams): Promise<{ + items: LessonFeedback[]; + total: number; + page: number; + pageSize: number; +}> { + return http.get('/teacher/feedbacks', { params }); +} + +// 获取教师自己的反馈统计 +export function getTeacherFeedbackStats(): Promise { + return http.get('/teacher/feedbacks/stats'); +} + +// ==================== 排课管理 API ==================== + +export interface TeacherSchedule { + id: number; + classId: number; + className: string; + courseId: number; + courseName: string; + teacherId?: number; + scheduledDate?: string; + scheduledTime?: string; + weekDay?: number; + repeatType: 'NONE' | 'DAILY' | 'WEEKLY'; + source: 'SCHOOL' | 'TEACHER'; + status: 'ACTIVE' | 'CANCELLED'; + note?: string; + hasLesson: boolean; + lessonId?: number; + lessonStatus?: string; + createdAt: string; +} + +export interface CreateTeacherScheduleDto { + classId: number; + courseId: number; + scheduledDate?: string; + scheduledTime?: string; + weekDay?: number; + repeatType?: 'NONE' | 'DAILY' | 'WEEKLY'; + repeatEndDate?: string; + note?: string; +} + +export interface TeacherTimetableItem { + date: string; + weekDay: number; + schedules: TeacherSchedule[]; +} + +export const getTeacherSchedules = (params?: { + startDate?: string; + endDate?: string; + status?: string; + page?: number; + pageSize?: number; +}) => http.get<{ items: TeacherSchedule[]; total: number; page: number; pageSize: number }>('/teacher/schedules', { params }); + +export const getTeacherTimetable = (params: { startDate: string; endDate: string }) => + http.get('/teacher/schedules/timetable', { params }); + +export const getTodayTeacherSchedules = () => + http.get('/teacher/schedules/today'); + +export const createTeacherSchedule = (data: CreateTeacherScheduleDto) => + http.post('/teacher/schedules', data); + +export const updateTeacherSchedule = (id: number, data: Partial & { status?: string }) => + http.put(`/teacher/schedules/${id}`, data); + +export const cancelTeacherSchedule = (id: number) => + http.delete<{ message: string }>(`/teacher/schedules/${id}`); + +// ==================== 阅读任务 API ==================== + +export interface TeacherTask { + id: number; + tenantId: number; + title: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + targetType: 'CLASS' | 'STUDENT'; + status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED'; + relatedCourseId?: number; + startDate: string; + endDate: string; + createdBy: number; + createdAt: string; + updatedAt: string; + course?: { + id: number; + name: string; + }; + targetCount?: number; + completionCount?: number; +} + +export interface TaskCompletion { + id: number; + taskId: number; + studentId: number; + status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED'; + completedAt?: string; + feedback?: string; + parentFeedback?: string; + createdAt: string; + student: { + id: number; + name: string; + gender?: string; + class?: { + id: number; + name: string; + }; + }; +} + +export interface CreateTeacherTaskDto { + title: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + targetType: 'CLASS' | 'STUDENT'; + targetIds: number[]; + relatedCourseId?: number; + startDate: string; + endDate: string; +} + +export interface UpdateTaskCompletionDto { + status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED'; + feedback?: string; +} + +export const getTeacherTasks = (params?: { + page?: number; + pageSize?: number; + status?: string; + taskType?: string; + keyword?: string; +}) => http.get<{ items: TeacherTask[]; total: number; page: number; pageSize: number }>('/teacher/tasks', { params }); + +export const getTeacherTask = (id: number) => + http.get(`/teacher/tasks/${id}`); + +export const getTeacherTaskCompletions = (taskId: number, params?: { + page?: number; + pageSize?: number; + status?: string; +}) => http.get<{ items: TaskCompletion[]; total: number; page: number; pageSize: number }>(`/teacher/tasks/${taskId}/completions`, { params }); + +export const createTeacherTask = (data: CreateTeacherTaskDto) => + http.post('/teacher/tasks', data); + +export const updateTeacherTask = (id: number, data: Partial & { status?: string }) => + http.put(`/teacher/tasks/${id}`, data); + +export const deleteTeacherTask = (id: number) => + http.delete<{ message: string }>(`/teacher/tasks/${id}`); + +export const updateTaskCompletion = (taskId: number, studentId: number, data: UpdateTaskCompletionDto) => + http.put(`/teacher/tasks/${taskId}/completions/${studentId}`, data); + +export const sendTaskReminder = (taskId: number) => + http.post<{ message: string }>(`/teacher/tasks/${taskId}/remind`); + +// ==================== 任务模板 API ==================== + +export interface TaskTemplate { + id: number; + tenantId: number; + name: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + relatedCourseId?: number; + defaultDuration: number; + isDefault: boolean; + status: string; + createdBy: number; + createdAt: string; + updatedAt: string; + course?: { + id: number; + name: string; + pictureBookName?: string; + }; +} + +export interface CreateTaskTemplateDto { + name: string; + description?: string; + taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK'; + relatedCourseId?: number; + defaultDuration?: number; + isDefault?: boolean; +} + +export interface CreateTaskFromTemplateDto { + templateId: number; + targetIds: number[]; + targetType: 'CLASS' | 'STUDENT'; + startDate?: string; +} + +export const getTaskTemplates = (params?: { + page?: number; + pageSize?: number; + taskType?: string; + keyword?: string; +}) => http.get<{ items: TaskTemplate[]; total: number; page: number; pageSize: number }>('/teacher/task-templates', { params }); + +export const getTaskTemplate = (id: number) => + http.get(`/teacher/task-templates/${id}`); + +export const getDefaultTaskTemplate = (taskType: string) => + http.get(`/teacher/task-templates/default/${taskType}`); + +export const createTaskFromTemplate = (data: CreateTaskFromTemplateDto) => + http.post('/teacher/tasks/from-template', data); + +// ==================== 任务统计 API ==================== + +export interface TaskStats { + totalTasks: number; + publishedTasks: number; + completedTasks: number; + inProgressTasks: number; + pendingCount: number; + totalCompletions: number; + completionRate: number; +} + +export interface TaskStatsByType { + [key: string]: { + total: number; + completed: number; + rate: number; + }; +} + +export interface TaskStatsByClass { + classId: number; + className: string; + grade: string; + total: number; + completed: number; + rate: number; +} + +export interface MonthlyTaskStats { + month: string; + tasks: number; + completions: number; + completed: number; + rate: number; +} + +export const getTaskStats = () => + http.get('/teacher/tasks/stats'); + +export const getTaskStatsByType = () => + http.get('/teacher/tasks/stats/by-type'); + +export const getTaskStatsByClass = () => + http.get('/teacher/tasks/stats/by-class'); + +export const getMonthlyTaskStats = (months?: number) => + http.get('/teacher/tasks/stats/monthly', { params: { months } }); diff --git a/reading-platform-frontend/src/api/theme.ts b/reading-platform-frontend/src/api/theme.ts new file mode 100644 index 0000000..01e8e86 --- /dev/null +++ b/reading-platform-frontend/src/api/theme.ts @@ -0,0 +1,58 @@ +import { http } from './index'; + +export interface Theme { + id: number; + name: string; + description?: string; + sortOrder: number; + status: string; + createdAt: string; + courses?: { + id: number; + name: string; + coverImagePath?: string; + }[]; +} + +export interface CreateThemeData { + name: string; + description?: string; + sortOrder?: number; +} + +export interface UpdateThemeData { + name?: string; + description?: string; + sortOrder?: number; + status?: string; +} + +// 获取主题列表 +export function getThemeList() { + return http.get('/admin/themes'); +} + +// 获取主题详情 +export function getThemeDetail(id: number) { + return http.get(`/admin/themes/${id}`); +} + +// 创建主题 +export function createTheme(data: CreateThemeData) { + return http.post('/admin/themes', data); +} + +// 更新主题 +export function updateTheme(id: number, data: UpdateThemeData) { + return http.put(`/admin/themes/${id}`, data); +} + +// 删除主题 +export function deleteTheme(id: number) { + return http.delete(`/admin/themes/${id}`); +} + +// 重新排序主题 +export function reorderThemes(ids: number[]) { + return http.put('/admin/themes/reorder', { ids }); +} diff --git a/reading-platform-frontend/src/auto-imports.d.ts b/reading-platform-frontend/src/auto-imports.d.ts new file mode 100644 index 0000000..0ca6bef --- /dev/null +++ b/reading-platform-frontend/src/auto-imports.d.ts @@ -0,0 +1,90 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const Modal: typeof import('ant-design-vue')['Modal'] + const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] + const computed: typeof import('vue')['computed'] + const createApp: typeof import('vue')['createApp'] + const createPinia: typeof import('pinia')['createPinia'] + const customRef: typeof import('vue')['customRef'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const defineStore: typeof import('pinia')['defineStore'] + const effectScope: typeof import('vue')['effectScope'] + const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const h: typeof import('vue')['h'] + const inject: typeof import('vue')['inject'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const mapActions: typeof import('pinia')['mapActions'] + const mapGetters: typeof import('pinia')['mapGetters'] + const mapState: typeof import('pinia')['mapState'] + const mapStores: typeof import('pinia')['mapStores'] + const mapWritableState: typeof import('pinia')['mapWritableState'] + const markRaw: typeof import('vue')['markRaw'] + const message: typeof import('ant-design-vue')['message'] + const nextTick: typeof import('vue')['nextTick'] + const notification: typeof import('ant-design-vue')['notification'] + const onActivated: typeof import('vue')['onActivated'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onMounted: typeof import('vue')['onMounted'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const provide: typeof import('vue')['provide'] + const reactive: typeof import('vue')['reactive'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const setActivePinia: typeof import('pinia')['setActivePinia'] + const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const storeToRefs: typeof import('pinia')['storeToRefs'] + const toRaw: typeof import('vue')['toRaw'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const unref: typeof import('vue')['unref'] + const useAttrs: typeof import('vue')['useAttrs'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVars: typeof import('vue')['useCssVars'] + const useId: typeof import('vue')['useId'] + const useLink: typeof import('vue-router')['useLink'] + const useModel: typeof import('vue')['useModel'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] + const useSlots: typeof import('vue')['useSlots'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const watch: typeof import('vue')['watch'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') +} diff --git a/reading-platform-frontend/src/components.d.ts b/reading-platform-frontend/src/components.d.ts new file mode 100644 index 0000000..e3cda1f --- /dev/null +++ b/reading-platform-frontend/src/components.d.ts @@ -0,0 +1,89 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + AAlert: typeof import('ant-design-vue/es')['Alert'] + AAvatar: typeof import('ant-design-vue/es')['Avatar'] + ABadge: typeof import('ant-design-vue/es')['Badge'] + AButton: typeof import('ant-design-vue/es')['Button'] + AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup'] + ACard: typeof import('ant-design-vue/es')['Card'] + ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] + ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup'] + ACol: typeof import('ant-design-vue/es')['Col'] + ACollapse: typeof import('ant-design-vue/es')['Collapse'] + ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] + ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] + ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] + ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] + ADivider: typeof import('ant-design-vue/es')['Divider'] + ADrawer: typeof import('ant-design-vue/es')['Drawer'] + ADropdown: typeof import('ant-design-vue/es')['Dropdown'] + AEmpty: typeof import('ant-design-vue/es')['Empty'] + AForm: typeof import('ant-design-vue/es')['Form'] + AFormItem: typeof import('ant-design-vue/es')['FormItem'] + AImage: typeof import('ant-design-vue/es')['Image'] + AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup'] + AInput: typeof import('ant-design-vue/es')['Input'] + AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] + AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] + AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] + ALayout: typeof import('ant-design-vue/es')['Layout'] + ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] + ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] + ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] + AList: typeof import('ant-design-vue/es')['List'] + AListItem: typeof import('ant-design-vue/es')['ListItem'] + AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta'] + AMenu: typeof import('ant-design-vue/es')['Menu'] + AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] + AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] + AModal: typeof import('ant-design-vue/es')['Modal'] + APageHeader: typeof import('ant-design-vue/es')['PageHeader'] + APagination: typeof import('ant-design-vue/es')['Pagination'] + APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] + AProgress: typeof import('ant-design-vue/es')['Progress'] + ARadio: typeof import('ant-design-vue/es')['Radio'] + ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] + ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] + ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] + ARate: typeof import('ant-design-vue/es')['Rate'] + AResult: typeof import('ant-design-vue/es')['Result'] + ARow: typeof import('ant-design-vue/es')['Row'] + ASelect: typeof import('ant-design-vue/es')['Select'] + ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup'] + ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] + ASkeleton: typeof import('ant-design-vue/es')['Skeleton'] + ASpace: typeof import('ant-design-vue/es')['Space'] + ASpin: typeof import('ant-design-vue/es')['Spin'] + AStatistic: typeof import('ant-design-vue/es')['Statistic'] + AStep: typeof import('ant-design-vue/es')['Step'] + ASteps: typeof import('ant-design-vue/es')['Steps'] + ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] + ASwitch: typeof import('ant-design-vue/es')['Switch'] + ATable: typeof import('ant-design-vue/es')['Table'] + ATabPane: typeof import('ant-design-vue/es')['TabPane'] + ATabs: typeof import('ant-design-vue/es')['Tabs'] + ATag: typeof import('ant-design-vue/es')['Tag'] + ATextarea: typeof import('ant-design-vue/es')['Textarea'] + ATimeline: typeof import('ant-design-vue/es')['Timeline'] + ATimelineItem: typeof import('ant-design-vue/es')['TimelineItem'] + ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker'] + ATooltip: typeof import('ant-design-vue/es')['Tooltip'] + ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] + AUpload: typeof import('ant-design-vue/es')['Upload'] + AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] + FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default'] + FileUploader: typeof import('./components/course/FileUploader.vue')['default'] + LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default'] + LessonStepsEditor: typeof import('./components/course/LessonStepsEditor.vue')['default'] + NotificationBell: typeof import('./components/NotificationBell.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + } +} diff --git a/reading-platform-frontend/src/components/FilePreviewModal.vue b/reading-platform-frontend/src/components/FilePreviewModal.vue new file mode 100644 index 0000000..2e3c0af --- /dev/null +++ b/reading-platform-frontend/src/components/FilePreviewModal.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/reading-platform-frontend/src/components/NotificationBell.vue b/reading-platform-frontend/src/components/NotificationBell.vue new file mode 100644 index 0000000..36ec186 --- /dev/null +++ b/reading-platform-frontend/src/components/NotificationBell.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/reading-platform-frontend/src/components/course/FileUploader.vue b/reading-platform-frontend/src/components/course/FileUploader.vue new file mode 100644 index 0000000..1fa2028 --- /dev/null +++ b/reading-platform-frontend/src/components/course/FileUploader.vue @@ -0,0 +1,296 @@ +