后台管理系统用户操作日志技术设计与实现

一、基础内容定义

1.操作模块

针对后台管理系统,操作模块类别命名为“【一级菜单】【二级菜单】......”

2.操作主体类别

操作主体类别跟操作模块大致对应,如操作模块为广告管理,则操作主体类别为广告。操作主体类别,用于拼接操作内容,定义的模板为

“{operationType}{objectType},名称:{objectName},ID:{objectId}”

如“新增了广告,名称:首页广告,ID:100”

3.操作类别

操作日志定义的操作类别为,新增、修改、删除、操作。操作对应的是审核、上架、打开开关等一键式操作,记录日志内容时,可自定义操作名称。

4.权限控制

基于后台系统的操作权限,分为集团和门店账号。集团账号可查看该集团下所有用户的所有操作日志,门店账号仅可查看同门店下所有用户的操作日志。用户的操作跟随登录用户的身份进行记录。

5.日志记录的主要内容

CREATE TABLE `operation_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `group_id` varchar(100) NOT NULL COMMENT '集团编号',
  `org_id` varchar(100) NOT NULL COMMENT '组织编号',
  `app_code` varchar(50) DEFAULT NULL COMMENT '模块Code',
  `app_name` varchar(200) NOT NULL COMMENT '模块名称',
  `operation_type` varchar(100) NOT NULL COMMENT '操作别名',
  `operation_name` varchar(200) NOT NULL COMMENT '操作名称',
  `operator_id` bigint(20) NOT NULL COMMENT '操作人ID',
  `operator_name` varchar(200) NOT NULL COMMENT '操作人用户名',
  `operator_real_name` varchar(100) DEFAULT NULL COMMENT '操作人姓名',
  `operation_time` datetime NOT NULL COMMENT '操作时间',
  `object_id` varchar(100) DEFAULT '' COMMENT '操作主体对象ID值',
  `object_name` varchar(500) DEFAULT '' COMMENT '操作主体对象名称值',
  `content` varchar(500) DEFAULT NULL COMMENT '操作内容',
  `extra_words` varchar(10000) DEFAULT NULL COMMENT '修改内容详情',
  PRIMARY KEY (`id`),
  KEY `objectId` (`object_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

6.日志记录样例

二、 要点解析

1.日志记录方式

为了尽量不侵入业务代码,采用Spring事件机制,并添加异步注解。在需要日志处理的地方发送事件,在监听器中异步处理。

applicationContext.publishEvent(new OperationLogEvent(this,
        OperationLogEventDto.builder()
                //操作模块
                .appName(AdminOperationAppEnum.MALL_PRD)
                //操作类型
                .operationType(AdminOperationTypeEnum.ADD)
                //操作主体类别
                .subjectName(AdminObjectEnum.MALL_PRD)
                //当前操作人信息
                .adminUserInfo((GlobalAdminUserInfo) SecurityUtils.getSubject().getPrincipal())
                //操作时间
                .operationTime(LocalDateTime.now())
                //操作主体
                .objectName(param.getName())
                .objectId(productId.toString())
                //当前切换的组织,跟操作人所属组织不绑定
                .curOrgId(GlobalAdminContextHolder.curOrgId())
                //扩展信息
                .extraWords("XXX")
                .build()));

2.语义化注解

当给用户展示时,用户并不知道对象中属性名的确切含义。因此,我们需要提升其可读性。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogTag {
    String alias() default "";
    
    BuiltinTypeHandler builtinType() default BuiltinTypeHandler.NORMAL;
    
    String extendedType() default "";
}

例如,我们要记录用户在界面上修改的商品退款文案

@LogTag(alias = "退款文案")
private String refundText;

加上注解,则记录时,不是记录属性名refundText,而是记录属性别名“退款文案”。

3.编辑前后对象属性对比

3.1模型定义

