Hap-3.1.9审计功能使用方法
1 审计功能简单介绍
审计简单来说其实是一个追溯表数据变化的一个功能,一个典型的审计功至少由数据库中的两个表组成,一个是被审计表,我们称之为T,另一个是审计表,我们称之为T_A,对表T开启审计功能之后,表T中数据的任何变化(插入、修改、删除数据)都会被记录到T_A表中。在表T_A中我们可以查看到对表T某条数据的操作行为(C、U、D)、操作时间、操作人以及某个字段值的变化等信息。
2 审计功能基本实现原理
Hap框架中定义了一个拦截器:AuditInterceptor
,根据某一个特定的标识符决定是否对某个表开启审计,一旦我们对表T开启审计, 则AuditInterceptor
会拦截所有的 Mybatis 通过 dto 执行的操作,并动态生成 SQL 语句, 完成对新记录的备份操作,那么对这个表中所有数据的操作都会被记录到表T_A中。
插入,更新 执行的操作是 先操作, 后记录。
删除 执行的操作是 先记录, 后操作。
也就是说, 审计表中插入的都是最新的快照。
基于这个原理的审计, 只能做到行级的记录, 无法具体到字段级.
但是结合一定的比较手段, 可以知道, 某次修改, 到底修改了哪些字段.
3 开启审计功能的方法
如果要对某个表开启审计,只需在该表对应的DTO类中加上@AuditEnabled
注解即可。如下:
@ExtensionAttribute(disable=true)
@AuditEnabled
@Table(name = "cux_gxp_cus_header")
public class GxpCusHeader extends BaseDTO{
......
}
Hap框架会根据是否存在这个标识符决定是否对这个表开启审计功能。
4 审计表表结构设计规范
如果对一个表开启了审计功能,那么就需要一个对应的审计表来储存该被审计表的操作记录。
审计表的命名规范为:基表名_A。例如被审计表名为T,那么别审计表名应该为T_A。
这个规则可以 实现接口 AuditTableNameProvider
来自定义, 实现类需要定义为 spring bean
。
同时可以在 AuditEnabled
注解中通过参数 auditTable
来指定审计表的表名。
审计表字段包含基表的所有字段,但不包含基表的所有索引和约束等
另外必须包括审计专用字段:
-
audit_id (varchar(80))
主键(uuid) -
audit_transaction_type (varchar (10))
审计操作(insert,update,delete) -
audit_timestamp (datetime)
审计时间 -
audit_session_id (varchar(64))
审计 session id -
audit_tag (nummber(1))
标记是否为最新快照 -
lang (varchar(20))
操作时系统语言
5 审计开发基本流程
5.1 Controller层
注入Service,并指定Service名称
@Autowired
@Qualifier("gxpMqBasicAServiceImpl")
private IAuditDTOService gxpMqBasicAService;
/**
*物料资质头审计查询
* @param dto
* @param page
* @param pageSize
* @param request
* @return
*/
@PostMapping(value = "/cux/gxp/mq/basic/a/query")
@ResponseBody
public ResponseData query(GxpMqBasicA dto,
@RequestParam(defaultValue = DEFAULT_PAGE) int page,
@RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize,
HttpServletRequest request) {
IRequest requestContext = createRequestContext(request);
return new ResponseData(gxpMqBasicAService.selectAuditDTO(null,dto, page, pageSize));
}
/**
*物料资质头历史记录查询
* @param dto
* @param request
* @param page
* @param pageSize
* @param basicId
* @return
*/
@PostMapping("/cux/gxp/mq/basic/a/detail/{basicId}")
@ResponseBody
public ResponseData queryDetail(GxpMqBasicA dto,HttpServletRequest request,
@RequestParam(defaultValue = DEFAULT_PAGE) int page,
@RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int pageSize,
@PathVariable int basicId){
IRequest requestContext = createRequestContext(request);
return new ResponseData(gxpMqBasicAService.queryAuditDTODetail(requestContext, basicId, page, pageSize));
}
controller中的重点在于注入service时指定serviceImp的名称。由于所有审计功能的serviceImp都是统一实现IAuditDTOService
接口的,因此在注入service时需要使用@Qualifier
注解指定注入的具体是哪个service实现类。
在本示例代码中,注入service时使用@Qualifier
注解将gxpMqBasicAServiceImpl
作为参数传进去,可以理解为将gxpMqBasicAServiceImpl
作为值赋给了变量gxpMqBasicAService
,此时gxpMqBasicAService
指代的就是gxpMqBasicAServiceImpl
,可以调用gxpMqBasicAServiceImpl
中的方法。
5.2 Service层
所有的serviceImp统一实现IAuditDTOService
接口,重写接口中的两个基本方法selectAuditDTO()
和queryAuditDTODetail()
@Service
@Transactional(rollbackFor = Exception.class)
public class GxpMqBasicAServiceImpl implements IAuditDTOService {
@Autowired
GxpMqBasicAMapper gxpMqBasicAMapper;
@Override
//查询所有审计数据最新版本记录(所有数据的最新快照)
public List<Map<String, Object>> selectAuditDTO(IRequest iRequest,BaseDTO gxpMqBasicA, int... param) {
PageHelper.startPage(param[0],param[1]);
return AuditDTOUtils.selectAuditDTO(gxpMqBasicAMapper.selectMqBasicA(gxpMqBasicA));
}
@Override
//查询某条数据的审计历史记录(单条数据的操作记录)
public List queryAuditDTODetail(IRequest requestContext, int basicId, int page, int pageSize) {
PageHelper.startPage(page,pageSize);
return AuditDTOUtils.queryAuditDTOSingleLanguageDetail(gxpMqBasicAMapper.selectMqBasicADetail(basicId)); //单语言查询
}
}
5.3 Mapper层
- java类:
public interface GxpMqBasicAMapper extends Mapper<GxpMqBasicA>{
List<Map<String,Object>> selectMqBasicA(BaseDTO dto);
List<Map<String,Object>> selectMqBasicADetail(int basicId);
}
- xml文件:
<!--查询审计数据快照和审计历史通用的字段-->
<sql id="MqBasicAPart">
SELECT
(select case when count(0) = 0 then 'Y' else 'N' end
from cux_gxp_mq_basic cgm where cgm.basic_id = cgmb.basic_id) delete_flag,
cgmb.audit_id,
cgmb.audit_timestamp,
cgmb.audit_transaction_type,
cgmb.lang,
cgmb.major_field,
cgmb.OBJECT_VERSION_NUMBER,
e.name as operator_name,
sy.user_name as operator_user,
cgmb.basic_id,
cmov.organization_code,
cmov.organization_name,
cmi.item_no,
cmi.item_desc,
cmi.general_name,
cmi.ITEM_SPECIFICATION spec,
cmi.ITEM_TYPE model,
cmi.item_category_desc,
cgmb.storage_condition,
(select MEANING from SYS_CODE_VALUE_B b1,SYS_CODE_B b2
where b1.code_id = b2.code_id and b2.code='CUX_GXP_PRO_SCO'and VALUE=cgmb.PRO_SCO) pro_sco,
(select MEANING from SYS_CODE_VALUE_B b1,SYS_CODE_B b2
where b1.code_id = b2.code_id and b2.code='CUX_GXP_SMED_TYPE'and VALUE=cgmb.SPECIAL_DRUGS) special_drugs,
(select MEANING from SYS_CODE_VALUE_B b1,SYS_CODE_B b2
where b1.code_id = b2.code_id and b2.code='SYS.YES_NO'and VALUE=cgmb.PRESCRIPTION_LICENSE) prescription_license,
(select MEANING from SYS_CODE_VALUE_B b1,SYS_CODE_B b2
where b1.code_id = b2.code_id and b2.code='CUX_GXP_STATUS'and VALUE=cgmb.STATUS_CODE) status_code,
(select MEANING from SYS_CODE_VALUE_B b1,SYS_CODE_B b2
where b1.code_id = b2.code_id and b2.code='SYS.YES_NO'and VALUE= cgmb.TRADE_LIMIT) trade_limit,
cgmb.modify_reason,
cgmb.remark
FROM
cux_gxp_mq_basic_a cgmb
JOIN cux_mdm_item cmi ON cgmb.item_id = cmi.item_id
JOIN cux_mdm_organization_v cmov ON cgmb.organization_id = cmov.organization_id
JOIN sys_user sy ON cgmb.last_updated_by = sy.user_id
JOIN hr_employee e ON sy.employee_id = e.employee_id
</sql>
<!--物料资质头审计快照,多条件查询-->
<select id="selectMqBasicA" parameterType="hgxp.core.audit.dto.GxpMqBasicA" resultType="java.util.Map">
<include refid="MqBasicAPart"></include>
<where>
cgmb.audit_tag = 1
<if test="basicId!=null">
AND cgmb.BASIC_ID = #{basicId}
</if>
<if test="organizationCode!=null">
AND cmov.organization_code LIKE concat('%',concat(#{organizationCode},'%'))
</if>
<if test="itemNo!=null">
AND cmi.item_no LIKE concat('%',concat(#{itemNo},'%'))
</if>
<if test="itemDesc!=null">
AND cmi.item_desc LIKE concat('%',concat(#{itemDesc},'%'))
</if>
<if test="medType!=null">
and cgmb.MED_TYPE = #{medType}
</if>
<if test="opeSco!=null">
and cgmb.OPE_SCO = #{opeSco}
</if>
<if test="statusCode != null">
and cgmb.status_code = #{ statusCode }
</if>
<if test="tradeLimit!=null">
and cgmb.TRADE_LIMIT = #{tradeLimit}
</if>
<if test="deleteFlag!=null">
and
(select case when count(0) = 0 then 'Y' else 'N' end
from cux_gxp_mq_basic cgm where cgm.basic_id = cgmb.basic_id)
= #{deleteFlag}
</if>
</where>
ORDER BY
cgmb.AUDIT_TIMESTAMP DESC
</select>
<!--物料资质审计头表单条数据审计历史详情查询-->
<select id="selectMqBasicADetail" parameterType="java.lang.Integer" resultType="java.util.Map">
<include refid="MqBasicAPart"></include>
<where>
<if test="_parameter!=0">
AND cgmb.BASIC_ID = #{_parameter}
</if>
</where>
ORDER BY
cgmb.AUDIT_TIMESTAMP DESC
</select>
由于表结构的多样性和复杂性,查询语句无法写成通用的,因此需要开发人员针对不同的业务编写对应的查询语句
5.4 HTML页面
5.4.1 审计快照页面
审计快照页需要显示哪些字段根据具体的业务需求来决定,将mapper中查出来的数据显示在页面中即可。查询条件和页面详细布局及样式根据业务需求自行定制。如图:
审计快照页面5.4.2 审计历史详情页面
Hap框架中编写了一个common.js文件用来处理审计详情历史页面中被修改字段的颜色显示,让被更改过的字段值能够更明显的被看到。因此第一步需要引入common.js,然后在需要以不同颜色显示被修改数据的单元格调用js中的dealAuditSingleLanguageData(data)
方法。
<script src="${base.contextPath}/resources/js/audit/common.js"></script>
// kandoUI Grid 调用dealAuditSingleLanguageData
{
field: "statusCode",
title: '状态',
headerAttributes: {
style: "text-align: center"
},
width: 100,
template : function(rowdata) {
//调用dealAuditSingleLanguageData方法显示不同颜色
return dealAuditSingleLanguageData(rowdata.statusCode);
}
},
效果如图:
审计历史详情页面不同颜色显示被修改的字段.png审计类型、修改人,修改时间为Hap建议在审计历史详情页面中显示的必要字段,可以根据业务实际需求添加或减少。
如图:
审计历史详情页面.png6 审计功能开发中需要注意的地方
- 在对表对应的DTO进行操作的时候,使用service层的方法,不要直接使用mapper层的方法
在实际开发中,有时候为了图方便,可能在某些不需要负载业务逻辑处理的时候会直接跳过service层而直接调用mapper层的方法。这样做在加入审计功能之后会发现之前不太容易发现的问题:数据库中没有记录到该次操作本条数据的操作人和操作时间的信息,即LAST_UPDATED_BY
字段和LAST_UPDATE_LOGIN
字段为默认值-1。由于审计表中需要记录操作基表数据的操作人信息,而这些信息都是直接从基表中复制过去的,如果基表中没有操作人信息,那么审计表中就也没有操作人信息。
而操作数据的操作人信息是Hap封装在页面传到后台的Request请求中的,如果直接使用mapper的方法,是不能传request参数的,因此需要调用service层中的方法并将request参数传进方法中,对DTO进行操作,这样才能将操作数据的操作人信息保存在DTO对应的表中。
- 基于审计功能实现的原理,目前Hap的审计拦截器只能拦截到所有通过DTO操作数据库表的方法,如果某个操作没有经过DTO,那么该操作是无法被记录到的
在我们HGXP项目的开发过程中,有一个“批量分配”的功能,实际上就是将一个表中的某些特定数据直接复制并插入到另一张表中的操作,而这个操作我们是通过自己写的SQL语句实现的,并没有经过表对应的DTO,因此通过该方法批量插入到目标表中的数据无法被审计功能捕捉到。目前业务上如果需要有这样的操作,并且需要所有数据能够被审计,只能改造原有的方法使其适应Hap审计功能。或者自己写方法手动将记录插入到审计表中。
- 审计详情页面上那个标红的效果,是根据一个特殊字符"&"处理的,可能会在某些情况下出现一些问题
Hap的审计功能对比某条数据当前版本和上次一版本的所有字段的值,如果发现某个值有改动,则在其后面加上一个"&"符号,在common.js的dealAuditSingleLanguageData()
方法中会将末尾带有"&"符号的值做一个红色的样式处理,达到标红修改数据的目的。但是这样做在碰到日期等特殊字段值的时候可能会出现错误,如果某个字段值原本末尾就有"&"符号,那么也可能会出现错误。这一点在实际开发中需要注意,期待Hap框架开发组对该方法进一步完善。
网友评论