后端注释添加日期人员划分

This commit is contained in:
chenhaodong
2026-06-18 22:44:37 +08:00
parent 1c0b377474
commit 5f99b138cb
104 changed files with 1371 additions and 430 deletions
@@ -15,7 +15,9 @@ import java.net.UnknownHostException;
import java.util.List;
/**
* 启动类
* @Description 描述:应用启动类,Spring Boot主入口及环境初始化
* @Author A贾宇婷034244310
* @Date 20260615
*/
@Log4j2
@SpringBootApplication
@@ -24,14 +24,27 @@ import java.util.List;
/**
* Shiro自定义Realm实现
* <p>
* 继承AuthorizingRealm,实现两个核心职责:
* 1. 认证(Authentication):通过doGetAuthenticationInfo校验用户JWT Token的合法性
* 2. 授权(Authorization):通过doGetAuthorizationInfo加载用户的角色和权限信息
* </p>
* <p>
* 当JwtFilter调用Subject.login()时,Shiro框架会自动调用本类中的认证和授权方法。
* </p>
*
* @Description 描述:用户登录鉴权和获取用户授权
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
/**
* 使用@Lazy延迟注入,避免Shiro在Spring容器初始化阶段过早依赖Service Bean导致循环依赖
*/
@Autowired
@Lazy
private SysUserService sysUserService;
@@ -41,6 +54,16 @@ public class ShiroRealm extends AuthorizingRealm {
private SysUserRoleService sysUserRoleService;
/**
* 声明当前Realm支持的Token类型
* <p>
* Shiro框架通过此方法判断是否将Token交给当前Realm处理。
* 仅当Token为JwtToken类型时,才会调用本Realm的认证/授权方法。
* </p>
*
* @param token Shiro认证令牌
* @return true-当前Realm支持该Token类型;false-不支持
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
@@ -48,22 +71,29 @@ public class ShiroRealm extends AuthorizingRealm {
/**
* 详细授权认证
* @param principals
* @return
* 授权认证:加载当前用户的角色和权限信息
* <p>
* 当请求中使用了@RequiresRoles、@RequiresPermissions等Shiro注解时,
* Shiro框架会自动调用此方法获取用户的授权信息,用于后续的权限判断。
* </p>
*
* @param principals 用户主体集合,包含认证成功后存储的用户信息(SysUserLoginDTO
* @return 包含用户角色和权限的授权信息对象
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String userId = null;
if (principals != null) {
// 从主体集合中获取认证时存储的用户登录信息
SysUserLoginDTO user = (SysUserLoginDTO) principals.getPrimaryPrincipal();
userId = user.getId();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 查找用户角色
// 根据用户ID查询该用户拥有的所有角色列表
List<String> roles = sysUserRoleService.listRoles(userId);
// 将角色列表设置到授权信息中,供Shiro进行角色权限校验
info.setRoles(new HashSet<>(roles));
log.info("++++++++++校验详细权限完成");
@@ -71,47 +101,65 @@ public class ShiroRealm extends AuthorizingRealm {
}
/**
* 校验用户的账号密码是否正确
* @param auth
* @return
* @throws AuthenticationException
* 身份认证:校验用户提交的JWT Token是否合法
* <p>
* 当JwtFilter调用Subject.login(jwtToken)时,Shiro框架会回调此方法。
* 方法内部提取Token中的用户信息并校验Token有效性,
* 返回的SimpleAuthenticationInfo中principal为用户对象,credentials为原始Token字符串。
* </p>
*
* @param auth 认证令牌,包含用户提交的JWT Token(通过getCredentials()获取)
* @return 认证信息对象,包含用户主体(principal)和凭证(credentials
* @throws AuthenticationException 当Token为空或校验失败时抛出
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
// 从认证令牌中提取JWT Token字符串(JwtToken的getCredentials()返回的是Token字符串)
String token = (String) auth.getCredentials();
if (token == null) {
throw new AuthenticationException("token为空!");
}
// 校验token有效性
// 校验Token有效性:解析用户名、查询用户信息、验证Token签名和过期时间
SysUserLoginDTO user = this.checkToken(token);
// 构造认证信息:user作为principal(后续可通过Subject.getPrincipal()获取),
// token作为credentialsgetName()返回当前Realm名称
return new SimpleAuthenticationInfo(user, token, getName());
}
/**
* 校验Token的有效性
* @param token
* @return
* @throws AuthenticationException
* 校验JWT Token的有效性
* <p>
* Token校验流程分为三步:
* 1. 从Token中解析用户名(JWT Payload部分)
* 2. 根据用户名从数据库中查询用户信息
* 3. 验证Token的签名和过期时间
* 任一步骤失败都会抛出AuthenticationException异常。
* </p>
*
* @param token JWT Token字符串
* @return 校验通过后返回的用户登录信息DTO
* @throws AuthenticationException 当Token无效、用户不存在或Token已过期时抛出
*/
public SysUserLoginDTO checkToken(String token) throws AuthenticationException {
// 查询用户信息
log.debug("++++++++++校验用户token "+ token);
// 从token中获取用户名
// 第一步:从JWT Token的Payload中解析出用户名
String username = JwtUtils.getUsername(token);
log.debug("++++++++++用户名: "+ username);
// 解析失败说明Token格式不正确或已损坏
if (username == null) {
throw new AuthenticationException("无效的token");
}
// 查找登录用户对象
// 第二步:根据用户名从数据库中查找对应的用户登录信息
SysUserLoginDTO user = sysUserService.token(token);
// 校验token是否失效
// 第三步:校验Token的签名完整性和是否已过期
if (!JwtUtils.verify(token, username)) {
throw new AuthenticationException("登陆失效,请重试登陆!");
}
@@ -122,8 +170,13 @@ public class ShiroRealm extends AuthorizingRealm {
/**
* 清除当前用户的权限认证缓存
* @param principals
* 清除指定用户的认证/授权缓存
* <p>
* 当用户信息发生变更(如角色调整、权限修改)时,可调用此方法
* 清除该用户在Shiro中的缓存,使下次请求时重新执行认证和授权逻辑。
* </p>
*
* @param principals 待清除缓存的用户主体标识集合
*/
@Override
public void clearCache(PrincipalCollection principals) {
@@ -14,42 +14,70 @@ import javax.servlet.http.HttpServletResponse;
/**
* JWT认证过滤器
* <p>
* 作为Shiro过滤链中的核心认证组件,拦截所有需要鉴权的HTTP请求。
* 工作流程:从请求头中提取JWT Token -> 封装为JwtToken对象 -> 提交给ShiroRealm进行认证。
* 继承BasicHttpAuthenticationFilter以融入Shiro的过滤器链机制。
* </p>
*
* @Description 描述:鉴权登录拦截器
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 执行登录认证
* @param request
* @param response
* @param mappedValue
* @return
* 判断当前请求是否允许访问(Shiro过滤器链的入口方法)
* <p>
* Shiro在处理每个请求时会调用此方法来决定是否放行。
* 内部调用executeLogin完成JWT认证,认证成功则放行,失败则返回统一错误响应。
* </p>
*
* @param request 当前HTTP请求对象
* @param response 当前HTTP响应对象
* @param mappedValue 过滤器链中配置的权限标识(如perms、roles等),此处未使用
* @return true-认证通过,允许访问;false-认证失败,拒绝访问
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
// 尝试执行JWT登录认证流程
executeLogin(request, response);
return true;
} catch (Exception e) {
// 写出统一错误信息
// 认证失败时,向客户端写出统一错误响应信息(如401未授权)
InjectUtils.restError((HttpServletResponse) response);
return false;
}
}
/**
* 执行JWT登录认证
* <p>
* 从HTTP请求头中提取JWT Token,封装为Shiro可识别的JwtToken对象,
* 然后提交给Shiro的Subject进行登录。Shiro会将JwtToken传递给ShiroRealm的
* doGetAuthenticationInfo方法完成实际的身份校验。
* </p>
*
* @param request 当前HTTP请求对象,用于获取Token请求头
* @param response 当前HTTP响应对象
* @return true-登录成功;若登录失败则抛出异常由上层捕获
* @throws Exception 当Token无效或认证失败时抛出异常
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 从请求头中获取JWT TokenToken字段名定义在Constant.TOKEN常量中)
String token = httpServletRequest.getHeader(Constant.TOKEN);
// 将原始Token字符串封装为Shiro的认证令牌对象
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
// 提交给ShiroRealm进行身份认证,如果Token无效,Realm会抛出AuthenticationException
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
// 如果没有抛出异常则代表认证成功,返回true放行
return true;
}
}
@@ -4,9 +4,9 @@ import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @Description 描述:
* @Description 描述:* @Author A贾宇婷034244310
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
public class JwtToken implements AuthenticationToken {
@@ -13,7 +13,7 @@ import java.util.Date;
/**
* @Description 描述:JWT工具类
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
public class JwtUtils {
@@ -25,9 +25,26 @@ import java.util.List;
/**
* 数据字典翻译AOP切面
* <p>
* 拦截所有Controller方法的返回结果,自动将带有@Dict注解的字段从字典编码翻译为可读文本。
* 翻译后的文本以"字段名_dictText"的形式追加到返回的JSON对象中。
* 同时处理Date类型字段的格式化输出。
* </p>
* <p>
* 支持的数据结构:
* - 单个对象:直接遍历字段进行翻译
* - 列表(List):遍历每个元素进行翻译
* - 分页结果(IPage):遍历分页记录进行翻译
* - 嵌套List字段:递归处理子列表中的字典翻译
* </p>
* <p>
* 注意:当前@Component注解被注释,如需启用字典翻译功能需取消注释或通过其他方式注册为Spring Bean。
* </p>
*
* @Description 描述:数据字典AOP类,处理数据字典值
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260616
*/
@Aspect
//@Component
@@ -35,14 +52,19 @@ import java.util.List;
public class DictAspect {
/** 字典服务,用于根据字典编码查询对应的可读文本 */
@Autowired
private SysDictService sysDictService;
/**
* 切入Controller执行
* @param pjp
* @return
* @throws Throwable
* 环绕通知:切入所有Controller方法执行前后
* <p>
* 切点表达式匹配com.bc.exam包下所有Controller类的public方法,
* 在方法执行完成后对返回结果进行字典翻译处理。
* </p>
* @param pjp 连接点对象,封装了目标方法的执行信息
* @return 经过字典翻译处理后的返回结果
* @throws Throwable 目标方法执行过程中抛出的异常
*/
@Around("execution(public * com.bc.exam..*.*Controller.*(..))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
@@ -50,25 +72,30 @@ public class DictAspect {
}
/**
* 进行翻译并返回,调用前必须实现:BaseDictService
*
* @param pjp
* @return
* @throws Throwable
* 执行目标方法并对返回结果进行字典翻译
* <p>
* 调用前必须确保SysDictService已正确配置并注入。
* </p>
* @param pjp 连接点对象
* @return 翻译处理后的返回结果
* @throws Throwable 目标方法执行异常
*/
public Object translate(ProceedingJoinPoint pjp) throws Throwable {
// 处理字典
// 先执行目标方法获取返回结果,再对结果进行字典翻译处理
return this.parseAllDictText(pjp.proceed());
}
/**
* 转换全部数据字典
*
* @param result
* <p>
* 仅对ApiRest类型的返回值进行字典翻译处理,其他类型直接透传。
* </p>
* @param result Controller方法的返回结果
* @return 处理后的返回结果(ApiRest类型会被翻译,其他类型原样返回)
*/
private Object parseAllDictText(Object result) {
// 非ApiRest类型处理
// 仅ApiRest统一响应格式才进行字典翻译,非ApiRest类型直接返回不做处理
if (result instanceof ApiRest) {
parseFullDictText(result);
}
@@ -78,45 +105,53 @@ public class DictAspect {
/**
* 转换所有类型的数据字典、包含子列表
*
* @param result
* 解析并翻译ApiRest响应体中的所有数据字典字段
* <p>
* 根据data字段的实际类型分三种情况处理:
* 1. 分页对象(IPage):遍历分页记录逐条翻译
* 2. 列表对象(List):遍历列表元素逐条翻译
* 3. 单个对象:直接进行字段翻译
* 基本数据类型和null值不做处理。
* </p>
* @param result ApiRest统一响应对象
*/
private void parseFullDictText(Object result) {
try {
// 从ApiRest中提取实际业务数据
Object rest = ((ApiRest) result).getData();
// 不处理普通数据类型
// null值或基本数据类型(String、Integer等)无需字典翻译,直接跳过
if (rest == null || this.isBaseType(rest.getClass())) {
return;
}
// 分页的
// 处理分页查询结果:遍历分页记录(records)逐条翻译字典字段
if (rest instanceof IPage) {
List<Object> items = new ArrayList<>(16);
for (Object record : ((IPage) rest).getRecords()) {
Object item = this.parseObject(record);
items.add(item);
}
// 用翻译后的记录替换原始分页记录
((IPage) rest).setRecords(items);
return;
}
// 数据列表的
// 处理列表查询结果:遍历列表中每个元素进行字典翻译
if (rest instanceof List) {
List<Object> items = new ArrayList<>();
for (Object record : ((List) rest)) {
Object item = this.parseObject(record);
items.add(item);
}
// 重新回写值
// 将翻译后的列表重新写回ApiRest的data字段
((ApiRest) result).setData(items);
return;
}
// 处理单对象
// 处理单对象:直接对对象的字段进行字典翻译
Object item = this.parseObject(((ApiRest) result).getData());
((ApiRest) result).setData(item);
@@ -126,10 +161,17 @@ public class DictAspect {
}
/**
* 处理数据字典值
*
* @param record
* @return
* 处理单个对象的字典翻译和日期格式化
* <p>
* 处理流程:
* 1. 将对象序列化为JSON再解析为JSONObject,便于动态添加翻译字段
* 2. 遍历对象所有字段,按类型分别处理:
* - List类型字段:递归调用processList处理嵌套列表
* - 带@Dict注解的字段:查询字典服务翻译编码为可读文本,结果以"字段名_dictText"存储
* - Date类型字段:按@JsonFormat注解指定的格式或默认格式(yyyy-MM-dd HH:mm:ss)格式化
* </p>
* @param record 待处理的业务对象
* @return 处理后的JSONObject(包含字典翻译字段和格式化日期),null输入则返回null
*/
public Object parseObject(Object record) {
@@ -137,18 +179,19 @@ public class DictAspect {
return null;
}
// 不处理普通数据类型
// 基本数据类型(String、Number等)没有复杂字段,无需翻译处理
if (this.isBaseType(record.getClass())) {
return record;
}
// 转换JSON字符
// 将对象转为JSON格式处理,便于动态添加字典翻译字段(如xxx_dictText
String json = JSON.toJSONString(record);
JSONObject item = JSONObject.parseObject(json);
// 遍历对象的所有字段(包括父类字段),逐个进行翻译或格式化处理
for (Field field : Reflections.getAllFields(record)) {
// 如果是List类型
// 分支1List类型字段 - 递归处理嵌套列表中的字典翻译
if (List.class.isAssignableFrom(field.getType())) {
try {
List list = this.processList(field, item.getObject(field.getName(), List.class));
@@ -160,37 +203,39 @@ public class DictAspect {
continue;
}
// 处理普通字段
// 分支2:带@Dict注解的字段 - 从字典表查询编码对应的可读文本
if (field.getAnnotation(Dict.class) != null) {
String code = field.getAnnotation(Dict.class).dicCode();
String text = field.getAnnotation(Dict.class).dicText();
String table = field.getAnnotation(Dict.class).dictTable();
String key = String.valueOf(item.get(field.getName()));
String code = field.getAnnotation(Dict.class).dicCode(); // 字典编码字段名
String text = field.getAnnotation(Dict.class).dicText(); // 字典文本字段名
String table = field.getAnnotation(Dict.class).dictTable(); // 字典表名
String key = String.valueOf(item.get(field.getName())); // 当前字段的字典编码值
//翻译字典值对应的txt
// 调用字典服务将编码值翻译为可读文本
String textValue = this.translateDictValue(code, text, table, key);
if (StringUtils.isEmpty(textValue)) {
textValue = "";
}
// 翻译结果以"字段名_dictText"的命名规则追加到JSON对象中
item.put(field.getName() + "_dictText", textValue);
continue;
}
//日期格式转换
// 分支3:Date类型字段 - 按注解格式或默认格式进行日期格式
if (field.getType().getName().equals("java.util.Date") && item.get(field.getName()) != null) {
// 获取注解
// 获取字段上的@JsonFormat注解以确定日期格式
JsonFormat ann = field.getAnnotation(JsonFormat.class);
// 格式化方式
// 日期格式化
SimpleDateFormat fmt;
// 使用注解指定的
// 优先使用@JsonFormat注解指定的日期格式
if (ann != null && !StringUtils.isEmpty(ann.pattern())) {
fmt = new SimpleDateFormat(ann.pattern());
} else {
// 默认时间样式
// 未指定格式时使用默认时间样式
fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
// 将时间戳(Long)格式化为日期字符串
item.put(field.getName(), fmt.format(new Date((Long) item.get(field.getName()))));
continue;
@@ -201,49 +246,55 @@ public class DictAspect {
}
/**
* 获得类型为List的值
*
* @param field
* @return
* 处理List类型字段的字典翻译
* <p>
* 通过反射获取List的泛型参数确定元素实际类型,
* 然后将每个列表元素反序列化为实际类型后递归调用parseObject进行字典翻译。
* 基本数据类型的列表(如List&lt;String&gt;)无需处理直接返回。
* </p>
* @param field List类型的字段对象,用于获取泛型类型信息
* @param list 字段对应的列表数据
* @return 经过字典翻译处理的列表,若输入为空则返回空列表
*/
private List<Object> processList(Field field, List list) {
// 空判断
// 空列表或null直接返回空集合,避免后续空指针
if (list == null || list.size() == 0) {
return new ArrayList<>();
}
// 获得List属性的真实类
// 通过泛型反射获取List元素的实际数据类型(如List<User>中的User类)
Type genericType = field.getGenericType();
Class<?> actualType = null;
if (genericType instanceof ParameterizedType) {
// 尝试获取数据类型
// 从参数化类型中提取第一个泛型参数作为实际类型
ParameterizedType pt = (ParameterizedType) genericType;
try {
actualType = (Class) pt.getActualTypeArguments()[0];
}catch (Exception e){
// 泛型类型解析失败,返回原始列表不做处理
return list;
}
}
// 常规列表无需处理
// 元素为基本数据类型(如String、Integer等)的列表无需字典翻译
if (isBaseType(actualType)) {
return list;
}
// 返回列表
// 对复杂对象列表逐条进行字典翻译处理
List<Object> result = new ArrayList<>(16);
for (int i = 0; i < list.size(); i++) {
// 创建实例-->赋值-->字典处理
// 将每个元素先转为JSON再反序列化为实际类型,确保类型准确
Object data = list.get(i);
try {
data = JSON.parseObject(JSON.toJSONString(data), actualType);
}catch (Exception e){
// 转换出错不处理
// 反序列化失败时使用原始数据继续处理
}
// 处理后的数据
// 递归调用parseObject对列表中每个对象进行字典翻译
Object pds = this.parseObject(data);
result.add(pds);
}
@@ -252,22 +303,27 @@ public class DictAspect {
}
/**
* 翻译实现
*
* @param code
* @param text
* @param table
* @param key
* @return
* 字典翻译实现:根据字典表、编码字段、文本字段和key值查询对应的可读文本
* <p>
* 通过SysDictService查询指定字典表中,匹配code字段值为key的记录,
* 返回其text字段的值作为翻译结果。
* </p>
* @param code 字典编码字段名(如"code"
* @param text 字典文本字段名(如"name"),即需要返回的翻译结果字段
* @param table 字典表名(如"sys_dict"
* @param key 当前字段的字典编码值,用于匹配查询
* @return 字典翻译后的可读文本,查询失败或未找到时返回空字符串
*/
private String translateDictValue(String code, String text, String table, String key) {
// key为空时无需查询,直接返回null
if (StringUtils.isEmpty(key)) {
return null;
}
try {
// 翻译值
// 调用字典服务查询翻译文本
String dictText = null;
if (!StringUtils.isEmpty(table)) {
// 从指定字典表中,根据code字段匹配key值,返回text字段的翻译结果
dictText = sysDictService.findDict(table, text, code, key.trim());
}
@@ -275,16 +331,20 @@ public class DictAspect {
return dictText;
}
} catch (Exception e) {
// 字典查询异常时不影响主流程,打印堆栈后返回空字符串
e.printStackTrace();
}
return "";
}
/**
* 判断是否基本类型
*
* @param clazz
* @return
* 判断给定类型是否基本类型(无需字典翻译的简单类型)
* <p>
* 基本类型包括:8种包装类型(Integer/Byte/Long/Double/Float/Character/Short/Boolean)、
* String类型以及Number类型。这些类型不包含@Dict注解字段,无需进行字典翻译处理。
* </p>
* @param clazz 待判断的类
* @return true表示是基本类型无需翻译,false表示是复杂对象需要翻译
*/
private boolean isBaseType(Class clazz) {
@@ -26,61 +26,95 @@ import java.util.Properties;
/**
* @Description 描述:查询拦截器,用于拦截处理通用的信息、如用户ID、多租户信息等;
* 特别注意:此处继承了PaginationInterceptor分页,分页必须在拦截数据后执行,否则容易出现分页不准确,分页计数大于实际数量等问题
* MyBatis查询拦截器
* <p>
* 继承自MyBatis-Plus的PaginationInterceptor,在提供分页能力的同时扩展了自定义查询条件注入功能。
* 核心作用:拦截所有SELECT查询,将SQL中的{{userId}}占位符自动替换为当前登录用户的ID,
* 实现数据权限的自动过滤,无需在业务代码中手动拼接用户ID条件。
* </p>
* <p>
* 工作原理:
* 1. 通过@Intercepts注解拦截StatementHandler的prepare方法(SQL准备阶段)
* 2. 获取原始SQL,检查是否包含{{userId}}占位符
* 3. 若包含占位符,通过Shiro获取当前登录用户ID并进行替换
* 4. 将替换后的SQL回写到BoundSql中,再交由父类PaginationInterceptor处理分页逻辑
* </p>
* @see PaginationInterceptor MyBatis-Plus分页拦截器(父类)
*
* @Author A贾宇婷034244310
* @Date 20260615
* @Description 描述:查询拦截器,用于拦截处理通用的信息、如用户ID、多租户信息等;
* @Author B吉柯梦034244314
* @Date 20260616
*/
@Log4j2
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),})
public class QueryInterceptor extends PaginationInterceptor implements Interceptor {
/**
* 客户ID
*/
/** SQL中用户ID占位符标识,在Mapper XML的SQL语句中使用此占位符实现自动用户过滤 */
private static final String USER_FILTER = "{{userId}}";
/**
* 拦截MyBatis的SQL准备阶段,对SELECT查询自动注入用户ID过滤条件
* <p>
* 处理流程:
* 1. 通过MetaObject反射获取MappedStatement和BoundSql
* 2. 仅对SELECT类型的SQL进行处理
* 3. 检查SQL中是否包含{{userId}}占位符,不包含则跳过
* 4. 替换占位符后回写SQL,再交由父类处理分页
* </p>
* @param invocation MyBatis拦截器调用对象,包含目标方法及其参数
* @return SQL执行结果
* @throws Throwable 拦截过程中可能抛出的异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//通过MetaObject优雅访问对象的属性,这里是访问statementHandler属性
// 通过MetaObject反射机制访问StatementHandler内部属性
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
//先拦截到RoutingStatementHandler,里面有个StatementHandler类型的delegate变量,其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
// 获取delegateRoutingStatementHandler)中的MappedStatement,包含SQL元信息
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
//sql语句类型
// 获取SQL语句类型(SELECT/INSERT/UPDATE/DELETE
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
// 只过滤查询的
// 仅对SELECT查询语句进行用户ID占位符替换处理
if (SqlCommandType.SELECT == sqlCommandType) {
// 获原始SQL
// 获原始SQL语句
String sql = statementHandler.getBoundSql().getSql();
// 处理
// 如果SQL中不包含{{userId}}占位符,则无需处理,直接交给父类(分页拦截器)处理
if(!sql.contains(USER_FILTER)){
return super.intercept(invocation);
}
// 处理SQL语句
// 解析SQL并将{{userId}}占位符替换为当前登录用户的实际ID
String outSql = this.parseSql(sql);
// 设置SQL
// 将替换后的SQL回写到BoundSql中
metaObject.setValue("delegate.boundSql.sql", outSql);
// 再分页
// 调用父类PaginationInterceptor的intercept方法处理分页逻辑
return super.intercept(invocation);
}
// 非SELECT语句(INSERT/UPDATE/DELETE)直接放行,不做处理
return invocation.proceed();
}
/**
* 创建拦截器代理对象
* @param target 被拦截的目标对象(StatementHandler
* @return 包装后的代理对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置拦截器属性(当前未使用)
* @param properties 拦截器配置属性
*/
@Override
public void setProperties(Properties properties) {
@@ -89,57 +123,67 @@ public class QueryInterceptor extends PaginationInterceptor implements Intercept
/**
* 获取当前登录用户
* @return
* 从Shiro安全框架中获取当前登录用户信息
* @return 当前登录用户的DTO对象,未登录或获取失败时返回null
*/
private SysUserLoginDTO getLoginUser() {
try {
// 通过Shiro的Subject获取当前登录用户的Principal(用户信息)
return SecurityUtils.getSubject().getPrincipal() != null ? (SysUserLoginDTO) SecurityUtils.getSubject().getPrincipal() : null;
} catch (Exception e) {
// 获取用户信息异常(如未登录状态)时返回null
return null;
}
}
/**
* 替换用户ID
* @param sql
* @return
* 将SQL中的{{userId}}占位符替换为当前登录用户的实际ID
* @param sql 包含{{userId}}占位符的原始SQL
* @return 替换用户ID后的SQL,用户ID为空时返回null
*/
private String processUserId(String sql) {
// 当前用户
// 获取当前登录用户并提取用户ID
SysUserLoginDTO user = this.getLoginUser();
String userId = user.getId();
if(StringUtils.isNotBlank(userId)){
// 将SQL中所有{{userId}}占位符替换为实际的用户ID
return sql.replace(USER_FILTER, userId);
}
return null;
}
/**
* 处理注入用户信息
* @param src
* @return
* 解析SQL并注入用户过滤信息
* <p>
* 使用JSqlParser解析SQL语句,在处理完用户ID替换后重新生成SQL字符串。
* 解析失败时返回原始SQL,确保不影响正常查询执行。
* </p>
* @param src 待处理的原始SQL语句
* @return 注入用户信息后的SQL语句,解析异常时返回原始SQL
*/
private String parseSql(String src) {
// 使用JSqlParser库解析SQL语句
CCJSqlParserManager parserManager = new CCJSqlParserManager();
try {
// 将SQL字符串解析为AST(抽象语法树)
Select select = (Select) parserManager.parse(new StringReader(src));
PlainSelect selectBody = (PlainSelect) select.getSelectBody();
// 过滤客户
// 将AST转回SQL字符串(JSqlParser可能会规范化SQL格式)
String sql = selectBody.toString();
// 过滤用户ID
// 将{{userId}}占位符替换为当前登录用户ID
sql = this.processUserId(sql);
// 获得SQL
// 返回处理完成的SQL
return sql;
} catch (Exception e) {
// SQL解析失败时打印异常,返回原始SQL保证查询不中断
e.printStackTrace();
}
@@ -17,9 +17,25 @@ import java.util.Objects;
import java.util.Properties;
/**
* MyBatis数据更新拦截器 - 自动填充时间字段
* <p>
* 拦截MyBatis的所有INSERT和UPDATE操作,自动为实体对象中的时间字段赋值:
* - INSERT操作:同时自动填充createTime(创建时间)和updateTime(更新时间)
* - UPDATE操作:仅自动填充updateTime(更新时间)
* </p>
* <p>
* 该拦截器通过反射机制扫描实体对象的所有字段(包括父类字段),
* 当字段名匹配"createTime"或"updateTime"时自动赋值当前时间戳,
* 避免在业务代码中手动设置时间字段,减少重复代码。
* </p>
* <p>
* 使用方式:实体类中声明createTime和updateTime字段即可自动生效,
* 字段类型需为java.sql.Timestamp或兼容类型。
* </p>
*
* @Description 描述:自动给创建时间和更新时间加值
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260616
*/
@Intercepts(value = {@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class UpdateInterceptor extends AbstractSqlParserHandler implements Interceptor {
@@ -33,15 +49,29 @@ public class UpdateInterceptor extends AbstractSqlParserHandler implements Inter
*/
private static final String UPDATE_TIME = "updateTime";
/**
* 拦截MyBatis的Executor.update方法,自动填充实体对象的时间字段
* <p>
* 处理逻辑:
* 1. 获取SQL操作类型(INSERT或UPDATE)和操作的实体对象
* 2. 通过反射获取实体所有字段(包括父类继承的字段)
* 3. 遍历字段,匹配createTime和updateTime并按规则赋值当前时间戳
* </p>
* @param invocation MyBatis拦截器调用对象
* @return 继续执行后续拦截器或实际SQL操作的结果
* @throws Throwable 反射操作或SQL执行过程中可能抛出的异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取MappedStatement,包含SQL操作类型等元信息
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// SQL操作命令
// 获取SQL操作类型(INSERT/UPDATE/DELETE
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
// 获取新增或修改的对象参数
// 获取当前操作的实体对象参数
Object parameter = invocation.getArgs()[1];
// 获取对象中所有的私有成员变量(对应表字段
// 通过反射获取实体类的所有声明字段
Field[] declaredFields = parameter.getClass().getDeclaredFields();
// 同时获取父类的字段,确保继承的createTime/updateTime也能被处理
if (parameter.getClass().getSuperclass() != null) {
Field[] superField = parameter.getClass().getSuperclass().getDeclaredFields();
declaredFields = ArrayUtils.addAll(declaredFields, superField);
@@ -50,23 +80,36 @@ public class UpdateInterceptor extends AbstractSqlParserHandler implements Inter
String fieldName = null;
for (Field field : declaredFields) {
fieldName = field.getName();
// 处理createTime字段:仅在INSERT操作时自动填充创建时间
if (Objects.equals(CREATE_TIME, fieldName)) {
if (SqlCommandType.INSERT.equals(sqlCommandType)) {
field.setAccessible(true);
// 设置创建时间为当前时间戳
field.set(parameter, new Timestamp(System.currentTimeMillis()));
}
}
// 处理updateTime字段:INSERT和UPDATE操作时都自动填充更新时间
if (Objects.equals(UPDATE_TIME, fieldName)) {
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
field.setAccessible(true);
// 设置更新时间为当前时间戳
field.set(parameter, new Timestamp(System.currentTimeMillis()));
}
}
}
// 时间字段填充完毕,继续执行后续拦截器链或实际的SQL操作
return invocation.proceed();
}
/**
* 创建拦截器代理对象
* <p>
* 仅对Executor类型的目标对象进行代理包装,其他类型直接返回原对象。
* </p>
* @param target 被拦截的目标对象
* @return Executor类型返回代理对象,其他类型返回原对象
*/
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
@@ -75,6 +118,10 @@ public class UpdateInterceptor extends AbstractSqlParserHandler implements Inter
return target;
}
/**
* 设置拦截器属性(当前未使用)
* @param properties 拦截器配置属性
*/
@Override
public void setProperties(Properties properties) {
}
@@ -13,8 +13,8 @@ import java.lang.reflect.Field;
/**
* @Description 描述:注入工具类 写出统一错误信息
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260616
*/
@Log4j2
@Component
@@ -11,24 +11,47 @@ import org.springframework.web.filter.CorsFilter;
/**
* 跨域资源共享(CORS)全局配置类
* <p>
* 通过注册CorsFilter过滤器,统一处理所有HTTP请求的跨域问题。
* 将过滤器优先级设置为最高(HIGHEST_PRECEDENCE),确保跨域处理在其他过滤器之前执行,
* 避免预检请求(OPTIONS)被拦截导致跨域失败。
* </p>
*
* @Description 描述:网关全局设置,允许跨域
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Configuration
public class CorsConfig {
/**
* 注册CORS跨域过滤器
* <p>
* 配置允许所有来源、所有请求头、所有请求方法的跨域访问,
* 并将过滤器优先级设为最高,保证跨域请求能被正确处理。
* </p>
* @return 配置完成的过滤器注册Bean
*/
@Bean
public FilterRegistrationBean corsFilter() {
// 创建基于URL路径匹配的CORS配置源
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 创建CORS配置对象
CorsConfiguration config = new CorsConfiguration();
// 允许携带凭证信息(如Cookie、Authorization头),前端需配合设置withCredentials
config.setAllowCredentials(true);
// 允许所有来源域名访问(生产环境建议限制为具体域名)
config.addAllowedOrigin(CorsConfiguration.ALL);
// 允许所有请求头通过
config.addAllowedHeader(CorsConfiguration.ALL);
// 允许所有HTTP方法(GET、POST、PUT、DELETE等)
config.addAllowedMethod(CorsConfiguration.ALL);
// 将CORS配置应用到所有路径(/**匹配所有URL)
source.registerCorsConfiguration("/**", config);
// 将CorsFilter包装为FilterRegistrationBean以便注册到Spring容器
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
// 设置过滤器优先级为最高,确保跨域处理优先于其他过滤器(如Shiro认证)执行
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
@@ -8,26 +8,47 @@ import org.springframework.context.annotation.Configuration;
/**
* MyBatis框架全局配置类
* <p>
* 负责配置MyBatis的核心组件:
* 1. 通过@MapperScan注解自动扫描指定包路径下的Mapper接口,避免逐个注册
* 2. 注册查询拦截器(QueryInterceptor),用于自动填充通用查询条件(如用户ID过滤)
* 3. 注册更新拦截器(UpdateInterceptor),用于自动填充createTime和updateTime字段
* </p>
* <p>注意:拦截器的注册顺序很重要,QueryInterceptor继承自分页拦截器,需要优先注册以保证分页逻辑正常执行。</p>
*
* @Description 描述:Mybatis过滤器配置 注意:必须按顺序进行配置,否则容易出现业务异常
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Configuration
@MapperScan("com.bc.exam.modules.**.mapper")
@MapperScan("com.bc.exam.modules.**.mapper") // 自动扫描所有模块下的mapper包,注册为MyBatis Mapper接口
public class MybatisConfig {
/**
* 数据查询过滤
* 注册数据查询拦截
* <p>
* 该拦截器继承自MyBatis-Plus的分页拦截器(PaginationInterceptor),
* 同时扩展了自定义的查询条件自动填充功能(如将SQL中的{{userId}}占位符替换为当前登录用户ID)。
* limit设置为-1表示默认不限制查询条数,由业务层自行控制分页。
* </p>
* @return 配置完成的查询拦截器实例
*/
@Bean
public QueryInterceptor queryInterceptor() {
QueryInterceptor query = new QueryInterceptor();
// 设置默认查询条数限制为-1(不限制),实际分页由业务层Page对象控制
query.setLimit(-1L);
return query;
}
/**
* 插入数据过滤
* 注册数据更新拦截
* <p>
* 拦截所有INSERT和UPDATE操作,自动为实体对象的createTime和updateTime字段赋值。
* INSERT时同时设置createTime和updateTimeUPDATE时仅设置updateTime。
* </p>
* @return 配置完成的更新拦截器实例
*/
@Bean
public UpdateInterceptor updateInterceptor() {
@@ -19,7 +19,7 @@ import java.util.concurrent.ThreadPoolExecutor;
/**
* @Description 描述:任务调度配置
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Log4j2
@Configuration
@@ -24,28 +24,40 @@ import java.util.Map;
/**
* @Description 描述:Shiro配置类
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Slf4j
@Configuration
public class ShiroConfig {
/**
* Filter Chain定义说明
*
* 配置Shiro过滤器链
* <p>
* Filter Chain定义说明:
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如permsroles
* 3、部分过滤器可指定参数,如permsroles
* </p>
* <p>
* 配置思路:先定义不需要认证的白名单路径(anon),
* 再对剩余所有路径(/**)统一使用自定义的JwtFilter进行JWT认证。
* LinkedHashMap保证路径匹配按配置顺序进行。
* </p>
*
* @param securityManager Shiro安全管理器
* @return 配置完成的ShiroFilter工厂Bean
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
// 定义过滤器链映射,使用LinkedHashMap保证匹配顺序(先匹配的优先)
Map<String, String> map = new LinkedHashMap<>();
// 需要排除的一些接口
// ========== 无需认证的接口白名单(anon = anonymous匿名访问) ==========
// 用户登录、注册相关接口
map.put("/exam/api/sys/user/login", "anon");
map.put("/exam/api/sys/user/reg", "anon");
map.put("/exam/api/sys/user/quick-reg", "anon");
@@ -53,15 +65,18 @@ public class ShiroConfig {
// 获取网站基本信息
map.put("/exam/api/sys/config/detail", "anon");
// 文件读取
// 文件读取(上传文件的访问路径)
map.put("/upload/file/**", "anon");
// ========== 静态资源和文档页面排除 ==========
map.put("/", "anon");
map.put("/v2/**", "anon");
map.put("/doc.html", "anon");
// JS/CSS/HTML等前端静态资源
map.put("/**/*.js", "anon");
map.put("/**/*.css", "anon");
map.put("/**/*.html", "anon");
// 图片和文档类静态资源
map.put("/**/*.svg", "anon");
map.put("/**/*.pdf", "anon");
map.put("/**/*.jpg", "anon");
@@ -69,31 +84,50 @@ public class ShiroConfig {
map.put("/**/*.gif", "anon");
map.put("/**/*.ico", "anon");
// 字体
// 字体文件
map.put("/**/*.ttf", "anon");
map.put("/**/*.woff", "anon");
map.put("/**/*.woff2", "anon");
// Druid数据库监控页面
map.put("/druid/**", "anon");
// Swagger API文档相关路径
map.put("/swagger-ui.html", "anon");
map.put("/swagger**/**", "anon");
map.put("/webjars/**", "anon");
// 添加自己的过滤器并且取名为jwt
// ========== 注册自定义JWT过滤器并应用到所有剩余路径 ==========
// 将JwtFilter注册为名为"jwt"的自定义过滤器
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 除上述白名单路径外,所有请求都需要经过JwtFilter进行JWT认证
map.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 配置Shiro安全管理器
* <p>
* 核心配置:
* 1. 绑定自定义的ShiroRealm(负责认证和授权逻辑)
* 2. 禁用Shiro内置的Session管理(因为本项目使用无状态的JWT Token认证,
* 不依赖服务端的Session来维持用户登录状态)
* </p>
*
* @param myRealm 自定义的ShiroRealm实现,注入由Spring容器管理的ShiroRealm Bean
* @return 配置完成的Web安全管理器
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义Realm,Shiro将通过它完成用户认证和授权
securityManager.setRealm(myRealm);
// 禁用Shiro内置的Session存储,实现无状态认证
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 关闭Session持久化,每次请求都通过JWT Token重新认证,不依赖Session
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
@@ -101,24 +135,52 @@ public class ShiroConfig {
}
/**
* 下面的代码是添加注解支持
* @return
* 配置AOP自动代理创建器,启用Shiro注解支持
* <p>
* 使用CGLIB代理(proxyTargetClass=true)而非JDK动态代理,
* 以确保对没有实现接口的类也能正确代理。
* 设置AdvisorBeanNamePrefix为"_no_advisor"是为了排除不需要被自动代理的Advisor,
* 避免与Shiro的注解拦截器产生冲突。
* </p>
*
* @return AOP自动代理创建器实例
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 启用CGLIB代理,支持对普通类(非接口)的AOP代理
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
// 开启前缀过滤,排除名称以"_no_advisor"开头的Advisor
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
return defaultAdvisorAutoProxyCreator;
}
/**
* 注册Shiro生命周期处理器
* <p>
* 负责管理Shiro相关Bean的生命周期回调(如初始化init和销毁destroy方法),
* 确保Shiro的SecurityManager等核心组件能够正确地初始化和释放资源。
* </p>
*
* @return Shiro生命周期Bean后处理器
*/
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 配置Shiro注解拦截器(授权属性源Advisor)
* <p>
* 使Shiro的安全注解(如@RequiresRoles、@RequiresPermissions、@RequiresAuthentication等)
* 能够生效。该Advisor会拦截带有安全注解的方法,在方法执行前进行权限校验。
* </p>
*
* @param securityManager Shiro安全管理器,注解拦截器需要通过它获取当前用户的授权信息
* @return 授权属性源Advisor实例
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
@@ -21,7 +21,7 @@ import java.util.Collections;
/**
* @Description 描述:Swagger配置
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Configuration
@EnableSwagger2
@@ -18,22 +18,46 @@ import java.util.Map;
/**
* @Description 描述:
* 基于AOP的声明式事务管理配置类
* <p>
* 通过Spring AOP切面机制,自动为Service层的业务方法添加事务管理,
* 无需在每个方法上手动添加@Transactional注解。
* 根据方法名前缀自动匹配事务规则:
* - 增删改操作(insert/add/create/up/set/remove/delete等)使用读写事务
* - 查询操作(query/select/get/find等)使用只读事务,数据库可对只读操作进行性能优化
* </p>
*
* @Description 描述:* @Author A贾宇婷034244310
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Aspect
@Configuration
public class TransactionAopConfig {
/** Spring事务管理器,由Spring Boot自动配置注入 */
@Resource
private PlatformTransactionManager transactionManager;
/**
* 定义事务切点表达式
* <p>
* 匹配com.bc.exam.modules包下所有子模块的service.impl包中所有类的所有方法,
* 即所有Service实现类的方法都会被事务拦截。
* </p>
* @return 切点表达式字符串
*/
@Pointcut("execution(* com.bc.exam.modules.*.service.impl.*.*(..))")
public String transactionPoint(){
return "execution(* com.bc.exam.modules.*.service.impl.*.*(..))";
}
/**
* 事务管理配置
* <p>
* 根据方法名前缀定义两类事务规则:
* 1. 写操作(CUD):使用PROPAGATION_REQUIRED传播行为 + ISOLATION_READ_COMMITTED隔离级别 + 遇到Exception即回滚
* 2. 读操作(R):使用PROPAGATION_SUPPORTS传播行为 + 只读模式,数据库可针对只读做优化
* </p>
* @return 配置完成的事务拦截器实例
*/
@Bean("txAdvice")
public TransactionInterceptor txAdvice() {
@@ -84,6 +108,11 @@ public class TransactionAopConfig {
/**
* 设置切面规则,容器自动注入
* <p>
* 将事务拦截器(txAdvice)绑定到Service实现类的切点上,
* 使得符合切点表达式的方法在执行时自动被事务管理。
* </p>
* @return 配置完成的Advisor切面通知实例
*/
@Bean
public Advisor advisor() {
@@ -9,7 +9,7 @@ import java.lang.annotation.Target;
/**
* @Description 描述:数据字典注解
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@@ -10,7 +10,7 @@ import java.io.Serializable;
/**
* @Description 描述:全局错误码定义,用于定义接口的响应数据,枚举名称全部使用代码命名,在系统中调用,免去取名难的问题。
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@NoArgsConstructor
@AllArgsConstructor
@@ -9,44 +9,61 @@ import lombok.NoArgsConstructor;
/**
* 统一API响应结果封装类
* <p>
* 所有Controller接口的返回值都使用此类进行统一包装,确保前后端交互的数据格式一致。
* 响应结构包含三个核心字段:
* - code: 业务状态码,0表示成功,1表示失败
* - msg: 响应消息描述
* - data: 响应数据体,泛型T支持任意业务数据类型
* </p>
* <p>
* 使用示例:
* <pre>
* // 成功响应
* return new ApiRest&lt;&gt;(); // 配合BaseController的success()方法使用
* // 异常响应
* return new ApiRest&lt;&gt;(serviceException); // 从异常构造错误响应
* </pre>
* </p>
* @param <T> 响应数据的泛型类型
*
* @Description 描述:数据结果返回的封装
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
@NoArgsConstructor
@ApiModel(value="接口响应", description="接口响应")
public class ApiRest<T>{
/**
* 响应消息
*/
/** 响应消息描述,用于向前端传达操作结果的文字说明 */
@ApiModelProperty(value = "响应消息")
private String msg;
/**
* 响应
* 响应状态
* <p>0 = 操作成功,1 = 操作失败</p>
*/
@ApiModelProperty(value = "响应代码,0为成功,1为失败", required = true)
private Integer code;
/**
* 请求或响应body
*/
/** 响应数据体,承载具体的业务数据,类型为泛型以支持不同业务场景 */
@ApiModelProperty(value = "响应内容")
protected T data;
/**
* 是否成功
* @return
* 判断当前响应是否成功状态
* @return true表示操作成功(code=0),false表示操作失败
*/
public boolean isSuccess(){
return code.equals(0);
}
/**
* 构造函数
* @param error
* 从ServiceException构造异常响应
* <p>将业务异常的code和msg映射到响应对象中</p>
* @param error 业务异常对象,包含错误码和错误信息
*/
public ApiRest(ServiceException error){
this.code = error.getCode();
@@ -54,8 +71,9 @@ public class ApiRest<T>{
}
/**
* 构造函数
* @param error
* 从ApiError构造异常响应
* <p>将API错误枚举的code和msg映射到响应对象中</p>
* @param error API错误枚举对象,包含错误码和错误信息
*/
public ApiRest(ApiError error){
this.code = error.getCode();
@@ -7,32 +7,42 @@ import com.bc.exam.core.exception.ServiceException;
/**
* 基础控制器
* <p>
* 所有业务Controller的公共基类,提供统一的响应构造方法,
* 封装success(成功)和failure(失败)两种响应模式及其多种重载形式,
* 使子类Controller只需调用简洁的方法即可返回规范化的ApiRest响应。
* </p>
* <p>
* 响应状态约定:
* - code=0 表示操作成功
* - code=1 表示操作失败
* </p>
*
* @Description 描述:基础控制器 control复用这里公共的内容
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
public class BaseController {
/**
* 成功默认消息
*/
/** 成功状态码 */
private static final Integer CODE_SUCCESS = 0;
/** 成功默认消息 */
private static final String MSG_SUCCESS = "操作成功!";
/**
* 失败默认消息
*/
/** 失败状态码 */
private static final Integer CODE_FAILURE = 1;
/** 失败默认消息 */
private static final String MSG_FAILURE = "请求失败!";
/**
* 完成消息构造
* @param code
* @param message
* @param data
* @param <T>
* @return
* 构造完整的响应消息(核心方法,其他方法均委托于此)
* @param code 响应状态码(0=成功,1=失败)
* @param message 响应消息描述
* @param data 响应数据体,为null时不设置data字段
* @param <T> 响应数据的泛型类型
* @return 构造完成的统一响应对象
*/
protected <T> ApiRest<T> message(Integer code, String message, T data){
ApiRest<T> response = new ApiRest<>();
@@ -45,9 +55,10 @@ public class BaseController {
}
/**
* 请求成功空数据
* @param <T>
* @return
* 请求成功 - 无数据响应
* <p>用于不需要返回数据的操作,如删除、状态更新等</p>
* @param <T> 响应数据的泛型类型
* @return 成功的空数据响应
*/
protected <T> ApiRest<T> success(){
return message(0, "请求成功!", null);
@@ -56,11 +67,11 @@ public class BaseController {
/**
* 请求成功,通用代码
* @param message
* @param data
* @param <T>
* @return
* 请求成功 - 带自定义消息和数据
* @param message 自定义成功消息
* @param data 响应数据体
* @param <T> 响应数据的泛型类型
* @return 包含自定义消息和数据的成功响应
*/
protected <T> ApiRest<T> success(String message, T data){
return message(CODE_SUCCESS, message, data);
@@ -68,10 +79,10 @@ public class BaseController {
/**
* 请求成功,仅内容
* @param data
* @param <T>
* @return
* 请求成功 - 仅带数据(使用默认成功消息"操作成功!")
* @param data 响应数据体
* @param <T> 响应数据的泛型类型
* @return 包含数据的成功响应
*/
protected <T> ApiRest<T> success(T data){
return message(CODE_SUCCESS, MSG_SUCCESS, data);
@@ -79,42 +90,42 @@ public class BaseController {
/**
* 请求失败完整构造
* @param code
* @param message
* @param data
* @param <T>
* @return
* 请求失败 - 完整构造(自定义状态码、消息和数据)
* @param code 自定义错误码
* @param message 自定义错误消息
* @param data 错误详情数据
* @param <T> 响应数据的泛型类型
* @return 包含完整错误信息的失败响应
*/
protected <T> ApiRest<T> failure(Integer code, String message, T data){
return message(code, message, data);
}
/**
* 请求失败,消息和内容
* @param message
* @param data
* @param <T>
* @return
* 请求失败 - 带消息和数据(使用默认失败状态码1)
* @param message 自定义错误消息
* @param data 错误详情数据
* @param <T> 响应数据的泛型类型
* @return 包含错误消息和数据的失败响应
*/
protected <T> ApiRest<T> failure(String message, T data){
return message(CODE_FAILURE, message, data);
}
/**
* 请求失败,消息
* @param message
* @return
* 请求失败 - 仅消息(使用默认失败状态码1,无数据)
* @param message 自定义错误消息
* @return 仅包含错误消息的失败响应
*/
protected <T> ApiRest<T> failure(String message){
return message(CODE_FAILURE, message, null);
}
/**
* 请求失败,仅内容
* @param data
* @param <T>
* @return
* 请求失败 - 仅数据(使用默认失败消息"请求失败!")
* @param data 错误详情数据
* @param <T> 响应数据的泛型类型
* @return 包含错误数据的失败响应
*/
protected <T> ApiRest<T> failure(T data){
return message(CODE_FAILURE, MSG_FAILURE, data);
@@ -122,9 +133,9 @@ public class BaseController {
/**
* 请求失败,仅内容
* @param <T>
* @return
* 请求失败 - 默认响应(使用默认状态码1和默认消息"请求失败!",无数据)
* @param <T> 响应数据的泛型类型
* @return 默认的失败响应
*/
protected <T> ApiRest<T> failure(){
return message(CODE_FAILURE, MSG_FAILURE, null);
@@ -133,9 +144,11 @@ public class BaseController {
/**
* 请求失败,仅内容
* @param <T>
* @return
* 请求失败 - 从ApiError枚举构造响应
* @param error API错误枚举,包含预定义的错误码和错误消息
* @param data 错误详情数据
* @param <T> 响应数据的泛型类型
* @return 基于ApiError枚举的失败响应
*/
protected <T> ApiRest<T> failure(ApiError error, T data){
return message(error.getCode(), error.msg, data);
@@ -144,10 +157,11 @@ public class BaseController {
/**
* 请求失败,仅内容
* @param ex
* @param <T>
* @return
* 请求失败 - 从ServiceException构造响应
* <p>将ServiceException中的错误码和错误信息映射到统一响应格式</p>
* @param ex 业务异常对象,包含错误码和错误信息
* @param <T> 响应数据的泛型类型
* @return 基于业务异常的失败响应
*/
protected <T> ApiRest<T> failure(ServiceException ex){
ApiRest<T> apiRest = message(ex.getCode(), ex.getMsg(), null);
@@ -9,7 +9,7 @@ import java.io.Serializable;
/**
* @Description 描述:请求和响应的基础类,用于处理序列化
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
public class BaseDTO implements Serializable {
@@ -9,7 +9,7 @@ import lombok.Data;
/**
* @Description 描述:主键通用请求类,用于根据ID查询
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
@ApiModel(value="主键通用请求类", description="主键通用请求类")
@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
/**
* @Description 描述:主键通用响应类,用于添加后返回内容
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
@ApiModel(value="主键通用响应类", description="主键通用响应类")
@@ -10,7 +10,7 @@ import java.util.List;
/**
* @Description 描述:通用ID列表类操作,用于批量删除、修改状态等
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
@ApiModel(value="删除参数", description="删除参数")
@@ -12,7 +12,7 @@ import java.util.List;
/**
* @Description 描述:通用状态请求类,用于修改状态什么的
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
@ApiModel(value="通用状态请求类", description="通用状态请求类")
@@ -9,7 +9,7 @@ import lombok.Data;
/**
* @Description 描述:分页查询类
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@ApiModel(value="分页参数", description="分页参数")
@Data
@@ -3,9 +3,9 @@ package com.bc.exam.core.api.dto;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* @Description 描述:
* @Author A贾宇婷034244310分页响应类
* @Date 20260615
* @Description 描述:分页响应类,继承MyBatis-Plus的Page,增强分页计算
* @Author A贾宇婷034244310
* @Date 20260616
*/
public class PagingRespDTO<T> extends Page<T> {
@@ -4,7 +4,7 @@ package com.bc.exam.core.enums;
/**
* @Description 描述:通用的状态枚举信息
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
public interface CommonState {
@@ -9,7 +9,7 @@ import lombok.NoArgsConstructor;
/**
* @Description 描述:服务自定义异常 继承运行时异常
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@Data
@AllArgsConstructor
@@ -7,16 +7,35 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
/**
* 全局异常处理器
* <p>
* 使用@RestControllerAdvice注解实现全局统一的异常捕获和处理,
* 确保所有Controller层抛出的异常都能被统一拦截并返回规范化的JSON响应。
* 避免在每个Controller中重复编写try-catch代码,实现异常处理的集中化管理。
* </p>
* <p>
* 处理机制:
* - @RestControllerAdvice = @ControllerAdvice + @ResponseBody
* 异常处理结果自动序列化为JSON格式返回给前端
* - @ExceptionHandler指定要捕获的异常类型及其处理方法
* - @InitBinder和@ModelAttribute提供数据绑定和模型属性的全局预处理能力
* </p>
*
* @Description 描述:统一异常处理类
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260616
*/
@RestControllerAdvice
public class ServiceExceptionHandler {
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
* @param binder
* 全局数据绑定器初始化
* <p>
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器。
* 可用于注册自定义的属性编辑器(PropertyEditor)或类型转换器(Converter),
* 当前为空实现,预留扩展点。
* </p>
* @param binder WebDataBinder数据绑定器实例
*/
@InitBinder
public void initWebBinder(WebDataBinder binder){
@@ -24,8 +43,13 @@ public class ServiceExceptionHandler {
}
/**
* 把值绑定到Model中,使全局@RequestMapping可以获取到该值
* @param model
* 全局模型属性绑定
* <p>
* 在每个@RequestMapping方法执行前调用,可向Model中添加公共属性,
* 使得所有Controller方法都能通过@ModelAttribute获取到这些值。
* 当前为空实现,预留扩展点。
* </p>
* @param model Spring MVC的Model对象,用于存放视图共享数据
*/
@ModelAttribute
public void addAttribute(Model model) {
@@ -33,13 +57,19 @@ public class ServiceExceptionHandler {
}
/**
* 捕获ServiceException
* @param e
* @return
* 统一捕获业务异常(ServiceException
* <p>
* 当Service层抛出ServiceException时,自动将异常中的错误码和错误信息
* 封装为ApiRest统一响应格式返回给前端,HTTP状态码固定为200(OK),
* 通过ApiRest中的code字段区分业务成功/失败状态。
* </p>
* @param e 捕获到的业务异常对象,包含错误码(code)和错误信息(msg)
* @return 封装了异常信息的统一响应对象
*/
@ExceptionHandler({com.bc.exam.core.exception.ServiceException.class})
@ResponseStatus(HttpStatus.OK)
public ApiRest serviceExceptionHandler(ServiceException e) {
// 将异常中的错误码和错误消息构造为统一响应格式返回
return new ApiRest(e);
}
@@ -10,13 +10,9 @@ import java.util.stream.Collectors;
/**
* 简单封装Dozer, 实现深度转换Bean<->Bean的Mapper.实现:
*
* 1. 持有Mapper的单例.
* 2. 返回值类型转换.
* 3. 批量转换Collection中的所有对象.
* 4. 区分创建新的B对象与将对象A值复制到已存在的B对象两种函数.
*
* @Description 描述:Bean对象映射工具类,基于Dozer实现深度属性拷贝与类型转换
* @Author A贾宇婷034244310
* @Date 20260615
*/
public class BeanMapper {
@@ -1,11 +1,15 @@
package com.bc.exam.modules;
/**
* @Description 描述:全局常量定义,存储系统级配置常量
* @Author C薛涵艺034244315
* @Date 20260615
*/
public class Constant {
/**
* 会话
* 会话令牌常量Key
*/
public static final String TOKEN = "token";
}
@@ -17,8 +17,8 @@ import org.springframework.web.bind.annotation.RestController;
/**
* @Description 描述:通用配置控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Api(tags={"系统配置"})
@RestController
@@ -9,8 +9,8 @@ import java.io.Serializable;
/**
* @Description 描述:通用配置请求类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Data
@ApiModel(value="系统配置", description="系统配置")
@@ -10,8 +10,8 @@ import lombok.Data;
/**
* @Description 描述:系统配置实体类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@TableName("sys_config")
@@ -6,8 +6,8 @@ import com.bc.exam.modules.config.entity.SysConfig;
/**
* @Description 描述:系统配置Mapper
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface SysConfigMapper extends BaseMapper<SysConfig> {
@@ -7,8 +7,8 @@ import com.bc.exam.modules.config.entity.SysConfig;
/**
* @Description 描述:系统配置接口
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface SysConfigService extends IService<SysConfig> {
@@ -12,8 +12,8 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:语言设置 服务实现类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class SysConfigServiceImpl extends ServiceImpl<SysConfigMapper, SysConfig> implements SysConfigService {
@@ -23,8 +23,8 @@ import javax.annotation.Resource;
/**
* @Description 描述:字典码值数据表 前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/sys/api/dict/data")
@@ -25,8 +25,8 @@ import java.util.stream.Collectors;
/**
* @Description 描述:字典 码值大类表 前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/sys/api/dict/type")
@@ -14,8 +14,8 @@ import lombok.EqualsAndHashCode;
/**
* @Description 描述:字典码值数据表
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -13,8 +13,8 @@ import lombok.EqualsAndHashCode;
/**
* @Description 描述:字典码值大类表
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -12,8 +12,8 @@ import java.util.List;
/**
* @Description 描述:码值数据表 Mapper 接口
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface SysDictDataMapper extends BaseMapper<SysDictData> {
@Select("${sqlStr}")
@@ -6,8 +6,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description 描述:码值大类表 Mapper 接口
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface SysDictTypeMapper extends BaseMapper<SysDictType> {
@@ -12,8 +12,8 @@ import java.util.Map;
/**
* @Description 描述:码值数据表 服务类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface SysDictDataService extends IService<SysDictData> {
@@ -11,8 +11,8 @@ import java.util.function.Function;
/**
* @Description 描述:码值大类表 服务类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface SysDictTypeService extends IService<SysDictType> {
/**
@@ -22,8 +22,8 @@ import java.util.stream.Collectors;
/**
* @Description 描述:码值数据表 服务实现类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class SysDictDataServiceImpl extends ServiceImpl<SysDictDataMapper, SysDictData> implements SysDictDataService {
@@ -23,8 +23,8 @@ import java.util.function.Function;
/**
* @Description 描述:码值大类表 服务实现类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class SysDictTypeServiceImpl extends ServiceImpl<SysDictTypeMapper, SysDictType> implements SysDictTypeService {
@@ -18,8 +18,8 @@ import javax.annotation.Resource;
/**
* @Description 描述:公告表 前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/gonggao/gggl")
@@ -8,8 +8,8 @@ import java.util.Date;
/**
* @Description 描述:公告表
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -5,8 +5,8 @@ import com.bc.exam.modules.gonggao.entity.TGonggao;
/**
* @Description 描述:公告表 Mapper 接口
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface TGonggaoMapper extends BaseMapper<TGonggao> {
@@ -7,8 +7,8 @@ import com.bc.exam.modules.gonggao.entity.TGonggao;
/**
* @Description 描述:公告表 服务类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface TGonggaoService extends IService<TGonggao> {
IPage<TGonggao> paging(PagingReqDTO<TGonggao> reqDTO);
@@ -15,8 +15,8 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:公告表 服务实现类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class TGonggaoServiceImpl extends ServiceImpl<TGonggaoMapper, TGonggao> implements TGonggaoService {
@@ -19,8 +19,8 @@ import javax.annotation.Resource;
/**
* @Description 描述:留言信息表 前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/liuyan/lygl")
@@ -8,8 +8,8 @@ import java.util.Date;
/**
* @Description 描述:留言信息表
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -5,8 +5,8 @@ import com.bc.exam.modules.liuyan.entity.TLiuyan;
/**
* @Description 描述:留言信息表 Mapper 接口
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface TLiuyanMapper extends BaseMapper<TLiuyan> {
@@ -7,8 +7,8 @@ import com.bc.exam.modules.liuyan.entity.TLiuyan;
/**
* @Description 描述:留言信息表 服务类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface TLiuyanService extends IService<TLiuyan> {
@@ -15,8 +15,8 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:留言信息表 服务实现类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class TLiuyanServiceImpl extends ServiceImpl<TLiuyanMapper, TLiuyan> implements TLiuyanService {
@@ -15,8 +15,8 @@ import javax.annotation.Resource;
/**
* @Description 描述:学生基础信息 前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/student/student")
@@ -11,8 +11,8 @@ import lombok.EqualsAndHashCode;
/**
* @Description 描述:学生基础信息
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -9,8 +9,8 @@ import org.apache.ibatis.annotations.Param;
/**
* @Description 描述:学生基础信息 Mapper 接口
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface StudentMapper extends BaseMapper<Student> {
IPage<Student> paging(Page<Student> page, @Param("ew") Wrapper<Student> wrapper);
@@ -8,8 +8,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
/**
* @Description 描述:学生基础信息 服务类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface StudentService extends IService<Student> {
@@ -14,8 +14,8 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:学生基础信息 服务实现类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {
@@ -5,8 +5,8 @@ import org.apache.ibatis.annotations.Param;
/**
* @Description 描述:机主信息Mapper
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Mapper
public interface SysDictMapper {
@@ -1,4 +1,10 @@
package com.bc.exam.modules.system.service;
/**
* @Description 描述:SysDictService 类
* @Author C薛涵艺034244315
* @Date 20260618
*/
public interface SysDictService {
@@ -5,6 +5,12 @@ import com.bc.exam.modules.system.service.SysDictService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Description 描述:SysDictServiceImpl 类
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Service
public class SysDictServiceImpl implements SysDictService {
@@ -26,8 +26,8 @@ import java.util.Date;
/**
* @Description 描述:借还信息表前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/tjhxxb/controller")
@@ -11,8 +11,8 @@ import lombok.EqualsAndHashCode;
/**
* @Description 描述:借还信息表
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -10,8 +10,8 @@ import org.apache.ibatis.annotations.Param;
/**
* @Description 描述:借还信息表mapper
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface TJhxxbMapper extends BaseMapper<TJhxxb> {
IPage<TJhxxb> paging(Page<TJhxxb> page, @Param("ew") Wrapper<TJhxxb> wrapper);
@@ -7,8 +7,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
/**
* @Description 描述:借还信息表实现
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface TJhxxbService extends IService<TJhxxb> {
@@ -13,35 +13,50 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:借还信息表实现
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class TJhxxbServiceImpl extends ServiceImpl<TJhxxbMapper, TJhxxb> implements TJhxxbService {
/**
* 借还书记录分页查询
* <p>查询当前用户的借阅/归还记录,支持按借还状态过滤,以及按关联书籍的ISBN/书名/作者/类型模糊搜索。
* 查询使用自定义SQLbaseMapper.paging),涉及借还表(a)与书籍表(b)的关联查询。</p>
* @param reqDTO 分页请求参数,包含当前用户ID、查询条件(借还状态、书籍信息等)
* @return 分页后的借还记录数据
*/
@Override
public IPage<TJhxxb> paging(PagingReqDTO<TJhxxb> reqDTO) {
//创建分页对象
// 创建分页对象,设置当前页码和每页条数
Page<TJhxxb> query = new Page<>(reqDTO.getCurrent(), reqDTO.getSize());
//查询条件
// 构建查询条件(使用表别名 a=借还表, b=书籍表)
QueryWrapper<TJhxxb> wrapper = new QueryWrapper<>();
TJhxxb params = reqDTO.getParams();
// 按借还记录ID精确查询
if(!StringUtils.isBlank(params.getId())){
wrapper.eq("a.id", params.getId());
}
// 按借还状态过滤(如:待归还、已归还、逾期等)
if(!StringUtils.isBlank(params.getState())){
wrapper.eq("a.state", params.getState());
}
// 限制只查询当前登录用户的借阅记录(通过reqDTO中的userId获取)
wrapper.eq("a.user_id", reqDTO.getUserId());
// 以下条件基于关联的书籍表(b表)进行模糊搜索
// 按ISBN书号模糊搜索
wrapper.like(!StringUtils.isBlank(params.getIsbn()),"b.isbn", params.getIsbn());
// 按书名模糊搜索
wrapper.like(!StringUtils.isBlank(params.getBName()),"b.b_name", params.getBName());
// 按作者名模糊搜索
wrapper.like(!StringUtils.isBlank(params.getBAuthor()),"b.b_author", params.getBAuthor());
// 按书籍类型模糊搜索
wrapper.like(!StringUtils.isBlank(params.getBType()),"b.b_type", params.getBType());
//获得数据
// 调用自定义Mapper方法执行关联查询(借还表JOIN书籍表),获得分页数据
IPage<TJhxxb> page = baseMapper.paging(query, wrapper);
return page;
}
@@ -18,8 +18,8 @@ import javax.annotation.Resource;
/**
* @Description 描述:书籍借还日志记录前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/tlog/logcontrol")
@@ -7,8 +7,8 @@ import lombok.EqualsAndHashCode;
/**
* @Description 描述:借还日志
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -6,8 +6,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description 描述:借还日志接口
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface TLogMapper extends BaseMapper<TLog> {
@@ -7,8 +7,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
/**
* @Description 描述:借还日志服务类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface TLogService extends IService<TLog> {
IPage<TLog> paging(PagingReqDTO<TLog> reqDTO);
@@ -13,8 +13,8 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:借还日志服务
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class TLogServiceImpl extends ServiceImpl<TLogMapper, TLog> implements TLogService {
@@ -18,8 +18,8 @@ import javax.annotation.Resource;
/**
* @Description 描述:书籍信息表前端控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@RestController
@RequestMapping("/tsjxxb/controller")
@@ -11,8 +11,8 @@ import lombok.EqualsAndHashCode;
/**
* @Description 描述:书籍信息表
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@EqualsAndHashCode(callSuper = false)
@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description 描述:书籍信息表mapper
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface TSjxxbMapper extends BaseMapper<TSjxxb> {
@@ -7,8 +7,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
/**
* @Description 描述:书籍信息表服务
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260619
*/
public interface TSjxxbService extends IService<TSjxxb> {
@@ -13,34 +13,46 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:书籍信息表服务实现类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260620
*/
@Service
public class TSjxxbServiceImpl extends ServiceImpl<TSjxxbMapper, TSjxxb> implements TSjxxbService {
/**
* 书籍信息分页查询
* <p>支持多条件组合查询:按ID精确查询、按书籍类型筛选,以及按ISBN/书名/作者模糊搜索,按状态过滤</p>
* @param reqDTO 分页请求参数,包含当前页码、每页大小及查询条件
* @return 分页后的书籍信息数据
*/
@Override
public IPage<TSjxxb> paging(PagingReqDTO<TSjxxb> reqDTO) {
//创建分页对象
// 创建分页对象,设置当前页码和每页条数
Page<TSjxxb> query = new Page<>(reqDTO.getCurrent(), reqDTO.getSize());
//查询条件
// 构建查询条件
QueryWrapper<TSjxxb> wrapper = new QueryWrapper<>();
TSjxxb params = reqDTO.getParams();
// 按书籍ID精确查询
if(!StringUtils.isBlank(params.getId())){
wrapper.eq("id", params.getId());
}
// 按书籍类型精确筛选(如教材、参考书等)
if(!StringUtils.isBlank(params.getBType())){
wrapper.eq("b_type", params.getBType());
}
// 按ISBN书号模糊搜索
wrapper.like(!StringUtils.isBlank(params.getIsbn()),"isbn", params.getIsbn());//书号
// 按书名模糊搜索
wrapper.like(!StringUtils.isBlank(params.getBName()),"b_name", params.getBName());//书名
// 按作者名模糊搜索
wrapper.like(!StringUtils.isBlank(params.getBAuthor()),"b_author", params.getBAuthor());//作者
// 按书籍状态过滤(如在馆/借出/损坏等)
wrapper.like(!(null== params.getBState()),"b_state", params.getBState());//状态
//获得数据
// 执行分页查询,获得数据
IPage<TSjxxb> page = this.page(query, wrapper);
return page;
}
@@ -6,32 +6,58 @@ import com.bc.exam.modules.user.dto.response.SysUserLoginDTO;
import org.apache.shiro.SecurityUtils;
/**
* 用户工具类
* <p>
* 提供与当前登录用户相关的静态工具方法,封装Shiro安全框架的用户信息获取逻辑。
* 所有方法均为静态方法,可在任意位置直接调用,无需注入。
* </p>
* <p>
* 核心功能:从Shiro的SecurityContext中获取当前登录用户的ID,
* 支持两种模式:抛异常模式(默认)和静默模式(返回null)。
* </p>
*
* @Description 描述:用户静态工具类
* @Author A贾宇婷034244310
* @Date 20260615
* @Date 20260618
*/
public class UserUtils {
/**
* 获取当前登录用户的ID
* @param throwable
* @return
* <p>
* 从Shiro安全框架的Subject中获取当前登录用户的Principal信息,
* 提取用户ID返回。支持通过throwable参数控制未登录时的行为:
* - throwable=true: 未登录时抛出ServiceException(错误码ApiError.ERROR_10010002
* - throwable=false: 未登录时静默返回null
* </p>
* @param throwable 是否在获取失败时抛出异常,true=抛异常,false=返回null
* @return 当前登录用户的ID字符串,获取失败且throwable=false时返回null
* @throws ServiceException 获取失败且throwable=true时抛出用户未登录异常
*/
public static String getUserId(boolean throwable){
try {
// 从Shiro的Subject中获取当前登录用户信息(Principal),转换为SysUserLoginDTO后提取ID
return ((SysUserLoginDTO) SecurityUtils.getSubject().getPrincipal()).getId();
}catch (Exception e){
// 获取用户信息失败(通常是因为用户未登录或会话已过期)
if(throwable){
// 需要抛异常模式:抛出用户未登录的业务异常
throw new ServiceException(ApiError.ERROR_10010002);
}
// 静默模式:返回null,由调用方自行处理
return null;
}
}
/**
* 获取当前登录用户的ID默认是会抛异常
* @return
* 获取当前登录用户的ID默认抛异常模式)
* <p>
* 便捷方法,等同于getUserId(true)。
* 适用于必须登录才能访问的接口,未登录时直接抛出异常中断请求。
* </p>
* @return 当前登录用户的ID字符串
* @throws ServiceException 用户未登录或会话过期时抛出异常
*/
public static String getUserId(){
return getUserId(true);
@@ -21,8 +21,8 @@ import java.util.List;
/**
* @Description 描述:用户角色
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Api(tags = {"管理用户"})
@RestController
@@ -23,8 +23,8 @@ import java.util.List;
/**
* @Description 描述:管理用户控制器
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Api(tags = {"管理用户"})
@RestController
@@ -8,8 +8,8 @@ import java.io.Serializable;
/**
* @Description 描述:角色类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Data
@ApiModel(value="角色", description="角色")
@@ -9,8 +9,8 @@ import java.util.Date;
/**
* @Description 描述:管理用户
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Data
@ApiModel(value="管理用户", description="管理用户")
@@ -8,8 +8,8 @@ import java.io.Serializable;
/**
* @Description 描述:用户角色
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Data
@ApiModel(value="用户角色", description="用户角色")
@@ -8,8 +8,8 @@ import java.io.Serializable;
/**
* @Description 描述:登录请求
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Data
@ApiModel(value="管理员登录请求类", description="管理员登录请求类")
@@ -9,8 +9,8 @@ import java.util.List;
/**
* @Description 描述:登录信息保存
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Data
@ApiModel(value="管理员保存请求类", description="管理员保存请求类")
@@ -8,8 +8,8 @@ import java.io.Serializable;
/**
* @Description 描述:请求token
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Data
@ApiModel(value="会话检查请求类", description="会话检查请求类")
@@ -12,8 +12,8 @@ import java.util.Map;
/**
* @Description 描述:管理用户登录响应类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Data
@ApiModel(value="管理用户登录响应类", description="管理用户登录响应类")
@@ -9,8 +9,8 @@ import lombok.Data;
/**
* @Description 描述:角色
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@TableName("sys_role")
@@ -11,8 +11,8 @@ import java.util.Date;
/**
* @Description 描述:用户类
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@TableName("sys_user")
@@ -9,8 +9,8 @@ import lombok.Data;
/**
* @Description 描述:用户角色
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
@Data
@TableName("sys_user_role")
@@ -4,8 +4,8 @@ import com.bc.exam.modules.user.entity.SysRole;
/**
* @Description 描述:角色Mapper
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface SysRoleMapper extends BaseMapper<SysRole> {
@@ -5,8 +5,8 @@ import com.bc.exam.modules.user.entity.SysUser;
/**
* @Description 描述:管理用户Mapper
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
@@ -5,8 +5,8 @@ import com.bc.exam.modules.user.entity.SysUserRole;
/**
* @Description 描述:用户角色Mapper
* @Author A贾宇婷034244310
* @Date 20260615
* @Author B吉柯梦034244314
* @Date 20260617
*/
public interface SysUserRoleMapper extends BaseMapper<SysUserRole> {
@@ -8,8 +8,8 @@ import com.bc.exam.modules.user.entity.SysRole;
/**
* @Description 描述:角色
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
public interface SysRoleService extends IService<SysRole> {
@@ -10,8 +10,8 @@ import java.util.List;
/**
* @Description 描述:用户角色
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
public interface SysUserRoleService extends IService<SysUserRole> {
@@ -12,8 +12,8 @@ import java.util.List;
/**
* @Description 描述:管理用户
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
public interface SysUserService extends IService<SysUser> {
@@ -15,24 +15,30 @@ import org.springframework.stereotype.Service;
/**
* @Description 描述:系统角色
* @Author A贾宇婷034244310
* @Date 20260615
* @Author C薛涵艺034244315
* @Date 20260618
*/
@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
/**
* 角色分页查询
* <p>查询系统中所有角色的分页列表,当前无条件过滤(全量分页)</p>
* @param reqDTO 分页请求参数,包含当前页码和每页大小
* @return 分页后的角色DTO数据
*/
@Override
public IPage<SysRoleDTO> paging(PagingReqDTO<SysRoleDTO> reqDTO) {
//创建分页对象
// 创建分页对象,设置当前页码和每页条数
IPage<SysRole> query = new Page<>(reqDTO.getCurrent(), reqDTO.getSize());
//查询条件
// 构建查询条件(当前为无条件查询,即查询所有角色)
QueryWrapper<SysRole> wrapper = new QueryWrapper<>();
//获得数据
// 执行分页查询,获得数据
IPage<SysRole> page = this.page(query, wrapper);
//转换结果
// 将实体对象转换为DTO对象返回(通过JSON序列化/反序列化实现深拷贝)
IPage<SysRoleDTO> pageData = JSON.parseObject(JSON.toJSONString(page), new TypeReference<Page<SysRoleDTO>>(){});
return pageData;
}

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