Field包装类
@Data
public class FieldWrapper {
    @ApiModelProperty(value = "属性名称")
    private String attributeName;
    @ApiModelProperty(value = "注解的属性名称,如果不存在则使用attributeName")
    private String attributeAlias;
    @ApiModelProperty(value = "属性的旧值")
    private Object oldValue;
    @ApiModelProperty(value = "属性的新值")
    private Object newValue;
    @ApiModelProperty(value = "属性旧值字符串")
    private String oldValueString;
    @ApiModelProperty(value = "属性新值字符串")
    private String newValueString;
    @ApiModelProperty(value = "是否有注解")
    private boolean withLogTag;
    @ApiModelProperty(value = "属性注解")
    private LogTag logTag;
    @ApiModelProperty(value = "是否是外部类型")
    private boolean withExtendedType;
    @ApiModelProperty(value = "外部类型具体值")
    private String extendedType;

    public FieldWrapper(Field field, Object oldValue, Object newValue) {
        this.attributeName = field.getName();
        this.oldValue = oldValue;
        this.newValue = newValue;
        this.oldValueString = oldValue == null ? "" : oldValue.toString();
        this.newValueString = newValue == null ? "" : newValue.toString();
        this.logTag = field.getAnnotation(LogTag.class);
        this.withLogTag = logTag != null;
        this.attributeAlias = (withLogTag && logTag.alias().length() != 0) ? logTag.alias() : field.getName();
        this.withExtendedType = withLogTag && logTag.extendedType().length() != 0;
        this.extendedType = withExtendedType ? logTag.extendedType() : null;
    }
}
Class包装类
public class ClazzWrapper {
    private List<Field> fieldList;

    public ClazzWrapper(Class clazz) {
        this.fieldList = getFields(clazz);
    }

    public List<Field> getFieldList() {
        return fieldList;
    }

    private List<Field> getFields(Class clazz) {
        List<Field> fieldList = new ArrayList<>();
        return getFields(fieldList, clazz);
    }

    private List<Field> getFields(List<Field> fieldList, Class clazz) {
        fieldList.addAll(Arrays.asList(clazz.getDeclaredFields()));
        Class superClazz = clazz.getSuperclass();
        if (superClazz != null) {
            getFields(fieldList, superClazz);
        }
        return fieldList;
    }
}
对比值存储类
@Data
public class BaseAttributeModel {
    @ApiModelProperty(value = "属性类型")
    private String attributeType;
    @ApiModelProperty(value = "属性名称")
    private String attributeName;
    @ApiModelProperty(value = "属性别名")
    private String attributeAlias;
    @ApiModelProperty(value = "旧值")
    private String oldValue;
    @ApiModelProperty(value = "新值")
    private String newValue;
    @ApiModelProperty(value = "差异值")
    private String diffValue;
}

3.2属性对比

日志模块的使用场景不同,要处理的对象(即oldObject和newObject)千奇百怪。因此,我们要自动分析对象的属性不同,然后记录。即将对象拆解开来,逐一对比两个对象(来自同一个类)的各个属性,然后将不同的记录下来,需要用到反射。

//获取到修改前的json字符串
String objectJsonStr = adminOperationLogCacheService.getQueryContentCache(objectKey);
Class clazz = operationLogEventDto.getObjectClass();
//将修改前和修改后的json字符串均转为对象
Object oldObject = new Gson().fromJson(objectJsonStr , clazz);
Object newObject = new Gson().fromJson(operationLogEventDto.getObjectJsonStr() , clazz);
Class oldModelClazz = oldObject.getClass();
Class modelClazz = newObject.getClass();
if (oldModelClazz.equals(modelClazz)) {
    ClazzWrapper clazzWrapper = new ClazzWrapper(modelClazz);
    //遍历字段列表
    List<Field> fieldList = clazzWrapper.getFieldList();
    for (Field field : fieldList) {
        field.setAccessible(true);
        FieldWrapper fieldWrapper = new FieldWrapper(field, field.get(oldObject), field.get(newObject));
        //对LogTag标记的属性进行属性对比
        if (fieldWrapper.isWithLogTag()) {
            if (!typeHandlerService.nullableEquals(fieldWrapper.getOldValue(), fieldWrapper.getNewValue())) {
                BaseAttributeModel baseAttributeModel;
                if (fieldWrapper.isWithExtendedType()) {
                    baseAttributeModel = typeHandlerService.handleExtendedTypeItem(fieldWrapper,modelClazz);
                } else {
                    baseAttributeModel = typeHandlerService.handleBuiltinTypeItem(fieldWrapper);
                }
                //若该属性有变动,将新旧变化值存入List<BaseAttributeModel>
                if (baseAttributeModel != null) {
                    operationModel.addBaseActionItemModel(baseAttributeModel);
                }
            }
        }
    }
}

