diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 1f16127..4f9b308 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -3,7 +3,8 @@
"allow": [
"Bash(mvn compile:*)",
"Bash(sed:*)",
- "Bash(grep:*)"
+ "Bash(grep:*)",
+ "Bash(export:*)"
]
}
}
diff --git a/reading-platform-frontend/src/api/generated/model/tenantUpdateRequest.ts b/reading-platform-frontend/src/api/generated/model/tenantUpdateRequest.ts
index 6a805d2..a6722db 100644
--- a/reading-platform-frontend/src/api/generated/model/tenantUpdateRequest.ts
+++ b/reading-platform-frontend/src/api/generated/model/tenantUpdateRequest.ts
@@ -24,7 +24,7 @@ export interface TenantUpdateRequest {
logoUrl?: string;
/** 状态 */
status?: string;
- /** 课程套餐ID(用于三层架构) */
+ /** 课程套餐 ID(用于三层架构) */
collectionIds?: number[];
/** 教师配额 */
teacherQuota?: number;
@@ -34,4 +34,6 @@ export interface TenantUpdateRequest {
startDate?: string;
/** 结束日期 */
expireDate?: string;
+ /** 是否强制移除套餐(当套餐下有排课计划时需要此参数) */
+ forceRemove?: boolean;
}
diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts
index ba6ac47..e5b00ea 100644
--- a/reading-platform-frontend/src/api/school.ts
+++ b/reading-platform-frontend/src/api/school.ts
@@ -875,29 +875,49 @@ export const getOperationLogs = (params?: {
action?: string;
startDate?: string;
endDate?: string;
-}) => http.get<{ records: OperationLog[]; total: number; pageNum: number; pageSize: number; pages: number }>(
+}) => http.get<{ list: any; total: number; pageNum: number; pageSize: number; pages: number }>(
'/v1/school/operation-logs',
{ params }
-).then(res => ({
- list: res.records || [],
- total: res.total || 0,
- pageNum: res.pageNum || 1,
- pageSize: res.pageSize || 10,
- pages: res.pages || 0,
-}));
+).then(res => {
+ // 字段映射:后端 userRole -> 前端 userType,后端 details -> 前端 description
+ return {
+ list: (res.list || []).map(log => ({
+ ...log,
+ userType: (log as any).userRole, // 后端字段 userRole 映射为前端 userType
+ description: (log as any).details, // 后端字段 details 映射为前端 description
+ oldValue: null, // 后端暂未支持
+ newValue: null, // 后端暂未支持
+ })),
+ total: res.total || 0,
+ pageNum: res.pageNum || 1,
+ pageSize: res.pageSize || 10,
+ pages: res.pages || 0,
+ };
+});
export const getOperationLogStats = (startDate?: string, endDate?: string) =>
http.get<{ totalLogs: number; byModule: Record; byOperator: Record }>('/v1/school/operation-logs/stats', {
params: { startDate, endDate },
- }).then(res => ({
- totalLogs: res.totalLogs || 0,
- byModule: res.byModule || {},
- byOperator: res.byOperator || {},
- }));
+ }).then(res => {
+ // 字段映射:后端 byModule -> 前端 modules 数组格式
+ return {
+ total: res.totalLogs || 0,
+ modules: Object.entries(res.byModule || {}).map(([name, count]) => ({ name, count })),
+ byModule: res.byModule || {},
+ byOperator: res.byOperator || {},
+ };
+ });
export const getOperationLogById = (id: number) =>
http.get(`/v1/school/operation-logs/${id}`);
+/**
+ * 获取可用模块列表
+ * 用于操作日志页面的模块筛选下拉框
+ */
+export const getOperationLogModules = () =>
+ http.get('/v1/school/operation-logs/modules');
+
// ==================== 任务模板 API ====================
export interface TaskTemplate {
diff --git a/reading-platform-frontend/src/constants/logOperationType.ts b/reading-platform-frontend/src/constants/logOperationType.ts
new file mode 100644
index 0000000..6863fe4
--- /dev/null
+++ b/reading-platform-frontend/src/constants/logOperationType.ts
@@ -0,0 +1,21 @@
+/**
+ * 操作类型映射(后端 code → 前端中文描述)
+ * 对应后端 LogOperationType 枚举
+ */
+export const LOG_OPERATION_TYPE_LABELS: Record = {
+ CREATE: '新增',
+ UPDATE: '修改',
+ DELETE: '删除',
+ QUERY: '查询',
+ EXPORT: '导出',
+ IMPORT: '导入',
+ OTHER: '其他',
+};
+
+/**
+ * 操作类型选项(用于下拉选择)
+ */
+export const LOG_OPERATION_TYPE_OPTIONS = Object.entries(LOG_OPERATION_TYPE_LABELS).map(([code, label]) => ({
+ code,
+ label,
+}));
diff --git a/reading-platform-frontend/src/router/index.ts b/reading-platform-frontend/src/router/index.ts
index d49e637..f72c69f 100644
--- a/reading-platform-frontend/src/router/index.ts
+++ b/reading-platform-frontend/src/router/index.ts
@@ -467,7 +467,9 @@ const router = createRouter({
// 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
- const userRole = localStorage.getItem('role');
+ const userRoleRaw = localStorage.getItem('role');
+ // role 转为小写以匹配路由
+ const userRole = userRoleRaw ? userRoleRaw.toLowerCase() : null;
// 设置页面标题
if (to.meta.title) {
diff --git a/reading-platform-frontend/src/utils/tagMaps.ts b/reading-platform-frontend/src/utils/tagMaps.ts
index c7c71b9..25f3c28 100644
--- a/reading-platform-frontend/src/utils/tagMaps.ts
+++ b/reading-platform-frontend/src/utils/tagMaps.ts
@@ -490,10 +490,10 @@ export function translateResourceType(type: string): string {
// 通用状态映射(用于 Student、Teacher、Parent、Class 等实体的 active/ACTIVE 状态)
export const GENERIC_STATUS_MAP: Record = {
- ACTIVE: "激活",
- active: "激活",
- INACTIVE: "未激活",
- inactive: "未激活",
+ ACTIVE: "启用",
+ active: "启用",
+ INACTIVE: "停用",
+ inactive: "停用",
ARCHIVED: "归档",
archived: "归档",
};
@@ -503,8 +503,8 @@ export const GENERIC_STATUS_COLORS: Record<
string,
{ bg: string; text: string }
> = {
- 激活: { bg: "#E8F5E9", text: "#43A047" },
- 未激活: { bg: "#F5F5F5", text: "#9E9E9E" },
+ 启用: { bg: "#E8F5E9", text: "#43A047" },
+ 停用: { bg: "#F5F5F5", text: "#9E9E9E" },
归档: { bg: "#FFF8E1", text: "#F9A825" },
};
diff --git a/reading-platform-frontend/src/views/admin/tenants/modify_tenant.py b/reading-platform-frontend/src/views/admin/tenants/modify_tenant.py
new file mode 100644
index 0000000..c9e7981
--- /dev/null
+++ b/reading-platform-frontend/src/views/admin/tenants/modify_tenant.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+with open('TenantListView.vue', 'r', encoding='utf-8') as f:
+ content = f.read()
+
+# 找到 handleModalOk 函数中的 catch 块并修改
+old_catch = ''' } catch (error: any) {
+ message.error(error.response?.data?.message || '操作失败');
+ } finally {
+ modalLoading.value = false;
+ }
+};'''
+
+new_catch = ''' } catch (error: any) {
+ // 处理错误码 3102 - 套餐下有排课计划
+ if (error.response?.data?.code === 3102) {
+ const warnings = error.response.data.data as Array<{
+ collectionId: number;
+ collectionName: string;
+ scheduleCount: number;
+ }>;
+ // 保存当前表单数据
+ pendingFormData.value = {
+ ...formData,
+ startDate,
+ expireDate,
+ };
+ // 显示强制移除确认弹窗
+ forceRemoveWarnings.value = warnings;
+ forceRemoveModalVisible.value = true;
+ modalLoading.value = false;
+ return;
+ }
+ message.error(error.response?.data?.message || '操作失败');
+ } finally {
+ modalLoading.value = false;
+ }
+};
+
+// 强制移除确认弹窗 - 确认
+const handleForceRemoveConfirm = async () => {
+ if (!pendingFormData.value || !editingId.value) {
+ return;
+ }
+
+ modalLoading.value = true;
+ try {
+ // 传递 forceRemove: true 重新调用更新接口
+ await updateTenant(editingId.value, { ...pendingFormData.value, forceRemove: true } as UpdateTenantDto);
+ message.success('更新成功');
+ modalVisible.value = false;
+ forceRemoveModalVisible.value = false;
+ pendingFormData.value = null;
+ loadData();
+ } catch (error: any) {
+ message.error(error.response?.data?.message || '操作失败');
+ } finally {
+ modalLoading.value = false;
+ }
+};
+
+// 强制移除确认弹窗 - 取消
+const handleForceRemoveCancel = () => {
+ forceRemoveModalVisible.value = false;
+ pendingFormData.value = null;
+ forceRemoveWarnings.value = [];
+};'''
+
+content = content.replace(old_catch, new_catch)
+
+with open('TenantListView.vue', 'w', encoding='utf-8') as f:
+ f.write(content)
+
+print('修改成功')
diff --git a/reading-platform-frontend/src/views/school/settings/OperationLogView.vue b/reading-platform-frontend/src/views/school/settings/OperationLogView.vue
index 4d231f4..d6fae76 100644
--- a/reading-platform-frontend/src/views/school/settings/OperationLogView.vue
+++ b/reading-platform-frontend/src/views/school/settings/OperationLogView.vue
@@ -25,8 +25,8 @@
style="width: 150px"
@change="loadLogs"
>
-
- {{ action }}
+
+ {{ option.label }}
([]);
-const modules = ref(['排课管理', '教师管理', '学生管理', '班级管理', '课程管理']);
-const actions = ref(['创建', '更新', '删除', '创建排课', '批量创建排课', '取消排课']);
+const modules = ref([]);
+const actions = ref(LOG_OPERATION_TYPE_OPTIONS);
// 筛选
const filters = reactive({
@@ -215,6 +216,16 @@ const loadLogs = async () => {
}
};
+// 加载模块列表
+const loadModules = async () => {
+ try {
+ const res = await getOperationLogModules();
+ modules.value = res;
+ } catch (error) {
+ console.error('加载模块列表失败', error);
+ }
+};
+
// 加载统计
const loadStats = async () => {
try {
@@ -278,6 +289,7 @@ const getUserTypeColor = (type: string) => {
};
onMounted(() => {
+ loadModules();
loadLogs();
loadStats();
});
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/annotation/Log.java b/reading-platform-java/src/main/java/com/reading/platform/common/annotation/Log.java
index b2a8e62..7d28844 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/common/annotation/Log.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/annotation/Log.java
@@ -1,5 +1,8 @@
package com.reading.platform.common.annotation;
+import com.reading.platform.common.enums.LogModule;
+import com.reading.platform.common.enums.LogOperationType;
+
import java.lang.annotation.*;
/**
@@ -22,7 +25,7 @@ public @interface Log {
* 例如:用户管理、课程管理、学校管理等
*
*/
- String module() default "";
+ LogModule module() default LogModule.OTHER;
/**
* 操作类型
@@ -30,7 +33,7 @@ public @interface Log {
* 例如:新增、修改、删除、查询、导出等
*
*/
- String type() default "";
+ LogOperationType type() default LogOperationType.OTHER;
/**
* 操作描述
@@ -49,54 +52,4 @@ public @interface Log {
* 默认:true
*/
boolean recordParams() default true;
-
- /**
- * 操作状态(枚举)
- */
- enum OperationType {
- /**
- * 新增
- */
- CREATE("新增"),
-
- /**
- * 修改
- */
- UPDATE("修改"),
-
- /**
- * 删除
- */
- DELETE("删除"),
-
- /**
- * 查询
- */
- QUERY("查询"),
-
- /**
- * 导出
- */
- EXPORT("导出"),
-
- /**
- * 导入
- */
- IMPORT("导入"),
-
- /**
- * 其他
- */
- OTHER("其他");
-
- private final String description;
-
- OperationType(String description) {
- this.description = description;
- }
-
- public String getDescription() {
- return description;
- }
- }
}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/aspect/LogAspect.java b/reading-platform-java/src/main/java/com/reading/platform/common/aspect/LogAspect.java
index ef01c4a..73e859a 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/common/aspect/LogAspect.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/aspect/LogAspect.java
@@ -19,7 +19,6 @@ import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
-import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
@@ -59,8 +58,9 @@ public class LogAspect {
// 构建操作日志对象
OperationLog operationLog = new OperationLog();
operationLog.setId(IdWorker.getId());
- operationLog.setModule(logAnnotation.module());
- operationLog.setAction(logAnnotation.description());
+ operationLog.setModule(logAnnotation.module().toString());
+ operationLog.setAction(logAnnotation.type().getCode());
+ operationLog.setDetails(logAnnotation.description());
operationLog.setCreatedAt(LocalDateTime.now());
// 记录操作人信息
@@ -76,6 +76,20 @@ public class LogAspect {
log.debug("获取当前用户信息失败:{}", e.getMessage());
}
+ // 设置租户 ID:超管端使用 0,其他端使用当前租户 ID
+ try {
+ Long tenantId = SecurityUtils.getCurrentTenantId();
+ if (tenantId != null) {
+ operationLog.setTenantId(tenantId);
+ } else {
+ // 超管端操作,使用 0 作为标记
+ operationLog.setTenantId(0L);
+ }
+ } catch (Exception e) {
+ // 获取租户 ID 失败(可能是超管端),使用 0 作为标记
+ operationLog.setTenantId(0L);
+ }
+
// 记录请求信息
operationLog.setIpAddress(getIpAddress(request));
operationLog.setUserAgent(request.getHeader("User-Agent"));
@@ -83,8 +97,11 @@ public class LogAspect {
// 记录请求参数
if (logAnnotation.recordParams()) {
try {
- String params = JSON.toJSONString(getRequestParams(joinPoint));
- operationLog.setDetails(params);
+ Object[] params = getRequestParams(joinPoint);
+ // 对参数进行脱敏处理(移除敏感信息)
+ Object[] sanitizedParams = desensitizeParams(joinPoint, params);
+ String paramsJson = JSON.toJSONString(sanitizedParams);
+ operationLog.setRequestParams(paramsJson);
} catch (Exception e) {
log.warn("记录请求参数失败:{}", e.getMessage());
}
@@ -192,4 +209,68 @@ public class LogAspect {
private Object[] getRequestParams(JoinPoint joinPoint) {
return joinPoint.getArgs();
}
+
+ /**
+ * 对请求参数进行脱敏处理(密码字段)
+ *
+ * 注意:DTO 类中的密码字段使用 @JsonIgnore 注解,序列化时会自动忽略
+ * 本方法主要处理 String 类型的直接参数(如 oldPassword, newPassword)
+ *
+ *
+ * @param joinPoint 切面连接点
+ * @param params 原始参数
+ * @return 脱敏后的参数
+ */
+ private Object[] desensitizeParams(JoinPoint joinPoint, Object[] params) {
+ if (params == null || params.length == 0) {
+ return params;
+ }
+
+ Object[] sanitized = new Object[params.length];
+ for (int i = 0; i < params.length; i++) {
+ if (params[i] instanceof String strParam) {
+ // 检查参数名是否包含敏感词(通过方法参数名判断)
+ String paramName = getParameterName(joinPoint, i);
+ if (isSensitiveParam(paramName)) {
+ sanitized[i] = "***";
+ } else {
+ sanitized[i] = strParam;
+ }
+ } else {
+ // DTO 对象:依赖 @JsonIgnore 注解自动脱敏
+ sanitized[i] = params[i];
+ }
+ }
+ return sanitized;
+ }
+
+ /**
+ * 获取参数名
+ */
+ private String getParameterName(JoinPoint joinPoint, int paramIndex) {
+ try {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ String[] paramNames = signature.getParameterNames();
+ if (paramNames != null && paramIndex < paramNames.length) {
+ return paramNames[paramIndex];
+ }
+ } catch (Exception e) {
+ log.debug("获取参数名失败:{}", e.getMessage());
+ }
+ return "";
+ }
+
+ /**
+ * 判断是否为敏感参数名
+ */
+ private boolean isSensitiveParam(String paramName) {
+ if (paramName == null || paramName.isEmpty()) {
+ return false;
+ }
+ String lowerName = paramName.toLowerCase();
+ return lowerName.contains("password") ||
+ lowerName.contains("passwd") ||
+ lowerName.contains("secret") ||
+ lowerName.contains("token");
+ }
}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java
index e7d9ecd..1bf9a87 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/ErrorCode.java
@@ -8,42 +8,44 @@ import lombok.Getter;
@Getter
public enum ErrorCode {
- SUCCESS(200, "success"),
- BAD_REQUEST(400, "Bad Request"),
- UNAUTHORIZED(401, "Unauthorized"),
- FORBIDDEN(403, "Forbidden"),
- NOT_FOUND(404, "Resource Not Found"),
- METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
- INTERNAL_ERROR(500, "Internal Server Error"),
+ SUCCESS(200, "成功"),
+ BAD_REQUEST(400, "请求失败"),
+ UNAUTHORIZED(401, "未授权"),
+ FORBIDDEN(403, "禁止访问"),
+ NOT_FOUND(404, "资源不存在"),
+ METHOD_NOT_ALLOWED(405, "方法不允许"),
+ INTERNAL_ERROR(500, "服务器内部错误"),
// Business Errors (1000+)
- LOGIN_FAILED(1001, "Login failed"),
- ACCOUNT_DISABLED(1002, "Account is disabled"),
- TOKEN_EXPIRED(1003, "Token expired"),
- TOKEN_INVALID(1004, "Token invalid"),
- PERMISSION_DENIED(1005, "Permission denied"),
+ LOGIN_FAILED(1001, "登录失败"),
+ ACCOUNT_NOT_FOUND(1002, "账号不存在"),
+ ACCOUNT_DISABLED(1003, "账户已停用"),
+ INCORRECT_PASSWORD(1004, "密码错误"),
+ TOKEN_EXPIRED(1005, "Token 已过期"),
+ TOKEN_INVALID(1006, "Token 无效"),
+ PERMISSION_DENIED(1007, "权限不足"),
// Data Errors (2000+)
- DATA_NOT_FOUND(2001, "Data not found"),
- DATA_ALREADY_EXISTS(2002, "Data already exists"),
- DATA_IN_USE(2003, "Data is in use"),
- INVALID_PARAMETER(2004, "Invalid parameter"),
+ DATA_NOT_FOUND(2001, "数据不存在"),
+ DATA_ALREADY_EXISTS(2002, "数据已存在"),
+ DATA_IN_USE(2003, "数据使用中"),
+ INVALID_PARAMETER(2004, "参数无效"),
// Tenant Errors (3000+)
- TENANT_NOT_FOUND(3001, "Tenant not found"),
- TENANT_EXPIRED(3002, "Tenant has expired"),
- TENANT_DISABLED(3003, "Tenant is disabled"),
+ TENANT_NOT_FOUND(3001, "租户不存在"),
+ TENANT_EXPIRED(3002, "租户已过期"),
+ TENANT_DISABLED(3003, "租户已停用"),
TENANT_SUSPENDED(3004, "您的账户因租户服务暂停而无法登录,请联系学校管理员"),
// Package Errors (3100+)
- PACKAGE_NOT_FOUND(3101, "Package not found"),
- REMOVE_PACKAGE_HAS_SCHEDULES(3102, "该套餐下有排课计划"),
+ PACKAGE_NOT_FOUND(3101, "套餐不存在"),
+ REMOVE_PACKAGE_HAS_SCHEDULES(3102, "该套餐下有排课计划,请确认是否强制移除"),
// User Errors (4000+)
- USER_NOT_FOUND(4001, "User not found"),
- USER_ALREADY_EXISTS(4002, "User already exists"),
- PASSWORD_MISMATCH(4003, "Password mismatch"),
- OLD_PASSWORD_ERROR(4004, "Old password is incorrect");
+ USER_NOT_FOUND(4001, "用户不存在"),
+ USER_ALREADY_EXISTS(4002, "用户已存在"),
+ PASSWORD_MISMATCH(4003, "密码不匹配"),
+ OLD_PASSWORD_ERROR(4004, "原密码错误");
private final Integer code;
private final String message;
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/GenericStatus.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/GenericStatus.java
index cbd8efa..25159bd 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/common/enums/GenericStatus.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/GenericStatus.java
@@ -9,8 +9,8 @@ import lombok.Getter;
@Getter
public enum GenericStatus {
- ACTIVE("ACTIVE", "激活"),
- INACTIVE("INACTIVE", "未激活"),
+ ACTIVE("ACTIVE", "启用"),
+ INACTIVE("INACTIVE", "停用"),
ARCHIVED("ARCHIVED", "归档");
private final String code;
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/LogModule.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/LogModule.java
new file mode 100644
index 0000000..af9086b
--- /dev/null
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/LogModule.java
@@ -0,0 +1,177 @@
+package com.reading.platform.common.enums;
+
+import lombok.Getter;
+
+/**
+ * 日志操作模块枚举
+ *
+ * 用于 @Log 注解的 module 参数赋值,统一管理操作日志的模块名称
+ *
+ *
+ * @author reading-platform
+ * @since 2026-03-23
+ */
+@Getter
+public enum LogModule {
+
+ /**
+ * 认证管理
+ */
+ AUTH("认证管理"),
+
+ /**
+ * 文件管理
+ */
+ FILE("文件管理"),
+
+ /**
+ * 班级管理
+ */
+ CLASS("班级管理"),
+
+ /**
+ * 教师管理
+ */
+ TEACHER("教师管理"),
+
+ /**
+ * 学生管理
+ */
+ STUDENT("学生管理"),
+
+ /**
+ * 家长管理
+ */
+ PARENT("家长管理"),
+
+ /**
+ * 排课管理
+ */
+ SCHEDULE("排课管理"),
+
+ /**
+ * 任务管理
+ */
+ TASK("任务管理"),
+
+ /**
+ * 任务模板
+ */
+ TASK_TEMPLATE("任务模板"),
+
+ /**
+ * 任务评价
+ */
+ TASK_FEEDBACK("任务评价"),
+
+ /**
+ * 套餐管理
+ */
+ PACKAGE("套餐管理"),
+
+ /**
+ * 成长记录
+ */
+ GROWTH("成长记录"),
+
+ /**
+ * 数据导出
+ */
+ EXPORT("数据导出"),
+
+ /**
+ * 通知管理
+ */
+ NOTIFICATION("通知管理"),
+
+ /**
+ * 课程套餐管理
+ */
+ COURSE_COLLECTION("课程套餐管理"),
+
+ /**
+ * 课程包管理
+ */
+ COURSE_PACKAGE("课程包管理"),
+
+ /**
+ * 课程环节管理
+ */
+ COURSE_LESSON("课程环节管理"),
+
+ /**
+ * 资源库管理
+ */
+ COURSE_RESOURCE("资源库管理"),
+
+ /**
+ * 主题管理
+ */
+ THEME("主题管理"),
+
+ /**
+ * 租户管理
+ */
+ TENANT("租户管理"),
+
+ /**
+ * 系统设置
+ */
+ SYSTEM_SETTING("系统设置"),
+
+ /**
+ * 课时管理
+ */
+ LESSON("课时管理"),
+
+ /**
+ * 课时反馈
+ */
+ LESSON_FEEDBACK("课时反馈"),
+
+ /**
+ * 学生记录
+ */
+ STUDENT_RECORD("学生记录"),
+
+ /**
+ * 阿里云 IMM 服务
+ */
+ IMM("阿里云 IMM 服务"),
+
+ /**
+ * 其他(未分类的模块)
+ */
+ OTHER("其他");
+
+ private final String description;
+
+ LogModule(String description) {
+ this.description = description;
+ }
+
+ /**
+ * 重写 toString 方法,返回描述文字
+ * 这样可以直接使用枚举值作为字符串存储到数据库
+ */
+ @Override
+ public String toString() {
+ return this.description;
+ }
+
+ /**
+ * 根据 description 获取枚举
+ *
+ * @param description 模块描述
+ * @return 日志操作模块枚举
+ */
+ public static LogModule fromDescription(String description) {
+ for (LogModule module : values()) {
+ if (module.description.equals(description)) {
+ return module;
+ }
+ }
+ // 找不到返回 null,表示未知的模块
+ return null;
+ }
+}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/LogOperationType.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/LogOperationType.java
new file mode 100644
index 0000000..23079c1
--- /dev/null
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/LogOperationType.java
@@ -0,0 +1,74 @@
+package com.reading.platform.common.enums;
+
+import lombok.Getter;
+
+/**
+ * 日志操作类型枚举
+ *
+ * 用于 @Log 注解的 type 参数赋值
+ *
+ *
+ * @author reading-platform
+ * @since 2026-03-23
+ */
+@Getter
+public enum LogOperationType {
+
+ /**
+ * 新增
+ */
+ CREATE("CREATE", "新增"),
+
+ /**
+ * 修改
+ */
+ UPDATE("UPDATE", "修改"),
+
+ /**
+ * 删除
+ */
+ DELETE("DELETE", "删除"),
+
+ /**
+ * 查询
+ */
+ QUERY("QUERY", "查询"),
+
+ /**
+ * 导出
+ */
+ EXPORT("EXPORT", "导出"),
+
+ /**
+ * 导入
+ */
+ IMPORT("IMPORT", "导入"),
+
+ /**
+ * 其他
+ */
+ OTHER("OTHER", "其他");
+
+ private final String code;
+ private final String description;
+
+ LogOperationType(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ /**
+ * 根据 code 获取枚举
+ *
+ * @param code 操作类型码
+ * @return 日志操作类型枚举
+ */
+ public static LogOperationType fromCode(String code) {
+ for (LogOperationType type : values()) {
+ if (type.code.equals(code)) {
+ return type;
+ }
+ }
+ return OTHER;
+ }
+}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/UserRole.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/UserRole.java
index 28d7c3a..5913192 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/common/enums/UserRole.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/UserRole.java
@@ -22,8 +22,11 @@ public enum UserRole {
}
public static UserRole fromCode(String code) {
+ if (code == null) {
+ throw new IllegalArgumentException("Role code cannot be null");
+ }
for (UserRole role : values()) {
- if (role.getCode().equals(code)) {
+ if (role.getCode().equalsIgnoreCase(code)) {
return role;
}
}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/response/Result.java b/reading-platform-java/src/main/java/com/reading/platform/common/response/Result.java
index ce8926c..e035291 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/common/response/Result.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/response/Result.java
@@ -95,6 +95,7 @@ public class Result implements Serializable {
return result;
}
+
/**
* 错误响应(默认 500)
*/
diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java
index 4b64a00..e3c7df1 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/controller/AuthController.java
@@ -1,5 +1,8 @@
package com.reading.platform.controller;
+import com.reading.platform.common.annotation.Log;
+import com.reading.platform.common.enums.LogModule;
+import com.reading.platform.common.enums.LogOperationType;
import com.reading.platform.common.mapper.AdminUserMapper;
import com.reading.platform.common.mapper.ParentMapper;
import com.reading.platform.common.mapper.TenantMapper;
@@ -37,12 +40,14 @@ public class AuthController {
private final AdminUserMapper adminUserMapper;
@Operation(summary = "用户登录")
+ @Log(module = LogModule.AUTH, type = LogOperationType.OTHER, description = "用户登录")
@PostMapping("/login")
public Result login(@Valid @RequestBody LoginRequest request) {
return Result.success(authService.login(request));
}
@Operation(summary = "用户登出")
+ @Log(module = LogModule.AUTH, type = LogOperationType.OTHER, description = "用户登出")
@PostMapping("/logout")
public Result logout() {
authService.logout();
@@ -64,6 +69,7 @@ public class AuthController {
}
@Operation(summary = "修改密码")
+ @Log(module = LogModule.AUTH, type = LogOperationType.UPDATE, description = "修改密码", recordParams = false)
@PostMapping("/change-password")
public Result changePassword(
@RequestParam String oldPassword,
@@ -75,6 +81,7 @@ public class AuthController {
}
@Operation(summary = "修改个人信息")
+ @Log(module = LogModule.AUTH, type = LogOperationType.UPDATE, description = "修改个人信息")
@PutMapping("/profile")
public Result updateProfile(
@Valid @RequestBody UpdateProfileRequest request) {
diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/FileUploadController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/FileUploadController.java
index fad3a49..c37161f 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/controller/FileUploadController.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/controller/FileUploadController.java
@@ -1,5 +1,8 @@
package com.reading.platform.controller;
+import com.reading.platform.common.annotation.Log;
+import com.reading.platform.common.enums.LogModule;
+import com.reading.platform.common.enums.LogOperationType;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.util.OssUtils;
import com.reading.platform.dto.response.OssTokenVo;
@@ -47,6 +50,7 @@ public class FileUploadController {
@PostMapping("/upload")
@Operation(summary = "上传文件")
+ @Log(module = LogModule.FILE, type = LogOperationType.CREATE, description = "上传文件")
public Result