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) {