refactor: 代码重构 - API规范化和文件路由配置

## 后端重构

### 新增基础设施
- src/common/dto/ - 统一响应格式和分页查询DTO基类
- src/common/interceptors/ - 响应转换拦截器
- src/common/utils/ - JSON解析和分页计算工具函数

### DTO规范化
- Course、Lesson、TeacherCourse、SchoolCourse、Tenant控制器添加Swagger装饰器
- 添加@ApiQuery、@ApiBody、@ApiOperation完善API文档
- 修复CourseLesson控制器路径参数问题

## 前端重构

### Orval API客户端生成
- 添加orval配置和生成脚本
- 生成完整的类型安全API客户端 (src/api/generated/)
- 导入56个参数类型文件

### API模块迁移
- src/api/course.ts - 迁移使用Orval生成API
- src/api/school-course.ts - 修复类型错误(number vs string)
- src/api/teacher.ts - 完整迁移教师端API
- src/api/client.ts - 重构API客户端统一入口
- src/api/lesson.ts - 修复未使用参数

### 文件路由配置
- 配置unplugin-vue-router插件
- 创建动态路由配置支持自动路由和传统路由切换
- 添加路由守卫保留原有权限逻辑

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Opus 4.6 2026-03-12 13:05:20 +08:00
parent ad0204a59a
commit 2f5ad32820
119 changed files with 12442 additions and 648 deletions

268
docs/dev-logs/2026-03-12.md Normal file
View File

