一、基础内容定义
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.新模块添加操作日志所需操作
根据需求,给Dto实体类,要记录变化值的属性,添加@LogTag注解,并标注每个属性的别名及属性类型
在Controller层,业务代码处理完成之后,组装相应的参数,调用Spring的publishEvent发送事件信息
若Dto实体类中有业务属性,在业务属性处理Service中添加新的handler