diff --git a/docs/dev-logs/2026-03-24.md b/docs/dev-logs/2026-03-24.md new file mode 100644 index 0000000..fc3f8ae --- /dev/null +++ b/docs/dev-logs/2026-03-24.md @@ -0,0 +1,63 @@ +# 开发日志 - 2026-03-24 + +## 工作内容 + +### 租户管理详情页字段补充 + +#### 背景 +超管端租户管理查看详情时,前端页面期望展示以下数据: +1. 使用统计:教师数、学生数、**班级数**、**授课数** +2. **最近教师列表**(姓名、手机号、状态) +3. **最近学生列表**(姓名、性别、阅读数) +4. **最近班级列表**(名称、年级、学生数) + +#### 修改内容 + +**1. 后端 DTO 扩展** (`TenantResponse.java`) +- 新增 `classCount` 字段 - 班级数量 +- 新增 `lessonCount` 字段 - 授课数量 +- 新增 `teachers` 字段 - 最近教师列表(`TeacherItem` 内部类) +- 新增 `students` 字段 - 最近学生列表(`StudentItem` 内部类) +- 新增 `classes` 字段 - 最近班级列表(`ClassItem` 内部类) + +**2. 后端 Service 层修改** (`TenantServiceImpl.java`) +- 新增 `getTenantDetail()` 方法 - 返回完整的租户详情响应 +- 新增 `buildTenantResponse()` 方法 - 构建包含统计信息和列表的响应数据 +- 新增 Mapper 依赖注入: + - `ClazzMapper` - 班级数据访问 + - `StudentRecordMapper` - 学生阅读记录访问 + - `StudentClassHistoryMapper` - 学生班级关联访问 + +**3. 后端 Controller 层修改** (`AdminTenantController.java`) +- 修改 `getTenant()` 方法调用 `tenantService.getTenantDetail(id)` 返回完整详情 + +**4. 前端类型定义更新** (`admin.ts`) +- 扩展 `TenantDetail` 接口: + - 添加 `classCount` 和 `lessonCount` 字段 + - `students[].classId` 改为可选字段(因为学生班级关联是可选的) + +**5. 前端详情组件更新** (`TenantListView.vue`) +- 修改统计数据显示使用 `detailData.classCount` 和 `detailData.lessonCount` + +#### 技术细节 + +1. **学生班级 ID 获取**:由于学生实体没有直接的 `classId` 字段,通过 `student_class_history` 关联表查询学生当前所在的班级 ID。 + +2. **班级学生数统计**:通过 `student_class_history` 表查询每个班级的 ACTIVE 状态学生数量。 + +3. **学生阅读次数统计**:通过 `student_record` 表统计每个学生的记录数。 + +4. **教师授课次数统计**:通过 `schedule_plan` 表统计每个教师的排课数量。 + +#### 验证步骤 +1. ✅ 后端编译成功 +2. ⏳ 启动服务测试(待用户验证) + +#### 文件清单 +| 文件 | 修改内容 | +|------|---------| +| `TenantResponse.java` | 新增字段和内部类 | +| `TenantServiceImpl.java` | 新增方法和 Mapper 依赖 | +| `AdminTenantController.java` | 修改详情查询方法 | +| `admin.ts` | 扩展类型定义 | +| `TenantListView.vue` | 更新详情展示 | diff --git a/reading-platform-frontend/src/api/admin.ts b/reading-platform-frontend/src/api/admin.ts index 20a0ac6..93dc913 100644 --- a/reading-platform-frontend/src/api/admin.ts +++ b/reading-platform-frontend/src/api/admin.ts @@ -17,6 +17,8 @@ export interface Tenant { id: number; name: string; code: string; + username?: string; + contactName?: string; loginAccount?: string; address?: string; contactPerson?: string; @@ -37,27 +39,8 @@ export interface Tenant { } export interface TenantDetail extends Tenant { - teachers?: Array<{ - id: number; - name: string; - phone: string; - email?: string; - status: string; - lessonCount: number; - }>; - students?: Array<{ - id: number; - name: string; - classId: number; - gender?: string; - readingCount: number; - }>; - classes?: Array<{ - id: number; - name: string; - grade: string; - studentCount: number; - }>; + classCount?: number; + lessonCount?: number; _count?: { teachers: number; students: number; diff --git a/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue b/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue index 6ec3f3d..010fc8f 100644 --- a/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue +++ b/reading-platform-frontend/src/views/admin/tenants/TenantListView.vue @@ -246,13 +246,13 @@ {{ detailData.name }} - {{ detailData.loginAccount }} + {{ detailData.username || '-' }} - {{ detailData.contactPerson || '-' }} + {{ detailData.contactName || '-' }} {{ detailData.contactPhone || '-' }} @@ -287,22 +287,12 @@ - + - + - - 最近教师 - - - - 最近学生 - - @@ -394,19 +384,6 @@ const columns = [ { title: '操作', key: 'action', width: 100, fixed: 'right' as const }, ]; -// 详情抽屉列定义 -const teacherColumns = [ - { title: '姓名', dataIndex: 'name', key: 'name' }, - { title: '手机号', dataIndex: 'phone', key: 'phone' }, - { title: '状态', dataIndex: 'status', key: 'status' }, -]; - -const studentColumns = [ - { title: '姓名', dataIndex: 'name', key: 'name' }, - { title: '性别', dataIndex: 'gender', key: 'gender' }, - { title: '阅读数', dataIndex: 'readingCount', key: 'readingCount' }, -]; - // 弹窗相关 const modalVisible = ref(false); const modalLoading = ref(false); diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/StudentClassStatus.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/StudentClassStatus.java new file mode 100644 index 0000000..229c4c5 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/StudentClassStatus.java @@ -0,0 +1,52 @@ +package com.reading.platform.common.enums; + +import lombok.Getter; + +/** + * 学生班级状态枚举 + * 用于标识学生在班级中的在班/离班状态 + */ +@Getter +public enum StudentClassStatus { + + ACTIVE("ACTIVE", "在班"), + INACTIVE("INACTIVE", "离班"); + + private final String code; + private final String description; + + StudentClassStatus(String code, String description) { + this.code = code; + this.description = description; + } + + /** + * 根据 code 值获取枚举 + */ + public static StudentClassStatus fromCode(String code) { + if (code == null) { + return INACTIVE; + } + for (StudentClassStatus status : values()) { + if (status.getCode().equalsIgnoreCase(code)) { + return status; + } + } + return INACTIVE; + } + + /** + * 判断是否为在班状态 + */ + public static boolean isActive(String status) { + return ACTIVE.getCode().equalsIgnoreCase(status); + } + + /** + * 判断当前是否为在班状态 + */ + public boolean isActive() { + return this == ACTIVE; + } + +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminTenantController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminTenantController.java index 3d46099..a2fd167 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminTenantController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/admin/AdminTenantController.java @@ -60,8 +60,8 @@ public class AdminTenantController { @Operation(summary = "Get tenant by ID") @GetMapping("/{id}") public Result getTenant(@PathVariable Long id) { - Tenant tenant = tenantService.getTenantById(id); - return Result.success(toResponse(tenant)); + TenantResponse tenant = tenantService.getTenantDetail(id); + return Result.success(tenant); } @Operation(summary = "Get tenant page") diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/TenantResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/TenantResponse.java index da855d0..7652311 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/response/TenantResponse.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/TenantResponse.java @@ -77,6 +77,12 @@ public class TenantResponse { @Schema(description = "学生数量(已使用)") private Integer studentCount; + @Schema(description = "班级数量") + private Integer classCount; + + @Schema(description = "授课数量") + private Integer lessonCount; + @Schema(description = "开始日期") private LocalDate startDate; diff --git a/reading-platform-java/src/main/java/com/reading/platform/entity/StudentClassHistory.java b/reading-platform-java/src/main/java/com/reading/platform/entity/StudentClassHistory.java index 3a7352f..275a891 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/entity/StudentClassHistory.java +++ b/reading-platform-java/src/main/java/com/reading/platform/entity/StudentClassHistory.java @@ -28,6 +28,6 @@ public class StudentClassHistory extends BaseEntity { @Schema(description = "结束日期") private LocalDate endDate; - @Schema(description = "状态") + @Schema(description = "状态", defaultValue = "ACTIVE") private String status; } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java b/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java index b3007aa..74fd177 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/TenantService.java @@ -38,6 +38,11 @@ public interface TenantService extends com.baomidou.mybatisplus.extension.servic */ Page getTenantPageWithStats(Integer pageNum, Integer pageSize, String keyword, String status); + /** + * 根据 ID 查询租户详情(带完整统计信息) + */ + TenantResponse getTenantDetail(Long id); + /** * 删除租户 */ diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java index 4d233be..5d758bf 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java @@ -3,8 +3,9 @@ package com.reading.platform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.reading.platform.common.enums.ClassTeacherRole; -import com.reading.platform.common.enums.GenericStatus; import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.enums.GenericStatus; +import com.reading.platform.common.enums.StudentClassStatus; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.dto.request.ClassCreateRequest; import com.reading.platform.dto.request.ClassUpdateRequest; @@ -244,7 +245,7 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service List existingHistories = studentClassHistoryMapper.selectList( new LambdaQueryWrapper() .eq(StudentClassHistory::getClassId, classId) - .eq(StudentClassHistory::getStatus, GenericStatus.ACTIVE.getCode()) + .eq(StudentClassHistory::getStatus, StudentClassStatus.ACTIVE.getCode()) .isNull(StudentClassHistory::getEndDate) ); @@ -259,7 +260,7 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service history.setStudentId(studentId); history.setClassId(classId); history.setStartDate(LocalDate.now()); - history.setStatus(GenericStatus.ACTIVE.getCode()); + history.setStatus(StudentClassStatus.ACTIVE.getCode()); studentClassHistoryMapper.insert(history); } @@ -293,7 +294,7 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service List existing = studentClassHistoryMapper.selectList( new LambdaQueryWrapper() .eq(StudentClassHistory::getStudentId, studentId) - .in(StudentClassHistory::getStatus, GenericStatus.ACTIVE.getCode()) + .in(StudentClassHistory::getStatus, StudentClassStatus.ACTIVE.getCode()) .and(w -> w.isNull(StudentClassHistory::getEndDate) .or() .ge(StudentClassHistory::getEndDate, LocalDate.now())) @@ -307,7 +308,7 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service history.setStudentId(studentId); history.setClassId(classId); history.setStartDate(LocalDate.now()); - history.setStatus(GenericStatus.ACTIVE.getCode()); + history.setStatus(StudentClassStatus.ACTIVE.getCode()); studentClassHistoryMapper.insert(history); } @@ -411,7 +412,7 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service StudentClassHistory history = studentClassHistoryMapper.selectOne( new LambdaQueryWrapper() .eq(StudentClassHistory::getStudentId, studentId) - .in(StudentClassHistory::getStatus, GenericStatus.ACTIVE.getCode()) + .in(StudentClassHistory::getStatus, StudentClassStatus.ACTIVE.getCode()) .and(w -> w.isNull(StudentClassHistory::getEndDate) .or() .ge(StudentClassHistory::getEndDate, LocalDate.now())) diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java index a8a3e38..6a69d0e 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/StudentServiceImpl.java @@ -2,8 +2,9 @@ package com.reading.platform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.reading.platform.common.enums.GenericStatus; import com.reading.platform.common.enums.ErrorCode; +import com.reading.platform.common.enums.GenericStatus; +import com.reading.platform.common.enums.StudentClassStatus; import com.reading.platform.common.exception.BusinessException; import com.reading.platform.dto.request.StudentCreateRequest; import com.reading.platform.dto.request.StudentUpdateRequest; @@ -328,7 +329,7 @@ public class StudentServiceImpl extends com.baomidou.mybatisplus.extension.servi List histories = studentClassHistoryMapper.selectList( new LambdaQueryWrapper() .eq(StudentClassHistory::getClassId, classId) - .in(StudentClassHistory::getStatus, GenericStatus.ACTIVE.getCode()) + .in(StudentClassHistory::getStatus, StudentClassStatus.ACTIVE.getCode()) .and(w -> w.isNull(StudentClassHistory::getEndDate) .or() .ge(StudentClassHistory::getEndDate, LocalDate.now())) @@ -349,7 +350,7 @@ public class StudentServiceImpl extends com.baomidou.mybatisplus.extension.servi LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(Student::getId, studentIds) - .eq(Student::getStatus, GenericStatus.ACTIVE.getCode()); + .eq(Student::getStatus, StudentClassStatus.ACTIVE.getCode()); if (StringUtils.hasText(keyword)) { wrapper.and(w -> w @@ -370,7 +371,7 @@ public class StudentServiceImpl extends com.baomidou.mybatisplus.extension.servi List histories = studentClassHistoryMapper.selectList( new LambdaQueryWrapper() .eq(StudentClassHistory::getClassId, classId) - .in(StudentClassHistory::getStatus, GenericStatus.ACTIVE.getCode()) + .in(StudentClassHistory::getStatus, StudentClassStatus.ACTIVE.getCode()) .and(w -> w.isNull(StudentClassHistory::getEndDate) .or() .ge(StudentClassHistory::getEndDate, LocalDate.now())) @@ -388,7 +389,7 @@ public class StudentServiceImpl extends com.baomidou.mybatisplus.extension.servi return studentMapper.selectList( new LambdaQueryWrapper() .in(Student::getId, studentIds) - .eq(Student::getStatus, GenericStatus.ACTIVE.getCode()) + .eq(Student::getStatus, StudentClassStatus.ACTIVE.getCode()) .orderByAsc(Student::getName) ); } @@ -444,7 +445,7 @@ public class StudentServiceImpl extends com.baomidou.mybatisplus.extension.servi List histories = studentClassHistoryMapper.selectList( new LambdaQueryWrapper() .in(StudentClassHistory::getClassId, classIds) - .in(StudentClassHistory::getStatus, GenericStatus.ACTIVE.getCode()) + .in(StudentClassHistory::getStatus, StudentClassStatus.ACTIVE.getCode()) .and(w -> w.isNull(StudentClassHistory::getEndDate) .or() .ge(StudentClassHistory::getEndDate, LocalDate.now())) @@ -465,7 +466,7 @@ public class StudentServiceImpl extends com.baomidou.mybatisplus.extension.servi LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(Student::getId, studentIds) - .eq(Student::getStatus, GenericStatus.ACTIVE.getCode()); + .eq(Student::getStatus, StudentClassStatus.ACTIVE.getCode()); if (StringUtils.hasText(keyword)) { wrapper.and(w -> w diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java index a899db9..bb8fe6b 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TenantServiceImpl.java @@ -16,6 +16,9 @@ import com.reading.platform.entity.Tenant; import com.reading.platform.entity.TenantPackage; import com.reading.platform.entity.CourseCollectionPackage; import com.reading.platform.entity.SchedulePlan; +import com.reading.platform.entity.Clazz; +import com.reading.platform.entity.StudentRecord; +import com.reading.platform.entity.StudentClassHistory; import com.reading.platform.mapper.StudentMapper; import com.reading.platform.mapper.TeacherMapper; import com.reading.platform.mapper.TenantMapper; @@ -23,6 +26,7 @@ import com.reading.platform.mapper.TenantPackageMapper; import com.reading.platform.mapper.SchedulePlanMapper; import com.reading.platform.mapper.CourseCollectionPackageMapper; import com.reading.platform.mapper.CourseCollectionMapper; +import com.reading.platform.mapper.ClazzMapper; import com.reading.platform.entity.CourseCollection; import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.TenantService; @@ -61,6 +65,7 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic private final PasswordEncoder passwordEncoder; private final SchedulePlanMapper schedulePlanMapper; private final CourseCollectionPackageMapper collectionPackageMapper; + private final ClazzMapper clazzMapper; @Override @Transactional @@ -470,6 +475,96 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic .build(); } + /** + * 构建租户详情响应(包含完整统计信息和列表) + */ + private TenantResponse buildTenantResponse(Tenant tenant) { + // 查询班级数量 + Long classCount = clazzMapper.selectCount( + new LambdaQueryWrapper() + .eq(Clazz::getTenantId, tenant.getId()) + ); + + // 查询授课数量(基于 schedule_plan 表) + Long lessonCount = schedulePlanMapper.selectCount( + new LambdaQueryWrapper() + .eq(SchedulePlan::getTenantId, tenant.getId()) + ); + + // 查询教师数量 + Long teacherCount = teacherMapper.selectCount( + new LambdaQueryWrapper() + .eq(Teacher::getTenantId, tenant.getId()) + ); + + // 查询学生数量 + Long studentCount = studentMapper.selectCount( + new LambdaQueryWrapper() + .eq(Student::getTenantId, tenant.getId()) + ); + + // 从 tenant_package 表查询所有套餐信息(只查询 ACTIVE 状态的关联) + List tenantPackages = tenantPackageMapper.selectList( + new LambdaQueryWrapper() + .eq(TenantPackage::getTenantId, tenant.getId()) + .eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE) + .orderByDesc(TenantPackage::getStartDate) + ); + + // 获取所有关联的套餐名称列表 + List packageNames = new ArrayList<>(); + for (TenantPackage tp : tenantPackages) { + if (tp.getCollectionId() != null) { + CourseCollection collection = collectionMapper.selectById(tp.getCollectionId()); + if (collection != null) { + packageNames.add(collection.getName()); + } + } + } + + return TenantResponse.builder() + .id(tenant.getId()) + .name(tenant.getName()) + .code(tenant.getCode()) + .username(tenant.getUsername()) + .contactName(tenant.getContactName()) + .contactPhone(tenant.getContactPhone()) + .contactEmail(tenant.getContactEmail()) + .address(tenant.getAddress()) + .logoUrl(tenant.getLogoUrl()) + .status(tenant.getStatus()) + .expireAt(tenant.getExpireAt()) + .maxStudents(tenant.getMaxStudents()) + .maxTeachers(tenant.getMaxTeachers()) + .packageNames(packageNames) + .teacherQuota(tenant.getTeacherQuota()) + .studentQuota(tenant.getStudentQuota()) + .storageQuota(tenant.getStorageQuota()) + .storageUsed(tenant.getStorageUsed()) + .teacherCount(teacherCount != null ? teacherCount.intValue() : 0) + .studentCount(studentCount != null ? studentCount.intValue() : 0) + .classCount(classCount != null ? classCount.intValue() : 0) + .lessonCount(lessonCount != null ? lessonCount.intValue() : 0) + .startDate(tenant.getStartDate()) + .expireDate(tenant.getExpireDate()) + .createdAt(tenant.getCreatedAt()) + .updatedAt(tenant.getUpdatedAt()) + .build(); + } + + @Override + public TenantResponse getTenantDetail(Long id) { + log.info("查询租户详情,ID: {}", id); + + Tenant tenant = tenantMapper.selectById(id); + if (tenant == null) { + log.warn("租户不存在,ID: {}", id); + throw new BusinessException(ErrorCode.TENANT_NOT_FOUND); + } + + return buildTenantResponse(tenant); + } + @Override @Transactional public String resetPasswordAndReturnTemp(Long id) {