需求:1.培训班管理;2.报名列表管理;3.申请信息变更;4.申请发布;5.申请审批
以上是本次需求中的5个功能菜单,根据客户需求,
要求在上述功能操作中的每一步都要进行日志的记录,分别记录登录人信息,IP地址,操作时间,如果是修改 要求记录为“修改姓名:张三→张小三
”共有8中日志操作类型。
日志实现的方案:
方案一、使用切面类
思路:切面类适合在不改变原有业务逻辑的基础上,对特定的方法进行增强。在这个场景中,可以定义一个切面,在目标方法执行前后进行拦截,根据不同的日志类型记录相应的日志信息。对于修改和申请变更的情况,可以在方法执行前后获取对象的状态,对比字段值的变化并记录。
方案二、使用工厂加策略模式实现
思路:工厂加策略模式适合根据不同的日志类型,动态选择不同的日志记录策略。可以定义一个日志策略接口,为每种日志类型实现具体的策略类,然后使用工厂类根据日志类型创建相应的策略对象。对于修改和申请变更的情况,在具体的策略类中实现字段值对比和记录的逻辑。
选择建议
切面类:
如果你的主要需求是在不改变现有业务代码的基础上,对特定方法进行统一的日志记录增强,且希望代码的侵入性最小,那么使用切面类是一个不错的选择。
工厂加策略模式:
如果后续可能会有更多的日志类型和复杂的日志记录逻辑,需要更灵活的扩展和管理不同的日志记录策略,那么工厂加策略模式会更合适。它将日志记录的逻辑封装在不同的策略类中,便于维护和扩展。
根据本次需求已经上面人不想动现有的日志管理,且这次的日志管理只是用于这5个功能模块,所以本次选择了工厂加策略模式的方式实现,
废话不多说直接上代码
策略接口类
import java.util.List;public interface LoggingStrategy {/** @Author 作者本人* @Description * @Date 14:31 2025/3/11* @Param args 本次新增、修改、发布等变动 的对象;* list 导入的数据;* operName:操作人;* operIp:ip地址;* logTye: 根据此值 选择不同的策略实现类 * method:日志操作类型 按照字典值维护 (新增操作、导入操作、删除操作,编辑操作,审批、发布等操作)* @return **/void saveLog(Object args, List<Object> list, String operName, String operIp, String logTye, String method, Integer applyType);
}
策略实现类
import com.bms.common.utils.StringUtils;
import com.bms.common.utils.bean.BeanUtils;
import com.bms.common.utils.bean.CompareObjectsDiffUtils;
import com.bms.common.utils.uuid.SnowFlake;
import com.bms.project.govern.entity.train.PxTrain;
import com.bms.project.govern.entity.train.PxTrainApply;
import com.bms.project.govern.entity.train.PxTrainLog;
import com.bms.project.govern.entity.train.PxTrainUser;
import com.bms.project.govern.entity.train.dto.PxTrainUserDTO;
import com.bms.project.govern.mapper.train.PxTrainApplyMapper;
import com.bms.project.govern.mapper.train.PxTrainLogMapper;
import com.bms.project.govern.mapper.train.PxTrainUserMapper;
import com.bms.project.govern.service.train.strategy.LoggingStrategy;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;/*** @className: InsertLogStrategy* @author: qh* @date: 2025/3/11 9:49* @Version: 1.0* @description: 培训班、报名列表、申请变更新增策略实现类*/
@Service
public class InsertLogStrategy implements LoggingStrategy {@Resourceprivate PxTrainLogMapper pxTrainLogMapper;@Resourceprivate PxTrainUserMapper pxTrainUserMapper;@Resourceprivate CompareObjectsDiffUtils compareObjectsDiffUtils;@Resourceprivate PxTrainApplyMapper pxTrainApplyMapper;/** @Author QH* @Description * @Date 10:03 2025/3/11* @Param logTye 1报名列表新增 2培训班新增 3申请新增* @return **/@Overridepublic void saveLog(Object args, List<Object> list, String operName, String operIp, String logTye,String method,Integer applyType) {PxTrainLog pxTrainLog = new PxTrainLog();pxTrainLog.setId(SnowFlake.id()+"");pxTrainLog.setMethod(method);pxTrainLog.setOperName(operName);pxTrainLog.setOperIp(operIp);pxTrainLog.setOperTime(new Date());if (StringUtils.equals("1",logTye)){//报名新增PxTrainUser trainUser = (PxTrainUser) args;pxTrainLog = creatLog(pxTrainLog, trainUser, null, null, logTye);pxTrainLogMapper.jwInsert(pxTrainLog);}else if (StringUtils.equals("2",logTye)){//培训班新增PxTrain pxTrain = (PxTrain) args;pxTrainLog = creatLog(pxTrainLog, null, null, pxTrain, logTye);pxTrainLogMapper.jwInsert(pxTrainLog);}else if (StringUtils.equals("3",logTye)){//申请新增PxTrainUserDTO pxTrainUserDTO = (PxTrainUserDTO) args;pxTrainLog = creatLog(pxTrainLog, null, pxTrainUserDTO, null, logTye);pxTrainLogMapper.jwInsert(pxTrainLog);}else if (StringUtils.equals("4",logTye)){//导入新增List<PxTrainLog> resultList = new ArrayList<>();for (Object object : list) {for (Object item : (ArrayList<?>) object) {if (item instanceof PxTrainUser) {PxTrainUser pxTrainUser = (PxTrainUser) item;PxTrainLog pxTrainLogs = new PxTrainLog();pxTrainLogs.setId(SnowFlake.id()+"");pxTrainLogs.setMethod(method);pxTrainLogs.setOperName(operName);pxTrainLogs.setOperIp(operIp);pxTrainLogs.setOperTime(new Date());pxTrainLogs.setName(pxTrainUser.getName());pxTrainLogs.setNumber(pxTrainUser.getNumber());pxTrainLogs.setIdCard(pxTrainUser.getIdCard());String s = assembleLogDescription(Integer.parseInt(logTye), pxTrainUser.getName(), pxTrainUser.getNumber(), null);pxTrainLogs.setOperParam(s);resultList.add(pxTrainLogs);}}}if (resultList.size()>0){pxTrainLogMapper.jwInsertBatchReplace(resultList);}}else if (StringUtils.equals("5",logTye)){//发布变更}else if (StringUtils.equals("6",logTye)){//审批 新增 编辑 删除 2 已通过 3 未通过}else if (StringUtils.equals("7",logTye)){// 撤回新增}public PxTrainLog creatLog(PxTrainLog pxTrainLog,PxTrainUser trainUser,PxTrainUserDTO pxTrainUserDTO,PxTrain pxTrain,String logTye){if (ObjectUtils.isNotEmpty(trainUser)){pxTrainLog.setName(trainUser.getName());pxTrainLog.setNumber(trainUser.getNumber());pxTrainLog.setIdCard(trainUser.getIdCard());String s = assembleLogDescription(Integer.parseInt(logTye), trainUser.getName(), trainUser.getNumber(), null);pxTrainLog.setOperParam(s);}else if (ObjectUtils.isNotEmpty(pxTrainUserDTO)){pxTrainLog.setName(pxTrainUserDTO.getName());pxTrainLog.setNumber(pxTrainUserDTO.getNumber());pxTrainLog.setIdCard(pxTrainUserDTO.getIdCard());String s = assembleLogDescription(Integer.parseInt(logTye), pxTrainUserDTO.getName(), pxTrainUserDTO.getNumber(), null);pxTrainLog.setOperParam(s);}else if (ObjectUtils.isNotEmpty(pxTrain)){pxTrainLog.setTrainName(pxTrain.getName());String s = assembleLogDescription(Integer.parseInt(logTye), null, null, pxTrain.getName());pxTrainLog.setOperParam(s);}return pxTrainLog;}/*** 根据日志类型和传入的值组装日志操作描述* @param logType 日志类型,1 表示新增报名人员,2 表示新增培训班,4 表示申请新增报名人员* @param name 人员姓名,如张三* @param hrNumber 人资编号,如 1000000* @param trainingClassName 培训班名称,如 2兴趣班* @return 组装好的日志操作描述*/public static String assembleLogDescription(int logType, String name, String hrNumber, String trainingClassName) {switch (logType) {case 1:return String.format("新增报名人员%s(人资编号%s)", name, hrNumber);case 2:return String.format("新增培训班%s", trainingClassName);case 3:return String.format("申请新增报名人员%s(人资编号%s)", name, hrNumber);case 4:return String.format("导入报名人员%s(人资编号%s)", name, hrNumber);default:return "未知日志类型,无法生成日志描述";}}public static String assembleLogDescriptionApply(int logType, String name, String hrNumber) {switch (logType) {case 0:return String.format("发布申请新增报名人员%s(人资编号%s)", name, hrNumber);case 1:return String.format("发布申请编辑报名人员%s(人资编号%s)", name, hrNumber);case 2:return String.format("发布申请删除报名人员%s(人资编号%s)", name, hrNumber);case 3:return String.format("撤回申请新增报名人员%s(人资编号%s)", name, hrNumber);case 4:return String.format("撤回申请编辑报名人员%s(人资编号%s)", name, hrNumber);case 5:return String.format("撤回申请删除报名人员%s(人资编号%s)", name, hrNumber);default:return "未知日志类型,无法生成日志描述";}}public static String assembleLogDescriptionApproval(int logType, String name, String hrNumber) {switch (logType) {case 2:return String.format("通过新增报名人员%s(人资编号%s)", name, hrNumber);case 3:return String.format("驳回新增报名人员%s(人资编号%s)", name, hrNumber);case 4:return String.format("通过删除报名人员%s(人资编号%s)", name, hrNumber);case 5:return String.format("驳回删除报名人员%s(人资编号%s)", name, hrNumber);default:return "未知日志类型,无法生成日志描述";}}
}
工厂类
import com.bms.project.govern.service.train.strategy.LoggingStrategy;
import com.bms.project.govern.service.train.strategy.impl.DelLogStrategy;
import com.bms.project.govern.service.train.strategy.impl.EditLogStrategy;
import com.bms.project.govern.service.train.strategy.impl.InsertLogStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;/*** @className: LoggingStrategyFactory* @author: qh* @date: 2025/3/11 9:50* @Version: 1.0* @description:*/
@Component
public class LoggingStrategyFactory {private final Map<Integer, LoggingStrategy> strategyMap = new HashMap<>();@Autowiredpublic LoggingStrategyFactory(InsertLogStrategy insertLogStrategy,EditLogStrategy editLogStrategy,DelLogStrategy delLogStrategy) {strategyMap.put(1, insertLogStrategy);strategyMap.put(2, editLogStrategy);strategyMap.put(3, delLogStrategy);// 这里可以继续添加其他日志类型对应的策略}public LoggingStrategy getStrategy(int logType) {return strategyMap.get(logType);}
}
新增/修改 使用
public AjaxResult singUpTrain(PxTrainUser data) {String username = getUsername();String ipAddr = IpUtils.getIpAddr();data.setUpdateTime(new Date());data.setUpdateBy(getUsername());if (ObjectUtils.isEmpty(data.getId())){//新增data.setId(SnowFlake.id() + "");data.setCreateBy(getUsername());data.setCreateTime(new Date());LoggingStrategy strategy = loggingStrategyFactory.getStrategy(1);strategy.saveLog(data,null,username,ipAddr,"1","1",null);}else {//修改LoggingStrategy strategy = loggingStrategyFactory.getStrategy(2);strategy.saveLog(data,null,username,ipAddr,"1","2",null);}Integer i = pxTrainUserMapper.jwInsertReplace(data);return i>0?AjaxResult.success():AjaxResult.error();}
对比并获取到哪些字段进行了修改
import com.bms.framework.anno.ChineseFieldName;
import com.bms.framework.redis.RedisCache;
import com.bms.project.govern.entity.train.dto.TrainPeriodDTO;
import com.bms.project.govern.mapper.train.PxTrainUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;/*** @className: CompareObjectsDiffUtils* @author: qh* @date: 2025/3/11 9:16* @Version: 1.0* @description:*/
@Component
public class CompareObjectsDiffUtils {@Autowiredprivate PxTrainUserMapper pxTrainUserMapper;/*** 比较两个对象差异* @param oldObj 原始对象* @param newObj 新对象* @return 变更描述列表*/public List<String> compareObjects(Object oldObj, Object newObj) {List<String> changes = new ArrayList<>();try {Class<?> clazz = oldObj.getClass();for (Field field : clazz.getDeclaredFields()) {field.setAccessible(true);String fieldName = field.getName();Object oldValue = field.get(oldObj);Object newValue = field.get(newObj);if (!Objects.equals(oldValue, newValue)) {// 获取字段上的 ChineseFieldName 注解ChineseFieldName annotation = field.getAnnotation(ChineseFieldName.class);String chineseFieldName;if (annotation != null) {String tag = annotation.tag();//如果是1 则说明该字段不需要进行对比新旧值 直接跳过进入下一个字段对比if ("1".equals(tag)){continue;}else {chineseFieldName = annotation.value();}} else {chineseFieldName = fieldName;}changes.add(String.format("%s: %s → %s",chineseFieldName,formatValue(fieldName,oldValue),formatValue(fieldName,newValue)));}}} catch (Exception e) {e.printStackTrace();}return changes;}private String formatValue(String fieldName, Object value) {if (value == null) return "空";if (value instanceof Date) {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) value);}// 一下是对一些特殊字段的处理,我这里是对映射的字段值做了处理if ("periodNumber".equals(fieldName) && value instanceof Integer) {TrainPeriodDTO trainPeriodDTO = pxTrainUserMapper.queryTrainPeriodByNum((Integer) value);return trainPeriodDTO.dictLabel;}if ("periodNumbers".equals(fieldName) && value instanceof Integer) {TrainPeriodDTO trainPeriodDTO = pxTrainUserMapper.queryTrainPeriodByNum((Integer) value);return trainPeriodDTO.dictLabel;}if ("gender".equals(fieldName) && value instanceof String) {if ("0".equals(value)){return "男";}else if ("1".equals(value)){return "女";}else {return "未知";}}if ("closed".equals(fieldName) && value instanceof Integer) {if (((Integer) value).intValue() == 0){return "关闭";}else if (((Integer) value).intValue() == 1){return "开启";}}return value.toString();}
}
通过注解来获取映射的字段名称及是否需要忽略该字段
注解类
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*** @className: ChineseFieldName* @author: qh* @date: 2025/3/11 11:16* @Version: 1.0* @description:*/
// 注解的作用目标为字段
@Target(ElementType.FIELD)
// 注解在运行时保留
@Retention(RetentionPolicy.RUNTIME)
public @interface ChineseFieldName {//字段汉字名称String value();//默认是0--进行对比 1 忽略对比String tag() default "0";
}
实体类部分代码
/*** 姓名*/@Excel(name = "姓名")@NotEmpty(message = "姓名不能为空")@ChineseFieldName(value = "姓名")private String name;/*** 员工编号*/@Excel(name = "员工编号")@NotEmpty(message = "员工编号不能为空")@ChineseFieldName(value = "员工编号")private String number;/*** 身份证号*/@Excel(name = "身份证号")@NotEmpty(message = "身份证号不能为空")@ChineseFieldName(value = "身份证号")private String idCard;/*** 此处为忽略标记 不对该字段进行新旧值的对比*/@ChineseFieldName(value = "", tag = "1")private Integer applyState;
通过上述代码可实现操作日志记录,且还存在 很大 的优化空间,但是由于要两天的时间进行开发、测试、上线,所以就这样吧 优化个der