4.对象属性处理

4.1普通属性

可以直接记录用户界面输入值的属性,采用普通属性处理器进行处理

public BaseAttributeModel handleBuiltinTypeItem(FieldWrapper fieldWrapper) {
    BuiltinTypeHandler builtinType = BuiltinTypeHandler.NORMAL;
    if (fieldWrapper.getLogTag() != null) {
        builtinType = fieldWrapper.getLogTag().builtinType();
    }
    BaseAttributeModel handlerOutput = builtinType.handlerAttributeChange(fieldWrapper);
    if (handlerOutput != null) {
        handlerOutput.setAttributeName(fieldWrapper.getAttributeName());
        handlerOutput.setAttributeAlias(fieldWrapper.getAttributeAlias());
        handlerOutput.setAttributeType(builtinType.name());
        return handlerOutput;
    } else {
        return null;
    }
}

直接记录为“{attributeAlias}:{oldValue}->{newValue}”的形式即可。

public BaseAttributeModel handlerAttributeChange(FieldWrapper fieldWrapper) {
    BaseAttributeModel baseAttributeModel = new BaseAttributeModel();
    String oldValueStr = fieldWrapper.getOldValueString();
    if(StringUtils.isBlank(oldValueStr)){
        oldValueStr = "空";
    }
    baseAttributeModel.setOldValue(oldValueStr);
    String newValueStr = fieldWrapper.getNewValueString();
    if(StringUtils.isBlank(newValueStr)){
        newValueStr = "空";
    }
    baseAttributeModel.setNewValue(newValueStr);
    return baseAttributeModel;
}

例如,商品对象的售价,可以记为:

商品售价:100->80

4.2特殊属性

不能直接记录用户界面输入值的属性均需要特殊处理,采用扩展类型处理器进行处理

public BaseAttributeModel handleExtendedTypeItem(FieldWrapper fieldWrapper, Class clazz) {
    BaseAttributeModel baseAttributeModel = baseExtendedTypeHandler.handleAttributeChange(
            fieldWrapper.getExtendedType(),
            fieldWrapper.getAttributeName(),
            fieldWrapper.getAttributeAlias(),
            fieldWrapper.getOldValue(),
            fieldWrapper.getNewValue(),
            clazz
    );

    if (baseAttributeModel != null) {
        if (baseAttributeModel.getAttributeType() == null) {
            baseAttributeModel.setAttributeType(fieldWrapper.getExtendedType());
        }
        if (baseAttributeModel.getAttributeName() == null) {
            baseAttributeModel.setAttributeName(fieldWrapper.getAttributeName());
        }
        if (baseAttributeModel.getAttributeAlias() == null) {
            baseAttributeModel.setAttributeAlias(fieldWrapper.getAttributeAlias());
        }
    }

    return baseAttributeModel;
}

有一些属性不可以直接记入日志,例如富文本。采用新值旧值的形式记录其变动是不合理的,可以将其简化为“修改了{attributeAlias}”

@LogTag(alias = "产品预订须知",extendedType = "richText")
private String noticeReservation;

我们在日志处理模块,识别出属性的extendType为richText后,使用富文本处理方式对其进行记录。

public BaseAttributeModel handleRichTextData(Object oldValue,Object newValue,String attributeAlias){
    BaseAttributeModel baseAttributeModel = new BaseAttributeModel();
    String oldValueStr = (String)oldValue;
    String newValueStr = (String)newValue;
    if(!oldValueStr.equals(newValueStr)){
        baseAttributeModel.setDiffValue("修改了"+attributeAlias);
        return baseAttributeModel;
    }else{
        return null;
    }
}

4.3业务属性

还有一种属性,更为特殊。如对象中记录的为枚举值,或另一个关联表的id值等,记录时需语义化为对应的业务名称。