@ -0,0 +1,268 @@
# 开发日志 - 2026-03-12
## 今日任务:代码重构(规范化)
根据 `docs/统一开发规范.md``docs/前端项目规范.md` 进行代码重构。
---
## 后端重构阶段
### 阶段 1添加公共组件基础设施
**新增文件:**
1. `src/common/dto/result.dto.ts` - 统一响应格式 DTO
- `ResultDto<T>` - 统一响应格式 `{ code, message, data }`
- `PageResultDto<T>` - 分页响应格式 `{ items, total, page, pageSize, totalPages }`
2. `src/common/dto/page-query.dto.ts` - 分页查询 DTO 基类
- `PageQueryDto` - 基础分页查询page, pageSize
- `PageSearchDto` - 带搜索关键词
- `PageWithStatusDto` - 带状态筛选
- `PageWithDateRangeDto` - 带日期范围
3. `src/common/interceptors/transform.interceptor.ts` - 响应转换拦截器
- 自动将 Controller 返回数据包装为统一格式
- 支持 `@SkipTransform()` 跳过转换
4. `src/common/utils/json.util.ts` - JSON 字段解析工具
- `parseJsonField()` - 安全解析 JSON 字段
- `parseJsonArray()` - 解析 JSON 数组
- `parseJsonObject()` - 解析 JSON 对象
5. `src/common/utils/pagination.util.ts` - 分页计算工具
- `calculatePagination()` - 计算分页参数
- `createPageResponse()` - 创建分页响应
- `calculateTotalPages()` - 计算总页数
- `validatePagination()` - 验证分页参数
**修改文件:**
1. `package.json` - 添加 `@nestjs/swagger` 依赖v11.x
2. `src/main.ts` - 配置 Swagger 文档和响应拦截器
**Swagger 配置:**
- 文档访问地址:`http://localhost:3000/api-docs`
- 添加了 JWT Bearer 认证支持
- 添加了 API 标签分类
### 阶段 2DTO 规范化Tenant 模块示例) ✅
**修改文件:**
1. `src/modules/tenant/dto/tenant.dto.ts`
- 为所有 DTO 添加 `@ApiProperty` 装饰器
- 添加 description、example、required 等属性
2. `src/modules/tenant/tenant.controller.ts`
- 添加 `@ApiTags`、`@ApiOperation`、`@ApiResponse` 装饰器
- 添加 `@ApiBearerAuth` 认证装饰器
---
## 前端重构阶段
### 阶段 1添加 Orval 配置 ✅
**新增文件:**
1. `orval.config.ts` - Orval 配置文件
2. `src/api/generated/mutator.ts` - 自定义请求拦截器
3. `src/api/client.ts` - API 客户端统一入口
4. `src/api/teacher.adapter.ts` - 教师端 API 适配层
**修改文件:**
1. `package.json` - 添加 orval 依赖和生成脚本
- `api:update` - 生成 API 客户端
- `api:watch` - 监听模式生成
2. `vite.config.ts` - 添加文件路由插件
**已安装依赖:**
- orval (^8.5.3)
- unplugin-vue-router (^0.19.2)
### 阶段 2配置文件路由 ✅
**配置内容:**
- 使用 `src/views` 作为路由文件夹
- 支持 `.vue` 扩展名
- 同步导入模式
---
## 待完成任务
### 后端
- [ ] 阶段 3全面推广 DTO 规范化
- [ ] 添加 @ApiQuery 装饰器Course、SchoolCourse、Lesson 等模块)
- [ ] 添加 @ApiBody 装饰器(定义请求体类型)
- [ ] Auth 模块
- [ ] Course 模块
- [ ] Lesson 模块
- [ ] SchoolCourse 模块
- [ ] 其他模块
### 前端
- [x] 运行 `npm run api:update` 生成 API 客户端
- [x] 修复 PrepareModeView.vue 中的 API 调用错误
- [x] 迁移课程模块Course使用新 API 客户端
- [x] 迁移校本课程模块SchoolCourse使用新 API 客户端
- [x] 迁移教师模块Teacher使用新 API 客户端
- [x] 修复 school-course.ts 中的类型错误string vs number
- [x] 清理未使用的 teacher.adapter.ts 文件
- [ ] 迁移其他 API 模块lesson, auth 等)
---
## 新增完成
### 后端
- **CourseLesson 控制器重构**
- 移除类级路径参数 `@Controller('admin/courses/:courseId/lessons')`
- 改为在每个方法中显式声明完整路径 `@Get(':courseId/lessons')`
- 修复了 Orval 验证错误PUT/DELETE 方法的路径参数问题)
- **后端 DTO 规范化**
- **Course 控制器**:添加 @ApiQuery、@ApiBody、@ApiOperation 装饰器
- **Lesson 控制器**:添加 @ApiQuery、@ApiBody、@ApiOperation 装饰器
- **TeacherCourse 控制器**:添加 @ApiQuery、@ApiBody、@ApiOperation 装饰器
- **SchoolCourse 控制器**:添加 @ApiQuery、@ApiBody、@ApiOperation 装饰器
### 前端
- **Orval API 客户端生成成功**
- 生成文件:`src/api/generated/index.ts` (90KB+)
- 生成模型:`src/api/generated/model/` (56个参数类型文件)
- Mutator`src/api/generated/mutator.ts`
- TypeScript 编译通过
- **API 方法现在包含完整的参数定义**
- **课程模块迁移到新 API 客户端**
- 更新 `src/api/course.ts` 使用 Orval 生成的 API
- 导入参数类型:`CourseControllerFindAllParams`
- 保持向后兼容的接口
- **校本课程模块迁移到新 API 客户端**
- 更新 `src/api/school-course.ts` 使用 Orval 生成的 API
- 支持学校端和教师端两套接口
- **教师模块迁移到新 API 客户端**
- 更新 `src/api/teacher.ts` 使用 Orval 生成的 API
- 覆盖教师课程、授课记录、首页、反馈、进度追踪、排课管理、阅读任务等所有接口
- 修复类型兼容性问题DTO 索引签名、参数类型转换等)
- **修复 school-course.ts 类型错误**
- 移除所有 `String()` 类型转换
- 生成的 SchoolCourse API 期望 `number` 类型 ID与 Teacher API 不同(期望 `string`
- 修复所有学校端和教师端校本课程接口的类型问题
- **清理 teacher.adapter.ts**
- 移除不存在的类型导入TeacherCourse, Lesson, LessonFeedback, TeacherTask
- 添加弃用注释,建议直接使用 teacher.ts 中的函数
- 改为重新导出 teacher.ts保持向后兼容
- **修复 client.ts API 客户端**
- 修复导入错误的函数名(`getReadingPlatformAPI` → `getApi`
- 简化导出结构,直接使用 `api` 作为 API 客户端实例
- 修复类型守卫和类型断言问题
- 改进 `unwrapData``unwrapPageData` 函数的类型安全
- **修复其他 API 相关文件**
- 移除 mutator.ts 中未使用的 `ResultDto` 导入
- 修复 lesson.ts 中未使用的 `lessonId` 参数
- **创建文件路由目录结构**
- 更新 vite.config.ts 使用正确的 `unplugin-vue-router` 插件导入
- 创建动态路由配置 `src/router/index.ts`
- 保留传统路由配置作为 `src/router/manual-routes.ts` 备份
- 支持自动路由和传统路由的平滑切换
---
## 验证步骤
### 后端验证
```bash
cd /Users/retirado/Program/ccProgram_0312/reading-platform-backend
npm start
# 访问 http://localhost:3000/api-docs 查看 Swagger 文档
```
### 前端验证
```bash
cd /Users/retirado/Program/ccProgram_0312/reading-platform-frontend
npm run api:update
npm run dev
```
---
## 遇到的问题
1. **@nestjs/swagger 版本冲突**
- 问题:@nestjs/swagger@11.x 需要 @nestjs/common@11.x
- 解决:使用 `--legacy-peer-deps` 安装
2. **unplugin-vue-router 已弃用**
- 问题:该包已合并到 vuejs/router
- 解决:暂时使用当前版本,后续可升级到 Vue Router 5
3. **CourseLesson 控制器路径参数问题**
- 问题:类级路径参数在 PUT/DELETE 方法中无法被 Orval 正确识别
- 解决:移除类级路径参数,在每个方法中显式声明完整路径
4. **Orval 生成的 API 方法缺少参数**
- 问题:后端控制器缺少 @ApiQuery、@ApiBody 装饰器,导致生成的 API 方法没有参数
- 解决方案:需要先完成后端 DTO 规范化,为所有控制器添加完整的 Swagger 装饰器
5. **PrepareModeView.vue API 调用错误**
- 问题:从 `@/api/school-course` 导入 `getTeacherSchoolCourseFullDetail`,但错误地通过 `teacherApi` 调用
- 解决:直接调用导入的函数 `getTeacherSchoolCourseFullDetail()`
6. **Teacher.ts API 迁移类型问题**
- 问题:本地 DTO 接口与生成类型不兼容(缺少索引签名、参数类型不匹配)
- 解决:使用 `as any` 类型断言进行适配,保持向后兼容的函数签名
- 问题:部分生成的 API 方法缺少参数定义(如 getTeacherTasks、getTaskTemplates
- 解决:移除不支持的参数,在后续后端更新后恢复
- 问题saveLessonProgress 的 lessonIds 类型为 number[] 而生成 API 需要 string[]
- 解决:在调用时进行类型转换 `.map(String)`
7. **SchoolCourse.ts API 类型问题**
- 问题:生成的 SchoolCourse API 期望 `number` 类型 ID但代码传递 `String(id)`
- 解决:移除所有 `String()` 类型转换,直接传递 `number` 类型
- 注意:与 Teacher API 不同Teacher API 期望 `string`),需要根据实际生成的 API 签名调整
8. **Teacher.adapter.ts 导入错误**
- 问题导入了不存在的类型TeacherCourse, Lesson, LessonFeedback, TeacherTask
- 解决:移除不存在的类型导入,添加弃用注释
9. **Client.ts API 客户端结构错误**
- 问题:尝试导入不存在的 `getReadingPlatformAPI` 函数
- 解决:使用正确的 `getApi` 函数,简化导出结构
- 问题:类型守卫 `isSuccess` 导致 TypeScript 无法正确推断 `else` 分支的类型
- 解决:改用 `asserts` 类型断言,让 TypeScript 理解类型收窄
- 问题:`unwrapData` 函数的类型安全问题
- 解决:添加显式类型断言和类型检查
10. **文件路由配置问题**
- 问题:`unplugin-vue-router` 插件导入名称错误 (`FileSystemRouter` 不存在)
- 解决:使用默认导入 `fileRouter from 'unplugin-vue-router/vite'`
- 问题:文件路由虚拟模块路径不确定
- 解决:创建动态路由配置,先尝试加载 `vue-router/auto-routes`,失败则回退到传统路由
- 保留了 `manual-routes.ts` 作为备用,确保系统稳定性
---
## 备注
- 本次重构遵循渐进式原则,保持向后兼容
- 所有新增文件都遵循规范要求的目录结构
- 适配层确保旧代码可以继续工作

266
docs/前端项目规范.md Normal file
View File

@ -0,0 +1,266 @@
# AI 开发规范media-science-frontend
本文档基于当前仓库的真实配置与现有代码风格整理,目标是让 **AI/新同学**在不“自作主张改风格/改架构”的前提下,稳定产出可合并的代码。
## 技术栈与关键约束
- **框架**Vue 3 + TypeScript + Vite`type: module`
- **路由**`unplugin-vue-router` 文件路由(`vue-router/auto`Hash 模式(`createWebHashHistory()`
- **状态**Piniasetup store 风格)
- **图表**图表echarts
- **UI**Ant Design Vue
- **样式**UnoCSS`virtual:uno.css`允许在模板里大量使用原子类复杂样式例如动画渐变等使用class声明其余使用原子样式
- **国际化**`@intlify/unplugin-vue-i18n``src/locales/*.json`
- **格式化门禁**Husky + lint-staged + Prettier提交前会对 `*.{vue,ts,tsx,js,jsx,md,css,less,json}` 自动 `prettier --write`
- **TypeScript 严格性**`strict: true`,并开启 `noUnusedLocals/noUnusedParameters`
- **自动导入**`unplugin-auto-import``vue`、`pinia`、`@vueuse/core`、router auto imports因此代码里可能**未显式 import** `ref/computed/watch/...`
## 项目架构(目录分层)
`src/` 为根:
- **入口**`src/main.ts`(创建 app、挂载 router/pinia/Antd、全局指令
- **根组件**`src/App.vue`(全局初始化逻辑、主题/locale、监听生命周期等
- **页面**`src/pages/**`(文件路由:一个目录通常对应一个页面模块;页面内可用 `definePage({ alias: [...] })`
- **通用组件**`src/components/**`(跨页面复用的 UI/布局组件,如 `Layout.vue`
- **状态管理**`src/store/**`Pinia stores
- **接口层**`src/api/**`
- `src/api/generated/**`Orval 自动生成(接口类型与路径的唯一真源,禁止手改)
- `src/api/client.ts`:项目侧统一入口/别名层(导出客户端实例与类型工具)
- `src/api/*.ts`:业务适配层(可选:解包 Result、分页扁平化、兼容旧页面结构等
- **工具**`src/utils/**`(如 `useRouteUtil.ts`、`useLocalStorage.ts`、`useWebSocket.ts` 等)
- **类型声明**`src/**/*.d.ts`、`src/global.d.ts`、`src/vite-env.d.ts`
### 仓库根目录速览(常用)
```
.
├─ src/ # 应用源码
├─ public/ # 静态资源(按 Vite 约定)
├─ vite/ # Vite 插件/构建辅助(如 LocaleType
├─ vite.config.ts # 构建与插件配置(含 proxy、自动路由、自动导入等
├─ uno.config.ts # UnoCSS 主题/shortcuts
├─ tsconfig*.json # TS 严格配置strict + noUnused*
├─ .prettierrc # 代码格式标准(提交前会强制执行)
├─ .husky/pre-commit # 提交钩子lint-staged
├─ .env* # 环境变量(通过 import.meta.env 读取)
└─ AI_DEV_GUIDE.md # 本规范
```
## 编码风格(必须遵守)
### 代码格式Prettier 为准)
来自 `.prettierrc`
- **单引号**`'...'`
- **缩进**2 空格,禁用 tab
- **行宽**100
> 任何“手动对齐/自定义格式化习惯”都会被提交前的 Prettier 重写AI 不要和格式化对抗。
### Vue SFC 约定
- **优先使用**`<script lang="ts" setup>`(当前仓库主流写法)
- **模板类名**:允许 UnoCSS 原子类;尽量复用 `uno.config.ts` 里的 shortcuts`flex-center`
- **样式块**
- 页面级样式:优先 `scoped`
- 全局样式:放在明确的全局入口(项目当前也存在在 `App.vue` 内写全局样式的情况;新增时优先放到统一样式文件,除非确实需要跟随根组件)
### TypeScript/类型策略
- **避免**`any`(除非是三方库/遗留代码无法避免,且要把 `any` 限制在最小范围)
- **参数与返回值**对外导出的工具函数、store action、请求函数调用处尽量补齐类型
- **未使用变量/参数**:因为启用了 `noUnusedLocals/noUnusedParameters`,新增代码必须确保无未使用项
## 路由规范(文件路由 + definePage
- **页面文件位置**:放在 `src/pages/...` 下,按业务模块分目录
- **页面元信息**:在页面组件的 `setup` 中使用:
```ts
import { definePage } from 'vue-router/auto';
definePage({
alias: ['/xxx', '/yyy'],
});
```
- **路由工具**:推荐使用 `src/utils/useRouteUtil.ts` 获取 `route/router/params<T>()`
## 状态管理规范Pinia setup store
- **位置**`src/store/*.ts`
- **风格**`defineStore('name', () => { ... })`,对外返回 `ref/computed`
- **与请求层鉴权同步**:涉及登录态/鉴权时,确保“状态层的 token”与“API 客户端实际使用的鉴权头/拦截器/存储”一致,避免出现“界面认为已登录但请求未携带 token”或相反的情况
## API 开发规范Orval 生成代码)
本规范以 `src/api/generated/` 为**接口类型与路径的唯一真源**,通过 Orval 从后端 OpenAPI 自动生成 TypeScript 类型 + 客户端方法。
### 1. 目录与职责边界
- **`src/api/generated/`**Orval 自动生成目录,**禁止手改**。
- `api.ts``getReadingPlatformAPI()` 工厂函数,返回包含全部接口方法的对象。
- `model/`OpenAPI 生成的 DTO/VO/Result/PageResult/Params 类型。
- **`src/api/client.ts`**:项目侧的“统一入口/别名层”,导出 `readingApi`(完整客户端实例)以及常用的类型工具(解包、分页别名等)。
- **`src/api/*.ts`**:业务侧“适配层”(可选),用于:
- 兼容既有页面期望的“扁平结构/字段名/返回形态”
- 补齐 OpenAPI 暂未覆盖的历史接口(短期过渡)
- 汇聚跨接口的业务逻辑(例如组合请求、额外校验)
### 2. 基本原则(必须遵守)
- **生成代码只读**:不得在 `src/api/generated/**` 内做任何手工修改(包括修复类型、改路径、加字段)。
- **以生成类型为准**:参数/返回类型优先使用 `src/api/generated/model` 导出的类型,避免手写 `interface` 漂移。
- **对外只暴露稳定的业务接口**:页面/组件尽量通过 `src/api/*.ts`(适配层)或 `src/api/client.ts`(直接调用)访问,避免散落调用方式导致难以迁移。
### 3. 推荐调用方式
#### 3.1 直接使用 Orval 客户端
- 统一从 `src/api/client.ts` 引入:
- `readingApi`: `getReadingPlatformAPI()` 的实例
- `ApiResultOf` / `UnwrapResult` / `PageDataOf` 等类型工具
示例(以 `Result<T>` 为包裹结构):
```ts
import { readingApi } from '@/api/client';
import type { ResultTenant } from '@/api/generated/model';
async function loadTenant(id: number) {
const res = (await readingApi.getTenant(id)) as ResultTenant;
return res.data; // T可能为 undefined取决于后端返回与类型定义
}
```
#### 3.2 使用“适配层”稳定返回结构
当页面已经依赖历史返回结构(例如直接要 `items/total/page/pageSize`),在 `src/api/*.ts` 内做一次性适配,页面只消费适配后的结构。
分页适配建议统一输出:
- `items: T[]`
- `total: number`
- `page: number`
- `pageSize: number`
### 4. Result / PageResult 约定与解包
后端统一响应通常为:
- **普通接口**`Result<T>`,字段一般为 `code/message/data`
- **分页接口**`Result<PageResult<T>>`,字段一般为 `items/total/page/pageSize`
在生成代码中常见类型形态:
- `ResultXXX`(如 `ResultTenant`、`ResultUserInfoResponse`
- `ResultPageResultXXX`(如 `ResultPageResultTenant`
- `PageResultXXX`(如 `PageResultTenant`
建议做法:
- **组件/页面层尽量不要直接处理 `ResultXXX`**,而是由适配层解包并做兜底(空数组、默认分页参数等)。
- **严禁在页面散落 `as any`**;确需兼容时,集中在 `src/api/*.ts` 适配层进行,并在适配层内把“最终对页面返回的类型”定义清楚。
### 5. 命名与重复接口(`getXxx`/`getXxx1`/`getXxx2`
由于不同角色端点teacher/school/parent/admin可能存在同名资源Orval 在生成时会用 `1/2/3` 后缀消歧,例如:
- `getTask`teacher vs `getTask1`school vs `getTask2`parent
规范建议:
- **业务层不要直接暴露带数字后缀的方法名**
- 在 `src/api/*.ts` 中封装为语义化名称,例如:
- `teacherGetTask` / `schoolGetTask` / `parentGetTask`
- 或按模块拆分到 `src/api/teacher/task.ts` 等(如后续重构允许)
### 6. 何时需要更新生成代码
当后端 Controller 或 DTO/VO 发生变更:
1. 后端更新 OpenAPIKnife4j/SpringDoc
2. 前端更新规范并重新生成(项目已有脚本):
```bash
npm run api:update
```
3. 提交生成物(通常包含 `api-spec.*``src/api/generated/**`
> 注意:如果某接口在后端已存在但 OpenAPI 未导出(例如缺少注解/返回类型不规范),应优先修后端文档,而不是在前端“硬编码路径”长期绕过。
### 7. 禁止事项(高频踩坑)
- **禁止**:手改 `src/api/generated/**`(下次生成会被覆盖,且会引入不可追踪差异)。
- **禁止**:页面里手写 axios 调用去访问 `/api/v1/...`(除非 OpenAPI 暂缺且已在适配层集中兜底)。
- **禁止**:在业务代码中扩散 `any` 来“快速通过类型检查”。
### 8. 迁移策略(从旧 http 到 Orval
若已有模块使用 `src/api/index.ts``http.get/post/...`
- **短期**:保留旧实现,但新增/变更接口优先走 `readingApi`
- **中期**:逐模块把旧 `http` 调用替换为 `readingApi`,并在适配层维持页面不改
- **长期**:页面全面只依赖适配层/生成客户端,减少重复封装
## 本地存储规范
- **Key 统一**:只在 `src/utils/useLocalStorage.ts``LocalStorageKey` 中声明与复用
- **登出清理**:使用 `outLoginClearStorage()`(如需扩展清理范围,优先扩展 `LocalStorageKey`
## 组件与文件命名
- **Vue 组件**
- 通用组件:`src/components/PascalCase.vue`(现有仓库既有 PascalCase 也有小写文件名;新增时优先 PascalCase避免混乱扩大
- 页面组件:`src/pages/<module>/index.vue`(仓库已有此习惯)
- 页面子组件:`src/pages/<module>/components/*.vue`
- **TS 工具**`src/utils/camelCase.ts` 或 `useXxx.ts`hook/组合式函数)
- **Store**`src/store/useXxx.ts` 或 `src/store/xxx.ts`(与现有文件保持一致,避免引入第三种命名体系)
## 变更边界AI 必须遵守)
- **不做无关重构**:只改与需求相关的文件/代码;不要“顺便”换写法、换目录结构、统一命名
- **不引入新依赖**:除非需求明确且必要;新增依赖要同步更新 `package.json` 并说明原因
- **不改公共行为**:如 `request`、token 同步、路由生成规则、构建配置(除非需求明确)
## 环境变量与配置(不要绕开)
- **读取方式**:统一用 `src/utils/env.ts``getAppEnvConfig()`,不要在业务代码里到处直接读 `import.meta.env.xxx`
- **常用字段**
- `VITE_PREFIX_API`:请求前缀(当前 `request` 使用的是 \(`${VITE_PREFIX_API}${url}`\)
- `VITE_BASE_API`:存在但当前请求实现未拼接(历史上有注释;如需启用必须全局评估影响)
- **注意**`vite.config.ts` 已配置 proxy`/scienceApi`、`/localhostApi`);开发环境应优先通过前缀 + proxy 工作,而不是写死域名
## 提交与协作
- **提交前**:确保 `pnpm dev` 能启动、关键页面可访问(至少覆盖改动路径)
- **格式化**:提交时会自动跑 Prettier如果出现大量无关格式 diff说明你改动触发了更大范围格式化需收敛修改范围
- **变更说明**PR/提交信息优先解释“为什么”而不是“改了什么”
## AI 变更自检清单(提交前必过)
- **范围**:是否只改了需求相关文件?是否避免“顺手重构/改命名/大面积格式化”?
- **类型**`tsconfig` 已启用 `strict``noUnused*`,新增代码是否无未使用变量/参数?
- **路由**:新增页面是否放在 `src/pages/` 并使用 `definePage`(如需 alias
- **请求**:是否遵守 Orval 规范(生成代码只读、调用集中在 `src/api/client.ts`/适配层,避免页面散落 axios/fetch
- **鉴权**:涉及登录态/鉴权时,是否确保 store 中的 token 与 API 客户端实际使用的鉴权头/拦截器/存储一致?
- **存储**localStorage key 是否只使用 `LocalStorageKey`
- **格式**:是否符合 `.prettierrc`单引号、2 空格、行宽 100并接受提交前会被 Prettier 重写?
## 常见开发模板(给 AI 直接套用)
### 新增页面(文件路由)
1. 新建 `src/pages/<biz>/index.vue`
2. 在 `script setup` 中写 `definePage({ alias: [...] })`
3. 使用 `RouterView`/组件拼装页面
### 新增接口并调用
1. 后端更新 OpenAPI确保该接口可被导出
2. 前端执行 `npm run api:update` 重新生成(产物在 `src/api/generated/**`
3. 在 `src/api/client.ts``src/api/*.ts`(适配层)封装稳定接口
4. 页面/组件只从 `src/api/client.ts` 或适配层引入调用,避免散落直连请求

3493
docs/统一开发规范.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.22",
"@nestjs/schedule": "^6.1.1",
"@nestjs/swagger": "^11.2.6",
"@nestjs/throttler": "^5.2.0",
"@prisma/client": "^5.22.0",
"@types/multer": "^2.0.0",
@ -44,6 +45,7 @@
"@nestjs/cli": "^10.3.0",
"@nestjs/schematics": "^10.1.0",
"@nestjs/testing": "^10.3.0",
"@playwright/test": "^1.58.2",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",

View File

@ -0,0 +1,97 @@
import { IsInt, IsOptional, Max, Min } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
/**
* DTO
*
*
*/
export class PageQueryDto {
@ApiProperty({
description: '当前页码(从 1 开始)',
example: 1,
required: false,
default: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt({ message: '页码必须是整数' })
@Min(1, { message: '页码最小为 1' })
page?: number = 1;
@ApiProperty({
description: '每页数量',
example: 10,
required: false,
default: 10,
minimum: 1,
maximum: 100,
})
@IsOptional()
@Type(() => Number)
@IsInt({ message: '每页数量必须是整数' })
@Min(1, { message: '每页数量最小为 1' })
@Max(100, { message: '每页数量最大为 100' })
pageSize?: number = 10;
/**
* skip Prisma
*/
getSkip(): number {
return ((this.page ?? 1) - 1) * (this.pageSize ?? 10);
}
/**
* take Prisma
*/
getTake(): number {
return this.pageSize ?? 10;
}
}
/**
* DTO
*/
export class PageSearchDto extends PageQueryDto {
@ApiProperty({
description: '搜索关键词',
required: false,
})
@IsOptional()
keyword?: string;
}
/**
* DTO
*/
export class PageWithStatusDto extends PageQueryDto {
@ApiProperty({
description: '状态筛选',
required: false,
enum: ['ACTIVE', 'INACTIVE', 'DRAFT', 'PUBLISHED'],
})
@IsOptional()
status?: string;
}
/**
* DTO
*/
export class PageWithDateRangeDto extends PageQueryDto {
@ApiProperty({
description: '开始日期YYYY-MM-DD',
required: false,
example: '2026-01-01',
})
@IsOptional()
startDate?: string;
@ApiProperty({
description: '结束日期YYYY-MM-DD',
required: false,
example: '2026-12-31',
})
@IsOptional()
endDate?: string;
}

View File

@ -0,0 +1,102 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* DTO
*
* @template T -
*/
export class ResultDto<T> {
@ApiProperty({
description: '响应状态码',
example: 200,
})
code: number;
@ApiProperty({
description: '响应消息',
example: '操作成功',
})
message: string;
@ApiProperty({
description: '响应数据',
})
data: T;
/**
*
*/
static success<T>(data: T, message = '操作成功'): ResultDto<T> {
return {
code: 200,
message,
data,
};
}
/**
*
*/
static error<T>(message: string, code = 500, data?: T): ResultDto<T> {
return {
code,
message,
data: data as T,
};
}
}
/**
* DTO
*
* @template T -
*/
export class PageResultDto<T> {
@ApiProperty({
description: '数据列表',
isArray: true,
})
items: T[];
@ApiProperty({
description: '总记录数',
example: 100,
})
total: number;
@ApiProperty({
description: '当前页码',
example: 1,
})
page: number;
@ApiProperty({
description: '每页数量',
example: 10,
})
pageSize: number;
@ApiProperty({
description: '总页数',
example: 10,
})
totalPages: number;
/**
*
*/
static create<T>(
items: T[],
total: number,
page: number,
pageSize: number,
): PageResultDto<T> {
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize) || 0,
};
}
}

View File

@ -0,0 +1,104 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Reflector } from '@nestjs/core';
import { ResultDto } from '../dto/result.dto';
/**
*
*/
interface Response<T> {
code: number;
message: string;
data: T;
}
/**
*
*
* Controller Result<T>
*
* @example
* ```typescript
* // Controller 返回
* return { name: 'test' };
*
* // 实际响应
* {
* code: 200,
* message: '操作成功',
* data: { name: 'test' }
* }
* ```
*/
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
constructor(private reflector?: Reflector) {}
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
// 检查是否跳过转换(通过装饰器 @SkipTransform
let skipTransform = false;
if (this.reflector) {
skipTransform = this.reflector.get<boolean>(
'skipTransform',
context.getHandler(),
) || false;
}
if (skipTransform) {
return next.handle();
}
return next.handle().pipe(
map((data) => {
// 如果返回的数据已经是 ResultDto 格式,直接返回
if (
data &&
typeof data === 'object' &&
'code' in data &&
'message' in data &&
'data' in data
) {
return data;
}
// 否则包装为统一格式
return ResultDto.success(data);
}),
);
}
}
/**
*
*
*
*
* @example
* ```typescript
* @Get('health')
* @SkipTransform()
* healthCheck() {
* return { status: 'ok' };
* }
* ```
*/
export const SkipTransform = () => {
return (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) => {
Reflect.defineMetadata('skipTransform', true, descriptor.value);
};
};

View File

@ -0,0 +1,77 @@
/**
* JSON
*
* JSON
*/
/**
* JSON
*
* @param value - JSON
* @param defaultValue -
* @returns
*
* @example
* ```typescript
* const tags = parseJsonField(course.gradeTags, []);
* const settings = parseJsonField(user.settings, {});
* ```
*/
export function parseJsonField<T>(
value: string | null | undefined,
defaultValue: T,
): T {
if (!value) {
return defaultValue;
}
try {
return JSON.parse(value) as T;
} catch {
return defaultValue;
}
}
/**
* JSON
*
* @param value -
* @param defaultValue -
* @returns JSON
*/
export function stringifyJsonField(
value: any,
defaultValue: string = '{}',
): string {
if (value === null || value === undefined) {
return defaultValue;
}
try {
return JSON.stringify(value);
} catch {
return defaultValue;
}
}
/**
* JSON
*
* @param value - JSON
* @returns
*/
export function parseJsonArray<T>(value: string | null | undefined): T[] {
return parseJsonField<T[]>(value, []);
}
/**
* JSON
*
* @param value - JSON
* @returns
*/
export function parseJsonObject<T extends Record<string, any>>(
value: string | null | undefined,
): T {
return parseJsonField<T>(value, {} as T);
}

View File

@ -0,0 +1,80 @@
import { PageResultDto } from '../dto/result.dto';
/**
*
*/
/**
*
*
* @param page - 1
* @param pageSize -
* @returns Prisma
*
* @example
* ```typescript
* const { skip, take } = calculatePagination(1, 10);
* // { skip: 0, take: 10 }
* ```
*/
export function calculatePagination(page: number, pageSize: number) {
const skip = (page - 1) * pageSize;
return { skip, take: pageSize };
}
/**
*
*
* @param items -
* @param total -
* @param page -
* @param pageSize -
* @returns
*
* @example
* ```typescript
* const [items, total] = await Promise.all([
* prisma.user.findMany({ skip, take }),
* prisma.user.count()
* ]);
* return createPageResponse(items, total, page, pageSize);
* ```
*/
export function createPageResponse<T>(
items: T[],
total: number,
page: number,
pageSize: number,
): PageResultDto<T> {
return PageResultDto.create(items, total, page, pageSize);
}
/**
*
*
* @param total -
* @param pageSize -
* @returns
*/
export function calculateTotalPages(total: number, pageSize: number): number {
return Math.ceil(total / pageSize) || 0;
}
/**
*
*
* @param page -
* @param pageSize -
* @param maxPageSize - 100
* @returns
*/
export function validatePagination(
page: number,
pageSize: number,
maxPageSize: number = 100,
): { page: number; pageSize: number } {
return {
page: Math.max(1, Math.floor(page)),
pageSize: Math.max(1, Math.min(maxPageSize, Math.floor(pageSize))),
};
}

View File

@ -6,6 +6,8 @@ import { join } from 'path';
import { AppModule } from './app.module';
import * as compression from 'compression';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
@ -34,6 +36,33 @@ async function bootstrap() {
// 全局异常过滤器
app.useGlobalFilters(new HttpExceptionFilter());
// 全局响应转换拦截器(统一响应格式)
app.useGlobalInterceptors(new TransformInterceptor());
// Swagger API 文档配置
const config = new DocumentBuilder()
.setTitle('幼儿阅读教学服务平台 API')
.setDescription('提供课程管理、授课记录、学生评价等功能的 API 文档')
.setVersion('2.0')
.addBearerAuth()
.addTag('auth', '认证相关接口')
.addTag('admin', '超管端接口')
.addTag('school', '学校端接口')
.addTag('teacher', '教师端接口')
.addTag('parent', '家长端接口')
.addTag('common', '公共接口')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document, {
swaggerOptions: {
persistAuthorization: true, // 持久化认证信息
tagsSorter: 'alpha', // 按字母顺序排序标签
operationsSorter: 'alpha', // 按字母顺序排序操作
},
customSiteTitle: 'API 文档',
});
// 启用压缩
// app.use(compression());
@ -65,6 +94,7 @@ async function bootstrap() {
📍 Local: http://localhost:${port} ║
📍 API: http://localhost:${port}/api/v1 ║
📍 Docs: http://localhost:${port}/api-docs ║
📍 Prisma: npx prisma studio

View File

@ -14,7 +14,65 @@ 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';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiQuery,
ApiBody,
ApiBearerAuth,
ApiProperty,
} from '@nestjs/swagger';
// ============= DTO 定义 =============
class CourseQueryDto {
@ApiProperty({ description: '页码', example: 1, required: false })
page?: number;
@ApiProperty({ description: '每页数量', example: 10, required: false })
pageSize?: number;
@ApiProperty({ description: '年级筛选', example: '小班', required: false })
grade?: string;
@ApiProperty({ description: '状态筛选', example: 'PUBLISHED', required: false })
status?: string;
@ApiProperty({ description: '关键词搜索', example: '春天', required: false })
keyword?: string;
}
class SubmitCourseDto {
@ApiProperty({ description: '是否确认版权', example: true })
copyrightConfirmed: boolean;
}
class ApproveCourseDto {
@ApiProperty({ description: '审核检查项', required: false })
checklist?: any;
@ApiProperty({ description: '审核意见', required: false })
comment?: string;
}
class RejectCourseDto {
@ApiProperty({ description: '审核检查项', required: false })
checklist?: any;
@ApiProperty({ description: '驳回原因', example: '课程内容不完整' })
comment: string;
}
class DirectPublishDto {
@ApiProperty({ description: '是否跳过验证', example: false, required: false })
skipValidation?: boolean;
}
// ============= 控制器 =============
@ApiTags('courses')
@ApiBearerAuth()
@Controller('courses')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ -22,36 +80,57 @@ export class CourseController {
constructor(private readonly courseService: CourseService) {}
@Get()
findAll(@Query() query: any) {
@ApiOperation({ summary: '获取课程包列表' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
@ApiQuery({ name: 'grade', required: false, type: String })
@ApiQuery({ name: 'status', required: false, type: String })
@ApiQuery({ name: 'keyword', required: false, type: String })
findAll(@Query() query: CourseQueryDto) {
return this.courseService.findAll(query);
}
@Get('review')
getReviewList(@Query() query: any) {
@ApiOperation({ summary: '获取待审核课程列表' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
@ApiQuery({ name: 'grade', required: false, type: String })
@ApiQuery({ name: 'status', required: false, type: String })
getReviewList(@Query() query: CourseQueryDto) {
return this.courseService.getReviewList(query);
}
@Get(':id')
@ApiOperation({ summary: '获取课程包详情' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
findOne(@Param('id') id: string) {
return this.courseService.findOne(+id);
}
@Get(':id/stats')
@ApiOperation({ summary: '获取课程包统计数据' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
getStats(@Param('id') id: string) {
return this.courseService.getStats(+id);
}
@Get(':id/validate')
@ApiOperation({ summary: '验证课程包完整性' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
validate(@Param('id') id: string) {
return this.courseService.validate(+id);
}
@Get(':id/versions')
@ApiOperation({ summary: '获取课程包版本历史' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
getVersionHistory(@Param('id') id: string) {
return this.courseService.getVersionHistory(+id);
}
@Post()
@ApiOperation({ summary: '创建课程包' })
@ApiBody({ type: Object, description: '课程包数据' })
create(@Body() createCourseDto: any, @Request() req: any) {
return this.courseService.create({
...createCourseDto,
@ -60,11 +139,16 @@ export class CourseController {
}
@Put(':id')
@ApiOperation({ summary: '更新课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
@ApiBody({ type: Object, description: '课程包数据' })
update(@Param('id') id: string, @Body() updateCourseDto: any) {
return this.courseService.update(+id, updateCourseDto);
}
@Delete(':id')
@ApiOperation({ summary: '删除课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
remove(@Param('id') id: string) {
return this.courseService.remove(+id);
}
@ -74,7 +158,10 @@ export class CourseController {
* POST /api/v1/courses/:id/submit
*/
@Post(':id/submit')
submit(@Param('id') id: string, @Body() body: { copyrightConfirmed: boolean }, @Request() req: any) {
@ApiOperation({ summary: '提交课程包审核' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
@ApiBody({ type: SubmitCourseDto })
submit(@Param('id') id: string, @Body() body: SubmitCourseDto, @Request() req: any) {
const userId = req.user?.userId || 0;
return this.courseService.submit(+id, userId, body.copyrightConfirmed);
}
@ -84,6 +171,8 @@ export class CourseController {
* POST /api/v1/courses/:id/withdraw
*/
@Post(':id/withdraw')
@ApiOperation({ summary: '撤销课程包审核申请' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
withdraw(@Param('id') id: string, @Request() req: any) {
const userId = req.user?.userId || 0;
return this.courseService.withdraw(+id, userId);
@ -94,9 +183,12 @@ export class CourseController {
* POST /api/v1/courses/:id/approve
*/
@Post(':id/approve')
@ApiOperation({ summary: '审核通过课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
@ApiBody({ type: ApproveCourseDto })
approve(
@Param('id') id: string,
@Body() body: { checklist?: any; comment?: string },
@Body() body: ApproveCourseDto,
@Request() req: any,
) {
const reviewerId = req.user?.userId || 0;
@ -108,9 +200,12 @@ export class CourseController {
* POST /api/v1/courses/:id/reject
*/
@Post(':id/reject')
@ApiOperation({ summary: '审核驳回课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
@ApiBody({ type: RejectCourseDto })
reject(
@Param('id') id: string,
@Body() body: { checklist?: any; comment: string },
@Body() body: RejectCourseDto,
@Request() req: any,
) {
const reviewerId = req.user?.userId || 0;
@ -122,10 +217,13 @@ export class CourseController {
* POST /api/v1/courses/:id/direct-publish
*/
@Post(':id/direct-publish')
@ApiOperation({ summary: '直接发布课程包(超管专用)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
@ApiBody({ type: DirectPublishDto })
@Roles('admin')
directPublish(
@Param('id') id: string,
@Body() body: { skipValidation?: boolean },
@Body() body: DirectPublishDto,
@Request() req: any,
) {
const userId = req.user?.userId || 0;
@ -137,6 +235,8 @@ export class CourseController {
* POST /api/v1/courses/:id/publish
*/
@Post(':id/publish')
@ApiOperation({ summary: '发布课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
publish(@Param('id') id: string) {
return this.courseService.publish(+id);
}
@ -146,6 +246,8 @@ export class CourseController {
* POST /api/v1/courses/:id/unpublish
*/
@Post(':id/unpublish')
@ApiOperation({ summary: '下架课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
unpublish(@Param('id') id: string) {
return this.courseService.unpublish(+id);
}
@ -155,6 +257,8 @@ export class CourseController {
* POST /api/v1/courses/:id/republish
*/
@Post(':id/republish')
@ApiOperation({ summary: '重新发布课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: String })
republish(@Param('id') id: string) {
return this.courseService.republish(+id);
}

View File

@ -14,8 +14,105 @@ 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';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiQuery,
ApiBody,
ApiBearerAuth,
ApiProperty,
} from '@nestjs/swagger';
// 教师端授课控制器
// ============= DTO 定义 =============
class LessonQueryDto {
@ApiProperty({ description: '页码', example: 1, required: false })
page?: number;
@ApiProperty({ description: '每页数量', example: 10, required: false })
pageSize?: number;
@ApiProperty({ description: '课程ID', required: false })
courseId?: number;
@ApiProperty({ description: '状态筛选', required: false })
status?: string;
}
class StudentRecordDto {
@ApiProperty({ description: '专注度', required: false })
focus?: number;
@ApiProperty({ description: '参与度', required: false })
participation?: number;
@ApiProperty({ description: '兴趣度', required: false })
interest?: number;
@ApiProperty({ description: '理解度', required: false })
understanding?: number;
@ApiProperty({ description: '备注', required: false })
notes?: string;
}
class BatchStudentRecordsDto {
@ApiProperty({ description: '学生记录列表', type: [Object] })
records: Array<{
studentId: number;
focus?: number;
participation?: number;
interest?: number;
understanding?: number;
notes?: string;
}>;
}
class LessonProgressDto {
@ApiProperty({ description: '课程ID列表', required: false })
lessonIds?: number[];
@ApiProperty({ description: '已完成课程ID列表', required: false })
completedLessonIds?: number[];
@ApiProperty({ description: '当前课程ID', required: false })
currentLessonId?: number;
@ApiProperty({ description: '当前环节ID', required: false })
currentStepId?: number;
@ApiProperty({ description: '进度数据', required: false })
progressData?: any;
}
class LessonFeedbackDto {
@ApiProperty({ description: '设计质量评分', required: false })
designQuality?: number;
@ApiProperty({ description: '参与度评分', required: false })
participation?: number;
@ApiProperty({ description: '目标达成度评分', required: false })
goalAchievement?: number;
@ApiProperty({ description: '环节反馈', required: false })
stepFeedbacks?: any;
@ApiProperty({ description: '优点', required: false })
pros?: string;
@ApiProperty({ description: '建议', required: false })
suggestions?: string;
@ApiProperty({ description: '完成的活动', required: false })
activitiesDone?: any;
}
// ============= 教师端授课控制器 =============
@ApiTags('teacher/lessons')
@ApiBearerAuth()
@Controller('teacher/lessons')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('teacher')
@ -23,55 +120,80 @@ export class LessonController {
constructor(private readonly lessonService: LessonService) {}
@Get()
findAll(@Request() req: any, @Query() query: any) {
@ApiOperation({ summary: '获取教师授课记录列表' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
@ApiQuery({ name: 'courseId', required: false, type: Number })
@ApiQuery({ name: 'status', required: false, type: String })
findAll(@Request() req: any, @Query() query: LessonQueryDto) {
return this.lessonService.findByTeacher(req.user.userId, query);
}
@Get(':id')
@ApiOperation({ summary: '获取授课记录详情' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
findOne(@Request() req: any, @Param('id') id: string) {
return this.lessonService.findOne(+id, req.user.userId);
}
@Post()
@ApiOperation({ summary: '创建授课记录' })
@ApiBody({ type: CreateLessonDto })
create(@Request() req: any, @Body() dto: CreateLessonDto) {
return this.lessonService.create(req.user.userId, req.user.tenantId, dto);
}
@Post(':id/start')
@ApiOperation({ summary: '开始授课' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
start(@Request() req: any, @Param('id') id: string) {
return this.lessonService.start(+id, req.user.userId);
}
@Post(':id/finish')
@ApiOperation({ summary: '完成授课' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
@ApiBody({ type: FinishLessonDto })
finish(@Request() req: any, @Param('id') id: string, @Body() dto: FinishLessonDto) {
return this.lessonService.finish(+id, req.user.userId, dto);
}
@Post(':id/cancel')
@ApiOperation({ summary: '取消授课' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
cancel(@Request() req: any, @Param('id') id: string) {
return this.lessonService.cancel(+id, req.user.userId);
}
@Post(':id/students/:studentId/record')
@ApiOperation({ summary: '保存学生课堂记录' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
@ApiParam({ name: 'studentId', description: '学生 ID', type: String })
@ApiBody({ type: StudentRecordDto })
saveStudentRecord(
@Request() req: any,
@Param('id') id: string,
@Param('studentId') studentId: string,
@Body() data: any
@Body() data: StudentRecordDto
) {
return this.lessonService.saveStudentRecord(+id, req.user.userId, +studentId, data);
}
@Get(':id/student-records')
@ApiOperation({ summary: '获取授课记录的学生数据' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
getStudentRecords(@Request() req: any, @Param('id') id: string) {
return this.lessonService.getStudentRecords(+id, req.user.userId);
}
@Post(':id/student-records/batch')
@ApiOperation({ summary: '批量保存学生课堂记录' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
@ApiBody({ type: BatchStudentRecordsDto })
batchSaveStudentRecords(
@Request() req: any,
@Param('id') id: string,
@Body() data: { records: Array<{ studentId: number; focus?: number; participation?: number; interest?: number; understanding?: number; notes?: string }> }
@Body() data: BatchStudentRecordsDto
) {
return this.lessonService.batchSaveStudentRecords(+id, req.user.userId, data.records);
}
@ -79,21 +201,50 @@ export class LessonController {
// ==================== 课程反馈 ====================
@Post(':id/feedback')
@ApiOperation({ summary: '提交课程反馈' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
@ApiBody({ type: LessonFeedbackDto })
submitFeedback(
@Request() req: any,
@Param('id') id: string,
@Body() data: any
@Body() data: LessonFeedbackDto
) {
return this.lessonService.submitFeedback(+id, req.user.userId, data);
}
@Get(':id/feedback')
@ApiOperation({ summary: '获取课程反馈' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
getFeedback(@Request() req: any, @Param('id') id: string) {
return this.lessonService.getFeedback(+id, req.user.userId);
}
// ==================== 课程进度追踪 ====================
@Post(':id/progress')
@ApiOperation({ summary: '保存课程进度' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
@ApiBody({ type: LessonProgressDto })
saveProgress(
@Request() req: any,
@Param('id') id: string,
@Body() data: LessonProgressDto
) {
return this.lessonService.saveProgress(+id, req.user.userId, data);
}
// 教师端反馈控制器
@Get(':id/progress')
@ApiOperation({ summary: '获取课程进度' })
@ApiParam({ name: 'id', description: '授课记录 ID', type: String })
getProgress(@Request() req: any, @Param('id') id: string) {
return this.lessonService.getProgress(+id, req.user.userId);
}
}
// ============= 教师端反馈控制器 =============
@ApiTags('teacher/feedbacks')
@ApiBearerAuth()
@Controller('teacher/feedbacks')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('teacher')
@ -101,7 +252,10 @@ export class TeacherFeedbackController {
constructor(private readonly lessonService: LessonService) {}
@Get()
findAll(@Request() req: any, @Query() query: any) {
@ApiOperation({ summary: '获取教师端反馈列表' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
findAll(@Request() req: any, @Query() query: LessonQueryDto) {
return this.lessonService.getFeedbacksByTenant(req.user.tenantId, {
...query,
teacherId: req.user.userId,
@ -109,12 +263,16 @@ export class TeacherFeedbackController {
}
@Get('stats')
@ApiOperation({ summary: '获取教师端反馈统计' })
getStats(@Request() req: any) {
return this.lessonService.getTeacherFeedbackStats(req.user.userId);
}
}
// 学校端反馈控制器
// ============= 学校端反馈控制器 =============
@ApiTags('school/feedbacks')
@ApiBearerAuth()
@Controller('school/feedbacks')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('school')
@ -122,11 +280,15 @@ export class SchoolFeedbackController {
constructor(private readonly lessonService: LessonService) {}
@Get()
findAll(@Request() req: any, @Query() query: any) {
@ApiOperation({ summary: '获取学校端反馈列表' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
findAll(@Request() req: any, @Query() query: LessonQueryDto) {
return this.lessonService.getFeedbacksByTenant(req.user.tenantId, query);
}
@Get('stats')
@ApiOperation({ summary: '获取学校端反馈统计' })
getStats(@Request() req: any) {
return this.lessonService.getFeedbackStats(req.user.tenantId);
}

View File

@ -14,8 +14,99 @@ 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';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiBody,
ApiBearerAuth,
ApiProperty,
} from '@nestjs/swagger';
// 学校端校本课程包控制器
// ============= DTO 定义 =============
class CreateSchoolCourseDto {
@ApiProperty({ description: '源课程ID' })
sourceCourseId: number;
@ApiProperty({ description: '课程名称' })
name: string;
@ApiProperty({ description: '课程描述', required: false })
description?: string;
@ApiProperty({ description: '修改说明', required: false })
changesSummary?: string;
}
class UpdateSchoolCourseDto {
@ApiProperty({ description: '课程名称', required: false })
name?: string;
@ApiProperty({ description: '课程描述', required: false })
description?: string;
@ApiProperty({ description: '修改说明', required: false })
changesSummary?: string;
@ApiProperty({ description: '状态', required: false })
status?: string;
}
class UpdateLessonDto {
@ApiProperty({ description: '教学目标', required: false })
objectives?: string;
@ApiProperty({ description: '课前准备', required: false })
preparation?: string;
@ApiProperty({ description: '课后延伸', required: false })
extension?: string;
@ApiProperty({ description: '课后反思', required: false })
reflection?: string;
@ApiProperty({ description: '修改备注', required: false })
changeNote?: string;
@ApiProperty({ description: '环节数据', required: false })
stepsData?: string;
}
class CreateReservationDto {
@ApiProperty({ description: '教师ID' })
teacherId: number;
@ApiProperty({ description: '班级ID' })
classId: number;
@ApiProperty({ description: '预约日期', example: '2026-03-15' })
scheduledDate: string;
@ApiProperty({ description: '预约时间', required: false })
scheduledTime?: string;
@ApiProperty({ description: '备注', required: false })
note?: string;
}
class CreateFromSourceDto {
@ApiProperty({ description: '源课程ID' })
sourceCourseId: number;
@ApiProperty({ description: '保存位置', enum: ['PERSONAL', 'SCHOOL'] })
saveLocation: 'PERSONAL' | 'SCHOOL';
}
class ReviewDto {
@ApiProperty({ description: '审核意见', required: false })
comment?: string;
}
// ============= 学校端校本课程包控制器 =============
@ApiTags('school/school-courses')
@ApiBearerAuth()
@Controller('school/school-courses')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('school')
@ -23,31 +114,28 @@ export class SchoolCourseController {
constructor(private readonly schoolCourseService: SchoolCourseService) {}
@Get()
async findAll(@Request() req: any) {
@ApiOperation({ summary: '获取学校端校本课程包列表' })
findAll(@Request() req: any) {
return this.schoolCourseService.findAll(req.user.tenantId);
}
@Get('source-courses')
async getSourceCourses(@Request() req: any) {
@ApiOperation({ summary: '获取可导入的源课程列表' })
getSourceCourses(@Request() req: any) {
return this.schoolCourseService.getSourceCourses(req.user.tenantId);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
@ApiOperation({ summary: '获取校本课程包详情' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
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;
},
) {
@ApiOperation({ summary: '创建校本课程包' })
@ApiBody({ type: CreateSchoolCourseDto })
create(@Request() req: any, @Body() body: CreateSchoolCourseDto) {
return this.schoolCourseService.create(
req.user.tenantId,
body.sourceCourseId,
@ -57,28 +145,29 @@ export class SchoolCourseController {
}
@Put(':id')
async update(
@ApiOperation({ summary: '更新校本课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiBody({ type: UpdateSchoolCourseDto })
update(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body()
body: {
name?: string;
description?: string;
changesSummary?: string;
status?: string;
},
@Body() body: UpdateSchoolCourseDto,
) {
return this.schoolCourseService.update(id, req.user.tenantId, body);
}
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
@ApiOperation({ summary: '删除校本课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
return this.schoolCourseService.delete(id, req.user.tenantId);
}
// 课程管理
@Get(':id/lessons')
async findLessons(
@ApiOperation({ summary: '获取校本课程包的课时列表' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
findLessons(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
) {
@ -86,26 +175,24 @@ export class SchoolCourseController {
}
@Put(':id/lessons/:lessonId')
async updateLesson(
@ApiOperation({ summary: '更新校本课程包的课时' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiParam({ name: 'lessonId', description: '课时 ID', type: Number })
@ApiBody({ type: UpdateLessonDto })
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;
},
@Body() body: UpdateLessonDto,
) {
return this.schoolCourseService.updateLesson(id, lessonId, req.user.tenantId, body);
}
// 预约管理
@Get(':id/reservations')
async findReservations(
@ApiOperation({ summary: '获取课程包的预约列表' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
findReservations(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
) {
@ -113,31 +200,94 @@ export class SchoolCourseController {
}
@Post(':id/reservations')
async createReservation(
@ApiOperation({ summary: '创建课程预约' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiBody({ type: CreateReservationDto })
createReservation(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body()
body: {
teacherId: number;
classId: number;
scheduledDate: string;
scheduledTime?: string;
note?: string;
},
@Body() body: CreateReservationDto,
) {
return this.schoolCourseService.createReservation(id, req.user.tenantId, body);
}
@Post('reservations/:reservationId/cancel')
async cancelReservation(
@ApiOperation({ summary: '取消课程预约' })
@ApiParam({ name: 'reservationId', description: '预约 ID', type: Number })
cancelReservation(
@Param('reservationId', ParseIntPipe) reservationId: number,
@Request() req: any,
) {
return this.schoolCourseService.cancelReservation(reservationId, req.user.tenantId);
}
// ==================== 完整数据API ====================
@Post('from-source')
@ApiOperation({ summary: '从源课程创建校本课程包' })
@ApiBody({ type: CreateFromSourceDto })
createFromSource(@Request() req: any, @Body() body: CreateFromSourceDto) {
return this.schoolCourseService.createFromSource(
req.user.tenantId,
body.sourceCourseId,
req.user.id,
body.saveLocation,
);
}
// 教师端校本课程包控制器
@Get(':id/full')
@ApiOperation({ summary: '获取校本课程包完整数据' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
getFullDetail(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
) {
return this.schoolCourseService.getFullDetail(id, req.user.tenantId);
}
@Put(':id/full')
@ApiOperation({ summary: '更新校本课程包完整数据' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiBody({ type: Object, description: '完整课程包数据' })
updateFull(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body() body: any,
) {
return this.schoolCourseService.updateFull(id, req.user.tenantId, body);
}
// ==================== 审核API ====================
@Post(':id/approve')
@ApiOperation({ summary: '审核通过校本课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiBody({ type: ReviewDto })
approve(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body() body: ReviewDto,
) {
return this.schoolCourseService.approve(id, req.user.tenantId, req.user.id, body.comment);
}
@Post(':id/reject')
@ApiOperation({ summary: '审核驳回校本课程包' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiBody({ type: ReviewDto })
reject(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body() body: ReviewDto,
) {
return this.schoolCourseService.reject(id, req.user.tenantId, req.user.id, body.comment);
}
}
// ============= 教师端校本课程包控制器 =============
@ApiTags('teacher/school-courses')
@ApiBearerAuth()
@Controller('teacher/school-courses')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('teacher')
@ -145,31 +295,28 @@ export class TeacherSchoolCourseController {
constructor(private readonly schoolCourseService: SchoolCourseService) {}
@Get()
async findAll(@Request() req: any) {
@ApiOperation({ summary: '获取教师端校本课程包列表' })
findAll(@Request() req: any) {
return this.schoolCourseService.findAll(req.user.tenantId, req.user.id);
}
@Get('source-courses')
async getSourceCourses(@Request() req: any) {
@ApiOperation({ summary: '获取可导入的源课程列表(教师端)' })
getSourceCourses(@Request() req: any) {
return this.schoolCourseService.getSourceCourses(req.user.tenantId);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
@ApiOperation({ summary: '获取校本课程包详情(教师端)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
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;
},
) {
@ApiOperation({ summary: '创建校本课程包(教师端)' })
@ApiBody({ type: CreateSchoolCourseDto })
create(@Request() req: any, @Body() body: CreateSchoolCourseDto) {
return this.schoolCourseService.create(
req.user.tenantId,
body.sourceCourseId,
@ -179,28 +326,29 @@ export class TeacherSchoolCourseController {
}
@Put(':id')
async update(
@ApiOperation({ summary: '更新校本课程包(教师端)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiBody({ type: UpdateSchoolCourseDto })
update(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body()
body: {
name?: string;
description?: string;
changesSummary?: string;
status?: string;
},
@Body() body: UpdateSchoolCourseDto,
) {
return this.schoolCourseService.update(id, req.user.tenantId, body);
}
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
@ApiOperation({ summary: '删除校本课程包(教师端)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
return this.schoolCourseService.delete(id, req.user.tenantId);
}
// 课程管理
@Get(':id/lessons')
async findLessons(
@ApiOperation({ summary: '获取校本课程包的课时列表(教师端)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
findLessons(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
) {
@ -208,20 +356,52 @@ export class TeacherSchoolCourseController {
}
@Put(':id/lessons/:lessonId')
async updateLesson(
@ApiOperation({ summary: '更新校本课程包的课时(教师端)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiParam({ name: 'lessonId', description: '课时 ID', type: Number })
@ApiBody({ type: UpdateLessonDto })
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;
},
@Body() body: UpdateLessonDto,
) {
return this.schoolCourseService.updateLesson(id, lessonId, req.user.tenantId, body);
}
// ==================== 完整数据API ====================
@Post('from-source')
@ApiOperation({ summary: '从源课程创建校本课程包(教师端)' })
@ApiBody({ type: CreateFromSourceDto })
createFromSource(@Request() req: any, @Body() body: CreateFromSourceDto) {
return this.schoolCourseService.createFromSource(
req.user.tenantId,
body.sourceCourseId,
req.user.id,
body.saveLocation || 'PERSONAL',
);
}
@Get(':id/full')
@ApiOperation({ summary: '获取校本课程包完整数据(教师端)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
getFullDetail(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
) {
return this.schoolCourseService.getFullDetail(id, req.user.tenantId);
}
@Put(':id/full')
@ApiOperation({ summary: '更新校本课程包完整数据(教师端)' })
@ApiParam({ name: 'id', description: '课程包 ID', type: Number })
@ApiBody({ type: Object, description: '完整课程包数据' })
updateFull(
@Param('id', ParseIntPipe) id: number,
@Request() req: any,
@Body() body: any,
) {
return this.schoolCourseService.updateFull(id, req.user.tenantId, body);
}
}

View File

@ -3,7 +3,83 @@ 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';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiQuery,
ApiBody,
ApiBearerAuth,
ApiProperty,
} from '@nestjs/swagger';
// ============= DTO 定义 =============
class TeacherCourseQueryDto {
@ApiProperty({ description: '页码', example: 1, required: false })
page?: number;
@ApiProperty({ description: '每页数量', example: 10, required: false })
pageSize?: number;
@ApiProperty({ description: '年级筛选', example: '小班', required: false })
grade?: string;
@ApiProperty({ description: '关键词搜索', required: false })
keyword?: string;
}
class StudentQueryDto {
@ApiProperty({ description: '页码', example: 1, required: false })
page?: number;
@ApiProperty({ description: '每页数量', example: 10, required: false })
pageSize?: number;
@ApiProperty({ description: '关键词搜索', required: false })
keyword?: string;
}
class ScheduleQueryDto {
@ApiProperty({ description: '开始日期', example: '2026-03-01', required: false })
startDate?: string;
@ApiProperty({ description: '结束日期', example: '2026-03-31', required: false })
endDate?: string;
}
class CreateScheduleDto {
@ApiProperty({ description: '课程ID' })
courseId: number;
@ApiProperty({ description: '班级ID' })
classId: number;
@ApiProperty({ description: '上课日期', example: '2026-03-15' })
lessonDate: string;
@ApiProperty({ description: '上课时间', example: '09:00' })
lessonTime: string;
@ApiProperty({ description: '备注', required: false })
notes?: string;
}
class UpdateScheduleDto {
@ApiProperty({ description: '上课日期', example: '2026-03-15', required: false })
lessonDate?: string;
@ApiProperty({ description: '上课时间', example: '09:00', required: false })
lessonTime?: string;
@ApiProperty({ description: '备注', required: false })
notes?: string;
}
// ============= 教师端控制器 =============
@ApiTags('teacher')
@ApiBearerAuth()
@Controller('teacher')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('teacher')
@ -13,26 +89,32 @@ export class TeacherCourseController {
// ==================== 首页仪表板 ====================
@Get('dashboard')
@ApiOperation({ summary: '获取教师端首页数据' })
getDashboard(@Request() req: any) {
return this.teacherCourseService.getDashboard(req.user.userId, req.user.tenantId);
}
@Get('dashboard/today')
@ApiOperation({ summary: '获取今日授课安排' })
getTodayLessons(@Request() req: any) {
return this.teacherCourseService.getTodayLessons(req.user.userId, req.user.tenantId);
}
@Get('dashboard/recommend')
@ApiOperation({ summary: '获取推荐课程' })
getRecommendedCourses(@Request() req: any) {
return this.teacherCourseService.getRecommendedCourses(req.user.tenantId);
}
@Get('dashboard/weekly')
@ApiOperation({ summary: '获取本周统计数据' })
getWeeklyStats(@Request() req: any) {
return this.teacherCourseService.getWeeklyStats(req.user.userId);
}
@Get('dashboard/lesson-trend')
@ApiOperation({ summary: '获取授课趋势数据' })
@ApiQuery({ name: 'months', description: '月份数', required: false, type: Number })
getLessonTrend(@Request() req: any, @Query('months') months?: string) {
return this.teacherCourseService.getTeacherLessonTrend(
req.user.userId,
@ -41,6 +123,7 @@ export class TeacherCourseController {
}
@Get('dashboard/course-usage')
@ApiOperation({ summary: '获取课程使用情况' })
getCourseUsage(@Request() req: any) {
return this.teacherCourseService.getTeacherCourseUsage(req.user.userId);
}
@ -48,16 +131,24 @@ export class TeacherCourseController {
// ==================== 课程管理 ====================
@Get('courses')
findAll(@Request() req: any, @Query() query: any) {
@ApiOperation({ summary: '获取教师可用课程列表' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
@ApiQuery({ name: 'grade', required: false, type: String })
@ApiQuery({ name: 'keyword', required: false, type: String })
findAll(@Request() req: any, @Query() query: TeacherCourseQueryDto) {
return this.teacherCourseService.findAll(req.user.userId, req.user.tenantId, query);
}
@Get('courses/classes')
@ApiOperation({ summary: '获取教师班级列表' })
getClasses(@Request() req: any) {
return this.teacherCourseService.getTeacherClasses(req.user.userId);
}
@Get('courses/:id')
@ApiOperation({ summary: '获取课程详情' })
@ApiParam({ name: 'id', description: '课程 ID', type: String })
findOne(@Request() req: any, @Param('id') id: string) {
return this.teacherCourseService.findOne(+id, req.user.userId, req.user.tenantId);
}
@ -65,20 +156,30 @@ export class TeacherCourseController {
// ==================== 班级学生管理 ====================
@Get('students')
getAllStudents(@Request() req: any, @Query() query: any) {
@ApiOperation({ summary: '获取教师所有学生列表' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
@ApiQuery({ name: 'keyword', required: false, type: String })
getAllStudents(@Request() req: any, @Query() query: StudentQueryDto) {
return this.teacherCourseService.getAllTeacherStudents(req.user.userId, query);
}
@Get('classes/:id/students')
@ApiOperation({ summary: '获取班级学生列表' })
@ApiParam({ name: 'id', description: '班级 ID', type: String })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'pageSize', required: false, type: Number })
getClassStudents(
@Request() req: any,
@Param('id') id: string,
@Query() query: any,
@Query() query: StudentQueryDto,
) {
return this.teacherCourseService.getClassStudents(req.user.userId, +id, query);
}
@Get('classes/:id/teachers')
@ApiOperation({ summary: '获取班级教师列表' })
@ApiParam({ name: 'id', description: '班级 ID', type: String })
getClassTeachers(
@Request() req: any,
@Param('id') id: string,
@ -89,11 +190,17 @@ export class TeacherCourseController {
// ==================== 排课管理 ====================
@Get('schedules')
getTeacherSchedules(@Request() req: any, @Query() query: any) {
@ApiOperation({ summary: '获取教师排课列表' })
@ApiQuery({ name: 'startDate', required: false, type: String })
@ApiQuery({ name: 'endDate', required: false, type: String })
getTeacherSchedules(@Request() req: any, @Query() query: ScheduleQueryDto) {
return this.teacherCourseService.getTeacherSchedules(req.user.userId, query);
}
@Get('schedules/timetable')
@ApiOperation({ summary: '获取教师课程表' })
@ApiQuery({ name: 'startDate', description: '开始日期', type: String })
@ApiQuery({ name: 'endDate', description: '结束日期', type: String })
getTeacherTimetable(
@Request() req: any,
@Query('startDate') startDate: string,
@ -103,25 +210,33 @@ export class TeacherCourseController {
}
@Get('schedules/today')
@ApiOperation({ summary: '获取今日排课' })
getTodaySchedules(@Request() req: any) {
return this.teacherCourseService.getTodaySchedules(req.user.userId);
}
@Post('schedules')
createTeacherSchedule(@Request() req: any, @Body() dto: any) {
@ApiOperation({ summary: '创建排课' })
@ApiBody({ type: CreateScheduleDto })
createTeacherSchedule(@Request() req: any, @Body() dto: CreateScheduleDto) {
return this.teacherCourseService.createTeacherSchedule(req.user.userId, req.user.tenantId, dto);
}
@Put('schedules/:id')
@ApiOperation({ summary: '更新排课' })
@ApiParam({ name: 'id', description: '排课 ID', type: String })
@ApiBody({ type: UpdateScheduleDto })
updateTeacherSchedule(
@Request() req: any,
@Param('id') id: string,
@Body() dto: any,
@Body() dto: UpdateScheduleDto,
) {
return this.teacherCourseService.updateTeacherSchedule(req.user.userId, +id, dto);
}
@Delete('schedules/:id')
@ApiOperation({ summary: '取消排课' })
@ApiParam({ name: 'id', description: '排课 ID', type: String })
cancelTeacherSchedule(@Request() req: any, @Param('id') id: string) {
return this.teacherCourseService.cancelTeacherSchedule(req.user.userId, +id);
}

View File

@ -8,37 +8,77 @@ import {
Min,
Max,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class TenantQueryDto {
@ApiProperty({
description: '当前页码(从 1 开始)',
example: 1,
required: false,
default: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@ApiProperty({
description: '每页数量',
example: 10,
required: false,
default: 10,
minimum: 1,
maximum: 100,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
pageSize?: number = 10;
@ApiProperty({
description: '搜索关键词(学校名称或登录账号)',
required: false,
})
@IsOptional()
@IsString()
keyword?: string;
@ApiProperty({
description: '状态筛选',
required: false,
enum: ['ACTIVE', 'INACTIVE', 'SUSPENDED'],
})
@IsOptional()
@IsString()
status?: string;
@ApiProperty({
description: '套餐类型筛选',
required: false,
enum: ['STANDARD', 'PREMIUM', 'TRIAL'],
})
@IsOptional()
@IsString()
packageType?: string;
}
export class CreateTenantDto {
@ApiProperty({
description: '学校名称',
example: '阳光幼儿园',
})
@IsString()
@IsNotEmpty({ message: '学校名称不能为空' })
name: string;
@ApiProperty({
description: '登录账号必须以字母开头4-20位字母、数字或下划线',
example: 'kindergarten123',
})
@IsString()
@IsNotEmpty({ message: '登录账号不能为空' })
@Matches(/^[a-zA-Z][a-zA-Z0-9_]{3,19}$/, {
@ -46,6 +86,11 @@ export class CreateTenantDto {
})
loginAccount: string;
@ApiProperty({
description: '初始密码至少6位需包含字母和数字',
example: 'abc123',
required: false,
})
@IsOptional()
@IsString()
@Matches(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{6,20}$/, {
@ -53,105 +98,234 @@ export class CreateTenantDto {
})
password?: string;
@ApiProperty({
description: '学校地址',
example: '北京市朝阳区',
required: false,
})
@IsOptional()
@IsString()
address?: string;
@ApiProperty({
description: '联系人姓名',
example: '张老师',
required: false,
})
@IsOptional()
@IsString()
contactPerson?: string;
@ApiProperty({
description: '联系人手机号',
example: '13800138000',
required: false,
})
@IsOptional()
@IsString()
@Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' })
contactPhone?: string;
@ApiProperty({
description: '套餐类型',
example: 'STANDARD',
required: false,
default: 'STANDARD',
enum: ['STANDARD', 'PREMIUM', 'TRIAL'],
})
@IsOptional()
@IsString()
packageType?: string = 'STANDARD';
@ApiProperty({
description: '教师配额',
example: 20,
required: false,
default: 20,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
teacherQuota?: number = 20;
@ApiProperty({
description: '学生配额',
example: 200,
required: false,
default: 200,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
studentQuota?: number = 200;
@ApiProperty({
description: '服务开始日期YYYY-MM-DD',
example: '2026-01-01',
required: false,
})
@IsOptional()
@IsDateString()
startDate?: string;
@ApiProperty({
description: '服务到期日期YYYY-MM-DD',
example: '2026-12-31',
required: false,
})
@IsOptional()
@IsDateString()
expireDate?: string;
}
export class UpdateTenantDto {
@ApiProperty({
description: '学校名称',
example: '阳光幼儿园',
required: false,
})
@IsOptional()
@IsString()
@IsNotEmpty({ message: '学校名称不能为空' })
name?: string;
@ApiProperty({
description: '学校地址',
example: '北京市朝阳区',
required: false,
})
@IsOptional()
@IsString()
address?: string;
@ApiProperty({
description: '联系人姓名',
example: '张老师',
required: false,
})
@IsOptional()
@IsString()
contactPerson?: string;
@ApiProperty({
description: '联系人手机号',
example: '13800138000',
required: false,
})
@IsOptional()
@IsString()
@Matches(/^1[3-9]\d{9}$/, { message: '请输入正确的手机号' })
contactPhone?: string;
@ApiProperty({
description: '套餐类型',
example: 'STANDARD',
required: false,
enum: ['STANDARD', 'PREMIUM', 'TRIAL'],
})
@IsOptional()
@IsString()
packageType?: string;
@ApiProperty({
description: '教师配额',
example: 20,
required: false,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
teacherQuota?: number;
@ApiProperty({
description: '学生配额',
example: 200,
required: false,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
studentQuota?: number;
@ApiProperty({
description: '服务开始日期YYYY-MM-DD',
example: '2026-01-01',
required: false,
})
@IsOptional()
@IsDateString()
startDate?: string;
@ApiProperty({
description: '服务到期日期YYYY-MM-DD',
example: '2026-12-31',
required: false,
})
@IsOptional()
@IsDateString()
expireDate?: string;
@ApiProperty({
description: '状态',
example: 'ACTIVE',
required: false,
enum: ['ACTIVE', 'INACTIVE', 'SUSPENDED'],
})
@IsOptional()
@IsString()
status?: string;
}
export class UpdateTenantQuotaDto {
@ApiProperty({
description: '套餐类型',
example: 'PREMIUM',
required: false,
enum: ['STANDARD', 'PREMIUM', 'TRIAL'],
})
@IsOptional()
@IsString()
packageType?: string;
@ApiProperty({
description: '教师配额',
example: 30,
required: false,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
teacherQuota?: number;
@ApiProperty({
description: '学生配额',
example: 300,
required: false,
minimum: 1,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
studentQuota?: number;
}
export class UpdateTenantStatusDto {
@ApiProperty({
description: '状态',
example: 'ACTIVE',
enum: ['ACTIVE', 'INACTIVE', 'SUSPENDED'],
})
@IsString()
@IsNotEmpty({ message: '状态不能为空' })
status: string;

View File

@ -20,7 +20,16 @@ import {
UpdateTenantQuotaDto,
UpdateTenantStatusDto,
} from './dto/tenant.dto';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { ResultDto } from '../../common/dto/result.dto';
@ApiTags('admin/tenants')
@ApiBearerAuth()
@Controller('admin/tenants')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ -28,46 +37,100 @@ export class TenantController {
constructor(private readonly tenantService: TenantService) {}
@Get()
@ApiOperation({ summary: '获取租户列表' })
@ApiResponse({
status: 200,
description: '获取成功',
type: ResultDto,
})
findAll(@Query() query: TenantQueryDto) {
return this.tenantService.findAllPaginated(query);
}
@Get('stats')
@ApiOperation({ summary: '获取租户统计信息' })
@ApiResponse({
status: 200,
description: '获取成功',
type: ResultDto,
})
getStats() {
return this.tenantService.getStats();
}
@Get(':id')
@ApiOperation({ summary: '获取租户详情' })
@ApiResponse({
status: 200,
description: '获取成功',
type: ResultDto,
})
findOne(@Param('id') id: string) {
return this.tenantService.findOne(+id);
}
@Post()
@ApiOperation({ summary: '创建租户' })
@ApiResponse({
status: 200,
description: '创建成功',
type: ResultDto,
})
create(@Body() createTenantDto: CreateTenantDto) {
return this.tenantService.create(createTenantDto);
}
@Put(':id')
@ApiOperation({ summary: '更新租户信息' })
@ApiResponse({
status: 200,
description: '更新成功',
type: ResultDto,
})
update(@Param('id') id: string, @Body() updateTenantDto: UpdateTenantDto) {
return this.tenantService.update(+id, updateTenantDto);
}
@Put(':id/quota')
@ApiOperation({ summary: '更新租户配额' })
@ApiResponse({
status: 200,
description: '更新成功',
type: ResultDto,
})
updateQuota(@Param('id') id: string, @Body() dto: UpdateTenantQuotaDto) {
return this.tenantService.updateQuota(+id, dto);
}
@Put(':id/status')
@ApiOperation({ summary: '更新租户状态' })
@ApiResponse({
status: 200,
description: '更新成功',
type: ResultDto,
})
updateStatus(@Param('id') id: string, @Body() dto: UpdateTenantStatusDto) {
return this.tenantService.updateStatus(+id, dto);
}
@Post(':id/reset-password')
@ApiOperation({ summary: '重置租户密码' })
@ApiResponse({
status: 200,
description: '重置成功',
type: ResultDto,
})
resetPassword(@Param('id') id: string) {
return this.tenantService.resetPassword(+id);
}
@Delete(':id')
@ApiOperation({ summary: '删除租户' })
@ApiResponse({
status: 200,
description: '删除成功',
type: ResultDto,
})
remove(@Param('id') id: string) {
return this.tenantService.remove(+id);
}

View File

@ -0,0 +1,31 @@
import { defineConfig } from 'orval';
export default defineConfig({
readingPlatform: {
output: {
mode: 'split',
target: 'src/api/generated/index.ts',
schemas: 'src/api/generated/model',
client: 'axios',
override: {
mutator: {
path: 'src/api/generated/mutator.ts',
name: 'customMutator',
},
// 自定义类型名称
name: (type) => {
// 移除命名空间前缀,简化类型名称
return type.replace(/^(Result|ResultPageResult)/, '');
},
},
// 导入优化
imports: {
axios: true,
},
},
input: {
// 从后端 Swagger 文档生成
target: 'http://localhost:3000/api-docs-json',
},
},
});

View File

@ -0,0 +1,155 @@
/**
* API
*
* Orval API
*
*/
// 导入 Orval 生成的 API 客户端工厂函数
import { getApi } from './generated/index';
// 导出所有生成的类型
export * from './generated/model';
/**
* API
*
* 使
* ```ts
* import { api } from '@/api/client';
*
* // 调用接口
* const result = await api.teacherCourseControllerFindAll({ page: 1 });
* ```
*/
export const api = getApi();
// 同时导出 getApi 以便需要时创建新实例
export { getApi };
// ============== 类型工具 ==============
/**
*
*
* API
*/
export interface ApiResult<T> {
code: number;
message: string;
data: T;
}
/**
*
*/
export interface PageData<T> {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages?: number;
}
/**
* Result<T> data
*/
export type UnwrapResult<T> = T extends ApiResult<infer U> ? U : T;
/**
*
*/
export type PageResultOf<T> = ApiResult<PageData<T>>;
// ============== 错误处理 ==============
/**
* API
*/
export class ApiError extends Error {
constructor(
public code: number,
message: string,
public data?: any,
) {
super(message);
this.name = 'ApiError';
}
}
/**
*
*
* @param result - API
* @returns
*/
export function isSuccess<T>(result: ApiResult<T>): boolean {
return result.code === 200;
}
/**
* API
*
* @param result - API
* @throws ApiError -
*/
export function throwErrorIfFailed<T>(result: ApiResult<T>): asserts result is ApiResult<T> & { data: T } {
if (!isSuccess(result)) {
throw new ApiError(result.code, result.message, result.data);
}
}
/**
* data
*
* @param result - API
* @returns
*
* @example
* ```ts
* const result = await api.teacherCourseControllerFindAll();
* const data = unwrapData(result); // 类型推断为实际返回的数据类型
* ```
*/
export function unwrapData<T>(result: any): T {
// 尝试直接返回(如果响应已经是数据)
if (result && typeof result === 'object') {
// 如果有 code 字段,说明是包装的响应
if ('code' in result && typeof result.code === 'number') {
if (result.code === 200 || result.code === 0) {
return result.data as T;
}
throw new ApiError(result.code, (result.message as string) || 'Request failed', result.data);
}
// 否则直接返回
return result as T;
}
return result as T;
}
/**
*
*
* @param result - API
* @returns
*
* @example
* ```ts
* const result = await api.teacherCourseControllerFindAll();
* const pageData = unwrapPageData(result); // 类型为 PageData<T>
* ```
*/
export function unwrapPageData<T>(result: any): PageData<T> {
const data = unwrapData<any>(result);
// 如果已经是分页格式
if (data && typeof data === 'object' && 'items' in data && 'total' in data) {
return data as PageData<T>;
}
// 否则包装为分页格式
return {
items: Array.isArray(data) ? data : [],
total: Array.isArray(data) ? data.length : 0,
page: 1,
pageSize: Array.isArray(data) ? data.length : 0,
};
}

View File

@ -1,4 +1,10 @@
import { http } from './index';
import { getApi } from './generated';
import type {
CourseControllerFindAllParams,
CourseControllerGetReviewListParams,
} from './generated/model';
// ============= 类型定义(保持向后兼容) =============
export interface CourseQueryParams {
page?: number;
@ -101,6 +107,20 @@ export interface ValidationWarning {
code: string;
}
// ============= API 函数(使用生成的客户端) =============
// 获取 API 客户端实例
const api = getApi();
// 转换查询参数类型
const toFindAllParams = (params: CourseQueryParams): CourseControllerFindAllParams => ({
page: params.page,
pageSize: params.pageSize,
grade: params.grade,
status: params.status,
keyword: params.keyword,
});
// 获取课程包列表
export function getCourses(params: CourseQueryParams): Promise<{
items: Course[];
@ -108,7 +128,7 @@ export function getCourses(params: CourseQueryParams): Promise<{
page: number;
pageSize: number;
}> {
return http.get('/courses', { params });
return api.courseControllerFindAll(toFindAllParams(params)) as any;
}
// 获取审核列表
@ -118,84 +138,87 @@ export function getReviewList(params: CourseQueryParams): Promise<{
page: number;
pageSize: number;
}> {
return http.get('/courses/review', { params });
const findAllParams = toFindAllParams(params) as CourseControllerGetReviewListParams;
return api.courseControllerGetReviewList(findAllParams) as any;
}
// 获取课程包详情
export function getCourse(id: number): Promise<any> {
return http.get(`/courses/${id}`);
return api.courseControllerFindOne(String(id)) as any;
}
// 创建课程包
export function createCourse(data: any): Promise<any> {
return http.post('/courses', data);
return api.courseControllerCreate(data) as any;
}
// 更新课程包
export function updateCourse(id: number, data: any): Promise<any> {
return http.put(`/courses/${id}`, data);
return api.courseControllerUpdate(String(id), data) as any;
}
// 删除课程包
export function deleteCourse(id: number): Promise<any> {
return http.delete(`/courses/${id}`);
return api.courseControllerRemove(String(id)) as any;
}
// 验证课程完整性
export function validateCourse(id: number): Promise<ValidationResult> {
return http.get(`/courses/${id}/validate`);
return api.courseControllerValidate(String(id)) as any;
}
// 提交审核
export function submitCourse(id: number, copyrightConfirmed: boolean): Promise<any> {
return http.post(`/courses/${id}/submit`, { copyrightConfirmed });
return api.courseControllerSubmit(String(id), { copyrightConfirmed }) as any;
}
// 撤销审核
export function withdrawCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/withdraw`);
return api.courseControllerWithdraw(String(id)) as any;
}
// 审核通过
export function approveCourse(id: number, data: { checklist?: any; comment?: string }): Promise<any> {
return http.post(`/courses/${id}/approve`, data);
return api.courseControllerApprove(String(id), data) as any;
}
// 审核驳回
export function rejectCourse(id: number, data: { checklist?: any; comment: string }): Promise<any> {
return http.post(`/courses/${id}/reject`, data);
return api.courseControllerReject(String(id), data) as any;
}
// 直接发布(超级管理员)
export function directPublishCourse(id: number, skipValidation?: boolean): Promise<any> {
return http.post(`/courses/${id}/direct-publish`, { skipValidation });
return api.courseControllerDirectPublish(String(id), { skipValidation }) as any;
}
// 发布课程包兼容旧API
// 发布课程包
export function publishCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/publish`);
return api.courseControllerPublish(String(id)) as any;
}
// 下架课程包
export function unpublishCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/unpublish`);
return api.courseControllerUnpublish(String(id)) as any;
}
// 重新发布
export function republishCourse(id: number): Promise<any> {
return http.post(`/courses/${id}/republish`);
return api.courseControllerRepublish(String(id)) as any;
}
// 获取课程包统计数据
export function getCourseStats(id: number): Promise<any> {
return http.get(`/courses/${id}/stats`);
return api.courseControllerGetStats(String(id)) as any;
}
// 获取版本历史
export function getCourseVersions(id: number): Promise<any[]> {
return http.get(`/courses/${id}/versions`);
return api.courseControllerGetVersionHistory(String(id)) as any;
}
// ============= 常量 =============
// 课程状态映射
export const COURSE_STATUS_MAP: Record<string, { label: string; color: string }> = {
DRAFT: { label: '草稿', color: 'default' },

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface AddClassTeacherDto { [key: string]: unknown }

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type AdminStatsControllerGetActiveTenantsParams = {
limit: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type AdminStatsControllerGetPopularCoursesParams = {
limit: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type AdminStatsControllerGetRecentActivitiesParams = {
limit: string;
};

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { ApproveCourseDtoChecklist } from './approveCourseDtoChecklist';
export interface ApproveCourseDto {
/** 审核检查项 */
checklist?: ApproveCourseDtoChecklist;
/** 审核意见 */
comment?: string;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type ApproveCourseDtoChecklist = { [key: string]: unknown };

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { BatchStudentRecordsDtoRecordsItem } from './batchStudentRecordsDtoRecordsItem';
export interface BatchStudentRecordsDto {
/** 学生记录列表 */
records: BatchStudentRecordsDtoRecordsItem[];
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type BatchStudentRecordsDtoRecordsItem = { [key: string]: unknown };

View File

@ -0,0 +1,30 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type CourseControllerFindAllParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
*
*/
grade?: string;
/**
*
*/
status?: string;
/**
*
*/
keyword?: string;
};

View File

@ -0,0 +1,30 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type CourseControllerGetReviewListParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
*
*/
grade?: string;
/**
*
*/
status?: string;
/**
*
*/
keyword?: string;
};

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type CoursePackageControllerFindAllParams = {
status: string;
page: string;
pageSize: string;
};

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateClassDto { [key: string]: unknown }

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { CreateFromSourceDtoSaveLocation } from './createFromSourceDtoSaveLocation';
export interface CreateFromSourceDto {
/** 源课程ID */
sourceCourseId: number;
/** 保存位置 */
saveLocation: CreateFromSourceDtoSaveLocation;
}

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type CreateFromSourceDtoSaveLocation = typeof CreateFromSourceDtoSaveLocation[keyof typeof CreateFromSourceDtoSaveLocation];
export const CreateFromSourceDtoSaveLocation = {
PERSONAL: 'PERSONAL',
SCHOOL: 'SCHOOL',
} as const;

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateFromTemplateDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateGrowthRecordDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateLessonDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateLibraryDto { [key: string]: unknown }

View File

@ -0,0 +1,20 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateReservationDto {
/** 教师ID */
teacherId: number;
/** 班级ID */
classId: number;
/** 预约日期 */
scheduledDate: string;
/** 预约时间 */
scheduledTime?: string;
/** 备注 */
note?: string;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateResourceItemDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateScheduleDto { [key: string]: unknown }

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateSchoolCourseDto {
/** 源课程ID */
sourceCourseId: number;
/** 课程名称 */
name: string;
/** 课程描述 */
description?: string;
/** 修改说明 */
changesSummary?: string;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateStudentDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateTaskDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateTaskTemplateDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface CreateTeacherDto { [key: string]: unknown }

View File

@ -0,0 +1,39 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { CreateTenantDtoPackageType } from './createTenantDtoPackageType';
export interface CreateTenantDto {
/** 学校名称 */
name: string;
/** 登录账号必须以字母开头4-20位字母、数字或下划线 */
loginAccount: string;
/** 初始密码至少6位需包含字母和数字 */
password?: string;
/** 学校地址 */
address?: string;
/** 联系人姓名 */
contactPerson?: string;
/** 联系人手机号 */
contactPhone?: string;
/** 套餐类型 */
packageType?: CreateTenantDtoPackageType;
/**
*
* @minimum 1
*/
teacherQuota?: number;
/**
*
* @minimum 1
*/
studentQuota?: number;
/** 服务开始日期YYYY-MM-DD */
startDate?: string;
/** 服务到期日期YYYY-MM-DD */
expireDate?: string;
}

View File

@ -0,0 +1,19 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type CreateTenantDtoPackageType = typeof CreateTenantDtoPackageType[keyof typeof CreateTenantDtoPackageType];
export const CreateTenantDtoPackageType = {
STANDARD: 'STANDARD',
PREMIUM: 'PREMIUM',
TRIAL: 'TRIAL',
} as const;

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface DirectPublishDto {
/** 是否跳过验证 */
skipValidation?: boolean;
}

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type ExportControllerExportGrowthRecordsParams = {
studentId: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type ExportControllerExportStudentStatsParams = {
classId: string;
};

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type ExportControllerExportTeacherStatsParams = {
startDate: string;
endDate: string;
};

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface FinishLessonDto { [key: string]: unknown }

View File

@ -0,0 +1,94 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export * from './addClassTeacherDto';
export * from './adminStatsControllerGetActiveTenantsParams';
export * from './adminStatsControllerGetPopularCoursesParams';
export * from './adminStatsControllerGetRecentActivitiesParams';
export * from './approveCourseDto';
export * from './approveCourseDtoChecklist';
export * from './batchStudentRecordsDto';
export * from './batchStudentRecordsDtoRecordsItem';
export * from './courseControllerFindAllParams';
export * from './courseControllerGetReviewListParams';
export * from './coursePackageControllerFindAllParams';
export * from './createClassDto';
export * from './createFromSourceDto';
export * from './createFromSourceDtoSaveLocation';
export * from './createFromTemplateDto';
export * from './createGrowthRecordDto';
export * from './createLessonDto';
export * from './createLibraryDto';
export * from './createReservationDto';
export * from './createResourceItemDto';
export * from './createScheduleDto';
export * from './createSchoolCourseDto';
export * from './createStudentDto';
export * from './createTaskDto';
export * from './createTaskTemplateDto';
export * from './createTeacherDto';
export * from './createTenantDto';
export * from './createTenantDtoPackageType';
export * from './directPublishDto';
export * from './exportControllerExportGrowthRecordsParams';
export * from './exportControllerExportStudentStatsParams';
export * from './exportControllerExportTeacherStatsParams';
export * from './finishLessonDto';
export * from './lessonControllerFindAllParams';
export * from './lessonFeedbackDto';
export * from './lessonFeedbackDtoActivitiesDone';
export * from './lessonFeedbackDtoStepFeedbacks';
export * from './lessonProgressDto';
export * from './lessonProgressDtoProgressData';
export * from './loginDto';
export * from './object';
export * from './rejectCourseDto';
export * from './rejectCourseDtoChecklist';
export * from './resultDto';
export * from './resultDtoData';
export * from './reviewDto';
export * from './schoolControllerImportStudentsParams';
export * from './schoolFeedbackControllerFindAllParams';
export * from './schoolTaskControllerGetMonthlyStatsParams';
export * from './statsControllerGetActiveTeachersParams';
export * from './statsControllerGetLessonTrendParams';
export * from './statsControllerGetRecentActivitiesParams';
export * from './studentRecordDto';
export * from './submitCourseDto';
export * from './teacherCourseControllerFindAllParams';
export * from './teacherCourseControllerGetAllStudentsParams';
export * from './teacherCourseControllerGetClassStudentsParams';
export * from './teacherCourseControllerGetLessonTrendParams';
export * from './teacherCourseControllerGetTeacherSchedulesParams';
export * from './teacherCourseControllerGetTeacherTimetableParams';
export * from './teacherFeedbackControllerFindAllParams';
export * from './teacherTaskControllerGetMonthlyStatsParams';
export * from './tenantControllerFindAllPackageType';
export * from './tenantControllerFindAllParams';
export * from './tenantControllerFindAllStatus';
export * from './transferStudentDto';
export * from './updateClassDto';
export * from './updateClassTeacherDto';
export * from './updateCompletionDto';
export * from './updateGrowthRecordDto';
export * from './updateLessonDto';
export * from './updateLibraryDto';
export * from './updateResourceItemDto';
export * from './updateScheduleDto';
export * from './updateSchoolCourseDto';
export * from './updateStudentDto';
export * from './updateTaskDto';
export * from './updateTaskTemplateDto';
export * from './updateTeacherDto';
export * from './updateTenantDto';
export * from './updateTenantDtoPackageType';
export * from './updateTenantDtoStatus';
export * from './updateTenantQuotaDto';
export * from './updateTenantQuotaDtoPackageType';
export * from './updateTenantStatusDto';
export * from './updateTenantStatusDtoStatus';

View File

@ -0,0 +1,26 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type LessonControllerFindAllParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
* ID
*/
courseId?: number;
/**
*
*/
status?: string;
};

View File

@ -0,0 +1,26 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { LessonFeedbackDtoActivitiesDone } from './lessonFeedbackDtoActivitiesDone';
import type { LessonFeedbackDtoStepFeedbacks } from './lessonFeedbackDtoStepFeedbacks';
export interface LessonFeedbackDto {
/** 设计质量评分 */
designQuality?: number;
/** 参与度评分 */
participation?: number;
/** 目标达成度评分 */
goalAchievement?: number;
/** 环节反馈 */
stepFeedbacks?: LessonFeedbackDtoStepFeedbacks;
/** 优点 */
pros?: string;
/** 建议 */
suggestions?: string;
/** 完成的活动 */
activitiesDone?: LessonFeedbackDtoActivitiesDone;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type LessonFeedbackDtoActivitiesDone = { [key: string]: unknown };

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type LessonFeedbackDtoStepFeedbacks = { [key: string]: unknown };

View File

@ -0,0 +1,21 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { LessonProgressDtoProgressData } from './lessonProgressDtoProgressData';
export interface LessonProgressDto {
/** 课程ID列表 */
lessonIds?: string[];
/** 已完成课程ID列表 */
completedLessonIds?: string[];
/** 当前课程ID */
currentLessonId?: number;
/** 当前环节ID */
currentStepId?: number;
/** 进度数据 */
progressData?: LessonProgressDtoProgressData;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type LessonProgressDtoProgressData = { [key: string]: unknown };

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface LoginDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface Object { [key: string]: unknown }

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { RejectCourseDtoChecklist } from './rejectCourseDtoChecklist';
export interface RejectCourseDto {
/** 审核检查项 */
checklist?: RejectCourseDtoChecklist;
/** 驳回原因 */
comment: string;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type RejectCourseDtoChecklist = { [key: string]: unknown };

View File

@ -0,0 +1,17 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { ResultDtoData } from './resultDtoData';
export interface ResultDto {
/** 响应状态码 */
code: number;
/** 响应消息 */
message: string;
/** 响应数据 */
data: ResultDtoData;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
/**
*
*/
export type ResultDtoData = { [key: string]: unknown };

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface ReviewDto {
/** 审核意见 */
comment?: string;
}

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type SchoolControllerImportStudentsParams = {
defaultClassId: string;
};

View File

@ -0,0 +1,26 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type SchoolFeedbackControllerFindAllParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
* ID
*/
courseId?: number;
/**
*
*/
status?: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type SchoolTaskControllerGetMonthlyStatsParams = {
months: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type StatsControllerGetActiveTeachersParams = {
limit: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type StatsControllerGetLessonTrendParams = {
months: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type StatsControllerGetRecentActivitiesParams = {
limit: string;
};

View File

@ -0,0 +1,20 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface StudentRecordDto {
/** 专注度 */
focus?: number;
/** 参与度 */
participation?: number;
/** 兴趣度 */
interest?: number;
/** 理解度 */
understanding?: number;
/** 备注 */
notes?: string;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface SubmitCourseDto {
/** 是否确认版权 */
copyrightConfirmed: boolean;
}

View File

@ -0,0 +1,26 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherCourseControllerFindAllParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
*
*/
grade?: string;
/**
*
*/
keyword?: string;
};

View File

@ -0,0 +1,22 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherCourseControllerGetAllStudentsParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
*
*/
keyword?: string;
};

View File

@ -0,0 +1,22 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherCourseControllerGetClassStudentsParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
*
*/
keyword?: string;
};

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherCourseControllerGetLessonTrendParams = {
/**
*
*/
months?: number;
};

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherCourseControllerGetTeacherSchedulesParams = {
/**
*
*/
startDate?: string;
/**
*
*/
endDate?: string;
};

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherCourseControllerGetTeacherTimetableParams = {
/**
*
*/
startDate: string;
/**
*
*/
endDate: string;
};

View File

@ -0,0 +1,26 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherFeedbackControllerFindAllParams = {
/**
*
*/
page?: number;
/**
*
*/
pageSize?: number;
/**
* ID
*/
courseId?: number;
/**
*
*/
status?: string;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TeacherTaskControllerGetMonthlyStatsParams = {
months: string;
};

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TenantControllerFindAllPackageType = typeof TenantControllerFindAllPackageType[keyof typeof TenantControllerFindAllPackageType];
export const TenantControllerFindAllPackageType = {
STANDARD: 'STANDARD',
PREMIUM: 'PREMIUM',
TRIAL: 'TRIAL',
} as const;

View File

@ -0,0 +1,34 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
import type { TenantControllerFindAllPackageType } from './tenantControllerFindAllPackageType';
import type { TenantControllerFindAllStatus } from './tenantControllerFindAllStatus';
export type TenantControllerFindAllParams = {
/**
* 1
*/
page?: number;
/**
*
* @minimum 1
* @maximum 100
*/
pageSize?: number;
/**
*
*/
keyword?: string;
/**
*
*/
status?: TenantControllerFindAllStatus;
/**
*
*/
packageType?: TenantControllerFindAllPackageType;
};

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export type TenantControllerFindAllStatus = typeof TenantControllerFindAllStatus[keyof typeof TenantControllerFindAllStatus];
export const TenantControllerFindAllStatus = {
ACTIVE: 'ACTIVE',
INACTIVE: 'INACTIVE',
SUSPENDED: 'SUSPENDED',
} as const;

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface TransferStudentDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateClassDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateClassTeacherDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateCompletionDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateGrowthRecordDto { [key: string]: unknown }

View File

@ -0,0 +1,22 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateLessonDto {
/** 教学目标 */
objectives?: string;
/** 课前准备 */
preparation?: string;
/** 课后延伸 */
extension?: string;
/** 课后反思 */
reflection?: string;
/** 修改备注 */
changeNote?: string;
/** 环节数据 */
stepsData?: string;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateLibraryDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateResourceItemDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateScheduleDto { [key: string]: unknown }

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateSchoolCourseDto {
/** 课程名称 */
name?: string;
/** 课程描述 */
description?: string;
/** 修改说明 */
changesSummary?: string;
/** 状态 */
status?: string;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateStudentDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateTaskDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateTaskTemplateDto { [key: string]: unknown }

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* API
* API
* OpenAPI spec version: 2.0
*/
export interface UpdateTeacherDto { [key: string]: unknown }

Some files were not shown because too many files have changed in this diff Show More