Files
exam-jyt/exam-api/src/main/java/com/bc/exam/aspect/DictAspect.java
T
2026-06-18 22:44:37 +08:00

379 lines
15 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.bc.exam.aspect;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.bc.exam.core.annon.Dict;
import com.bc.exam.core.api.ApiRest;
import com.bc.exam.core.utils.Reflections;
import com.bc.exam.modules.system.service.SysDictService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
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 C薛涵艺034244315
* @Date 20260616
*/
@Aspect
//@Component
@Slf4j
public class DictAspect {
/** 字典服务,用于根据字典编码查询对应的可读文本 */
@Autowired
private SysDictService sysDictService;
/**
* 环绕通知:切入所有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 {
return this.translate(pjp);
}
/**
* 执行目标方法并对返回结果进行字典翻译
* <p>
* 调用前必须确保SysDictService已正确配置并注入。
* </p>
* @param pjp 连接点对象
* @return 翻译处理后的返回结果
* @throws Throwable 目标方法执行异常
*/
public Object translate(ProceedingJoinPoint pjp) throws Throwable {
// 先执行目标方法获取返回结果,再对结果进行字典翻译处理
return this.parseAllDictText(pjp.proceed());
}
/**
* 转换全部数据字典
* <p>
* 仅对ApiRest类型的返回值进行字典翻译处理,其他类型直接透传。
* </p>
* @param result Controller方法的返回结果
* @return 处理后的返回结果(ApiRest类型会被翻译,其他类型原样返回)
*/
private Object parseAllDictText(Object result) {
// 仅ApiRest统一响应格式才进行字典翻译,非ApiRest类型直接返回不做处理
if (result instanceof ApiRest) {
parseFullDictText(result);
}
return 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);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理单个对象的字典翻译和日期格式化
* <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) {
if (record == null) {
return null;
}
// 基本数据类型(String、Number等)没有复杂字段,无需翻译处理
if (this.isBaseType(record.getClass())) {
return record;
}
// 将对象转为JSON格式处理,便于动态添加字典翻译字段(如xxx_dictText
String json = JSON.toJSONString(record);
JSONObject item = JSONObject.parseObject(json);
// 遍历对象的所有字段(包括父类字段),逐个进行翻译或格式化处理
for (Field field : Reflections.getAllFields(record)) {
// 分支1:List类型字段 - 递归处理嵌套列表中的字典翻译
if (List.class.isAssignableFrom(field.getType())) {
try {
List list = this.processList(field, item.getObject(field.getName(), List.class));
item.put(field.getName(), list);
continue;
} catch (Exception e) {
e.printStackTrace();
}
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 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;
}
}
return item;
}
/**
* 处理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<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);
}
return result;
}
/**
* 字典翻译实现:根据字典表、编码字段、文本字段和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());
}
if (!StringUtils.isEmpty(dictText)) {
return dictText;
}
} catch (Exception e) {
// 字典查询异常时不影响主流程,打印堆栈后返回空字符串
e.printStackTrace();
}
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) {
// 基础数据类型
if (clazz.equals(java.lang.Integer.class) ||
clazz.equals(java.lang.Byte.class) ||
clazz.equals(java.lang.Long.class) ||
clazz.equals(java.lang.Double.class) ||
clazz.equals(java.lang.Float.class) ||
clazz.equals(java.lang.Character.class) ||
clazz.equals(java.lang.Short.class) ||
clazz.equals(java.lang.Boolean.class)) {
return true;
}
// String类型
if (clazz.equals(java.lang.String.class)) {
return true;
}
// 数字
if (clazz.equals(java.lang.Number.class)) {
return true;
}
return false;
}
}