此时,我们可以使用注解来标明一个属性的值需要由业务系统辅助处理。

@LogTag(alias = "销售渠道",extendedType = "arr")
private Integer[] salesChannels;
@LogTag(alias = "扣库存方式",extendedType = "enum")
private Short deductStockTrigger;

在返回的对象中salesChannels可能为[1,2],但展示在界面上,需展示为支付宝渠道、微信渠道。

@Override
public String handleEnumData(String attributeName, Object value, Class clazz) {
    if(clazz.getName().equals(AdminProductDetailDto.class.getName())
        ||clazz.getName().equals(AdminProductDetailDto.AdminProductSkuDto.class.getName())
        ||clazz.getName().equals(AdminProductDetailDto.ProductCombinationRequest.class.getName())){
        return productExtendedTypeHandler.handleEnumData(attributeName,value);
    }
    if (clazz.getName().equals(AdminDeliverDetailDto.class.getName())) {
        return orderDeliverExtendedTypeHandler.handleEnumData(attributeName, value);
    }
    if (clazz.getName().equals(AdminPointListResponseDto.class.getName())) {
        return mallPointPayExtendedTypeHandler.handleEnumData(attributeName, value);
    }
    return null;
}

商品业务属性处理器示例如下:

public String handleEnumData(String attributeName,Object value){
    String outputValue = "空";
    if(Objects.isNull(value)){
        return outputValue;
    }
    Byte code = (Byte) value;
    //商品状态
    if(attributeName.equals("status")){
        outputValue = ProductStatusEnum.getDescByCode(code);
    }
    //退款类型
    if(attributeName.equals("refundType")){
        outputValue = RefundTypeEnum.getDescByCode(code);
    }
    //优惠券使用限制类型
    if(attributeName.equals("couponLimitType")){
        outputValue = CouponLimitTypeEnum.getDescByCode(code);
    }
    return outputValue;
}

三、流程图

1.整体流程

2.属性对比流程

3.新模块添加操作日志所需操作

  1. 根据需求,给Dto实体类,要记录变化值的属性,添加@LogTag注解,并标注每个属性的别名及属性类型

  1. 在Controller层,业务代码处理完成之后,组装相应的参数,调用Spring的publishEvent发送事件信息

  1. 若Dto实体类中有业务属性,在业务属性处理Service中添加新的handler

  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
后台管理系统页面操作日志设计与代码实现可以参考以下步骤: 1. 设计数据库表结构 可以创建一个名为“sys_log”的表来保存操作日志。表中可以包含以下字段:日志ID、操作人员、操作时间、操作模块、操作类型、操作对象、操作结果等。 2. 编写AOP切面 使用AOP技术,在系统中切入日志记录代码。在AOP切面中,可以通过注解或者切入点来确定需要记录日志的方法,并在方法执行前后记录相关操作日志信息。 3. 编写日志记录代码 在AOP切面中编写日志记录代码,将日志信息保存到数据库中。可以使用Spring JDBC或MyBatis等数据库操作框架来进行数据持久化操作。 4. 集成日志管理模块 可以在系统中集成日志管理模块,将保存的日志信息展示给管理员。管理员可以根据需求查询、导出、删除等日志操作。 以下是一个简单的AOP切面示例,用于记录操作日志: ```java @Component @Aspect public class LogAspect { @Autowired private LogService logService; @Pointcut("execution(* com.example.controller.*.*(..))") public void logPointcut() {} @AfterReturning(pointcut = "logPointcut()") public void doAfterReturning(JoinPoint joinPoint) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); User user = (User) request.getSession().getAttribute("user"); if (user != null) { String username = user.getUsername(); String ip = request.getRemoteAddr(); String method = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(); String params = Arrays.toString(joinPoint.getArgs()); String operation = "操作描述"; String result = "操作结果"; Log log = new Log(username, ip, method, params, operation, result); logService.saveLog(log); } } } ``` 在上述代码中,@Pointcut注解用于定义切入点,@AfterReturning注解用于定义在方法执行后记录日志操作。在记录日志时,可以获取当前用户、请求IP、请求方法、请求参数等信息,并将这些信息保存到数据库中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值