一般的管理系统都会涉及一些用户部门角色权限之类的管理,比如ERP/CRM/OA这些系统早都已经很多成熟的产品以及案例。这里目标是希望设计一套足够简单,能够方便自己进行复用和扩展的系统,主要内容也就是一些表结构设计,也参考了开源管理系统若依的一些经验,尤其是菜单权限设计。
用户部门岗位
我们的数据模型优先以自增id作为主键,但是在一些需要从外部系统接入数据的场景中,没有办法要求别人数据与我们的id保持一致。所以额外定义了一个code字段,用来记录对方数据的标识信息,这样数据接入过来之后还是统一用自增id作为表示,同时也保持了与外部数据id的对应关系。
另外可能也确实有一些需要展示编码的场景,比如单位编码、岗位编码等。
自身层级关系:对于用户、部门、岗位,数据本身可能就存在上下级关系,并且可能是多个上级,所以分别都额外定义一个Tree关系表;
部门与岗位关系:多对多关系,一个部门会有多个岗位,但一个岗位也可能存在于多个部门;
用户与部门岗位关系:用户可能存在于一个或多个部门中;也可能兼任一个或多个岗位;或者兼任一个或多个不同部门的多个不同岗位;
为了方便Tree数据在界面查询过滤,我们会提前构建以下缓存
用户树缓存:Tree:user
单位树缓存:Tree:dept
岗位树缓存:Tree:post
单位用户树缓存:Tree:dept-user
单位岗位树缓存:Tree:dept-post
使用缓存存在另一个问题,如果数据发生了变化,应该在什么时机更新缓存 ?而且这可能是一个耗时操作,如果放在每次数据变化后更新,那么会增加请求耗时降低用户体验。所以我们提供了一个刷新缓存操作,交给用户自己来决定何时更新缓存,他可以在批量操作之后再刷新缓存,我们觉得以上这些并不是那种会频繁变动的业务数据,让用户自己手动刷新缓存也是能够接受的。如果因为数据量大导致刷新延时,那也是可预期的,好过那种由于刷新缓存导致其它增删改查操作出现不可预期的延时。
很多场景中,我们可能是获取一个子Tree,以下是使用广度优先进行遍历获取子树的示例
Tree:dept 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static final String KEY_DEPT = "Tree:dept" ;public List<Tree<String>> tree(String deptId) { Tree<String> root = redis.getValue(KEY_DEPT); if (deptId == null || deptId.equals(root.getId())) { return List.of(root); } if (CollectionUtils.isEmpty(root.getChildren())) { return List.of(new Tree<>()); } Deque<Tree<String>> queue = new LinkedList<>(root.getChildren()); while (!queue.isEmpty()) { root = queue.pop(); if (Objects.equals(deptId, root.getId())) { return List.of(root); } if (CollectionUtils.isNotEmpty(root.getChildren())) { queue.addAll(root.getChildren()); } } return List.of(new Tree<>()); }
角色菜单权限
目录:本身不跳转页面,一般只是控制菜单的收缩展开
菜单:具体的页面
按钮:页面中的操作
以上是定义的三种菜单类型,我们希望能对界面的菜单进行动态控制:
对于(展示)目录和菜单,可以通过角色与用户进行关联,即给角色分配菜单,再给用户分配角色,然后只返回那些与用户关联或者本身是公开的菜单;
对于(操作)按钮,可以通过权限符来控制。我们在菜单上添加一个权限符标识,这里的菜单可以是一个具体的菜单或按钮,也可以是一个抽象的操作权限定义。相当于在按钮与角色之间添加了一层权限,这样就可以给一个权限分配多个菜单(多个菜单共用一个权限符),然后给一个角色分配多个权限,再给用户分配多个角色。
在实际操作中,我们还是统一以菜单来进行分配操作,因为菜单展示更直观一些,而且大多情况下,操作权限与菜单按钮是一一对应的(如果控制粒度够细的话)。如果对角色分配到的菜单进行权限符聚合去重,就可以得到角色的权限了(这样有个缺点,如果A和B共用权限标识,那么给角色分配了A菜单也就分配了B菜单,但用户可能不想这样,需要通过细化权限粒度解决)。
以下给普通角色分配菜单权限,这里只读控制权限就控制了所有界面中的只读按钮,当然也可以细化给每个只读按钮定义一个权限
在登录返回的Token信息中会包含用户的权限集合,那么可以根据用户的权限,以及按钮所绑定的权限符,来决定是显示还是移除按钮
hasPermi.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import store from '@/store' export default { inserted(el, binding, vnode) { const { value } = binding const permissions = store.getters && store.getters.permissions if (value && value instanceof Array && value.length > 0 ) { const permissionFlag = value const hasPermissions = permissions.some(permission => { return "*:*:*" === permission || permissionFlag.includes(permission) }) if (!hasPermissions) { el.parentNode && el.parentNode.removeChild(el) } } } }
可以简单通过标签在目标按钮上绑定权限符
user/ index.vue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <el-row :gutter="10" class ="mb8" > <el-col :span="1.5" > <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['sys:user:new']" >{{$t('route.system.user.new' )}}</el-button> </ el-col> <el-col :span="1.5" > <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['sys:user:edit']" >{{$t('route.system.user.edit' )}}</el-button> </ el-col> <el-col :span="1.5" > <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['sys:user:delete']" >{{$t('route.system.user.delete' )}}</el-button> </ el-col></el-row>
类似的,也可以通过角色来进行按钮元素控制
hasRole.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import store from '@/store' export default { inserted(el, binding, vnode) { const { value } = binding const roles = store.getters && store.getters.roles if (value && value instanceof Array && value.length > 0 ) { const roleFlag = value const hasRole = roles.some(role => { return "sysAdmin" === role || roleFlag.includes(role) }) if (!hasRole) { el.parentNode && el.parentNode.removeChild(el) } } } }
对应的在后端服务中,也可以根据用户权限进行判断
PermissionService.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @ConditionalOnClass (WebSecurityConfigurerAdapter.class ) @Service("permit") public class PermissionService { public boolean isAdmin () { List<String> roles = Access.userRoles(); if (CollectionUtils.isEmpty(roles)) { return false ; } return roles.contains("sysAdmin" ); } public boolean hasPermit (String permission) { if (isAdmin()) { return true ; } List<String> perms = Access.userPermissions(); if (CollectionUtils.isEmpty(perms)) { return false ; } return perms.contains(permission); } public boolean hasRole (String role) { if (isAdmin()) { return true ; } List<String> roles = Access.userRoles(); if (CollectionUtils.isEmpty(roles)) { return false ; } return roles.contains(role); } }
然后通过@PreAuthorize
给接口绑定所需要的权限
SysUserController.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @PreAuthorize ("@permit.hasPermit('sys:user:new')" )@PostMapping ("/add" )public Response<Void> add (@Validated @RequestBody SysUser sysUser) { sysUserService.add(sysUser); return Response.success(); } @PreAuthorize ("@permit.hasPermit('sys:user:edit')" )@PostMapping ("/edit" )public Response<SysUser> edit (@Validated @RequestBody SysUser sysUser) { return Response.success(sysUserService.edit(sysUser)); } @PreAuthorize ("@permit.hasPermit('sys:user:delete')" )@GetMapping (value = {"/delete" }) public Response<List<SysUser>> delete(@NotNull (message = "user.notnull.id" ) Long[] userId) { return Response.success(sysUserService.delete(userId)); }
字典信息
为了方便字典管理,一般都会字典信息进行分组分类,但是又如何来定义字典的分组和分类?我们不希望要维护额外的信息,所以想法是直接用字典来定义字典的分组和分类,可以使用parent_code
来给字典码添加层级关系,然后定义一个根字典dict_root
,接着可以在根字典下面添加分组字典,在分组字典下面添加类型字典,最后在类型字典下面再添加具体的字典,像下面这样
其实有了层级关系,就可以构造任意多层字典关系,不过大多场景下有了分组分类两层关系也就足够了,这样也方便直接查询出所有的分组分类
options 1 2 3 4 5 6 7 8 9 10 select t.group_code, case t.group_code when 'dict_root' then '根字典' else d1.dict_label end group_name, case t.group_code when 'dict_root' then 'dict_root' else d1.dict_label end group_en, t.type_code, d2.dict_label type_name, d2.dict_en type_en from (with groups as (select parent_code, dict_code from sys_dict where parent_code = 'dict_group' ) select d1.parent_code as group_code, d1.dict_code as type_code from sys_dict d1 where d1.parent_code = 'dict_root' union all select groups.parent_code as group_code, groups.dict_code as type_code f rom groups union all select d.parent_code as group_code, d.dict_code as type_code from sys_dict d, groups where d.parent_code = groups.dict_code) t left join sys_dict d1 on t.group_code = d1.dict_codeleft join sys_dict d2 on t.type_code = d2.dict_code
可以抽象出一个接口,方便进行统一的缓存操作
Dict.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Dict { String getGroupCode () ; String getTypeCode () ; String getDictCode () ; String getDictLabel () ; Object getDictValue () ; Integer getDictOrder () ; }
通过关联查询,可以列出所有字典及对应的分组分类
list 1 2 3 4 5 6 7 8 9 10 11 12 select case d.parent_code when 'dict_root' then '根字典' when 'dict_group' then '根字典' else g.dict_label end groupName,case d.parent_code when 'dict_root' then 'dict_root' when 'dict_group' then 'dict_root' else g.dict_en end groupEn,case d.parent_code when 'dict_root' then 'dict_root' when 'dict_group' then 'dict_root' else g.dict_code end groupCode,case d.parent_code when 'dict_root' then '根字典' else t.dict_label end typeName,case d.parent_code when 'dict_root' then 'dict_root' else t.dict_en end typeEn,case d.parent_code when 'dict_root' then 'dict_root' else t.dict_code end typeCode,d.id, d.dict_code dictCode, d.dict_label dictLabel, d.dict_en dictEn, d.dict_value dictValue, from sys_dict dleft join sys_dict t on d.parent_code = t.dict_codeleft join sys_dict g on t.parent_code = g.dict_codeorder by d.dict_order, d.parent_code
然后我们构建如下三个缓存Key
DictHelper.java 1 2 3 4 5 6 7 8 9 10 11 12 13 private static final String KEY_DICT = "sys-dict:dict:" ;private static final String KEY_TYPE = "sys-dict:type:" ;private static final String KEY_GROUP = "sys-dict:group:" ;public void put (Dict dict) { if (!"dict_root" .equals(dict.getTypeCode())){ redis.putMapValue(KEY_GROUP + dict.getGroupCode(), dict.getDictCode(), dict); if (!"dict_root" .equals(dict.getGroupCode())){ redis.putMapValue(KEY_TYPE + dict.getTypeCode(), dict.getDictCode(), dict); } } redis.putValue(KEY_DICT + dict.getDictCode(), dict); }
于是:
通过sys-dict:dict:{dictCode}
可以获取某个字典;
通过sys-dict:type:{typeCode}
可以获取某个类型下的所有字典;
通过sys-dict:group:{groupCode}
可以获取某个分组下面的所有字典(除了类型字典);
通过sys-dict:type:{groupCode}
可以获取某个分组下面的所有类型字典;
通过sys-dict:type:dict_root
可以获取所有的分组字典;
通过sys-dict:type:dict_group
可以获取所有的类型字典;
DictHelper.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public <T extends Dict> List<T> getGroup (String groupCode) { if (StringUtils.isBlank(groupCode)){ return new ArrayList<>(); } Map<String, T> map = redis.getMap(KEY_GROUP + groupCode); if (map == null ){ return new ArrayList<>(); } List<T> list = new ArrayList<>(map.values()); list.sort(Comparator.comparingInt(Dict::getDictOrder)); return list; } public <T extends Dict> List<T> getType (String typeCode) { if (StringUtils.isBlank(typeCode)){ return new ArrayList<>(); } Map<String, T> map = redis.getMap(KEY_TYPE + typeCode); if (map == null ){ return new ArrayList<>(); } List<T> list = new ArrayList<>(map.values()); list.sort(Comparator.comparingInt(Dict::getDictOrder)); return list; } public <T extends Dict> T getDict (String dictCode) { if (StringUtils.isBlank(dictCode)){ return null ; } return redis.getValue(KEY_DICT + dictCode); } public String getDictLabel (String dictCode) { Dict dict = getDict(dictCode); if (dict == null ){ return null ; } return dict.getDictLabel(); } public <T> T getDictValue (String dictCode) { Dict dict = getDict(dictCode); if (dict == null ){ return null ; } return (T)dict.getDictValue(); }
方便字典获取的代价是提高了维护的成本,比如对应的删除操作
DictHelper.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public void removeDict (String dictCode) { if (StringUtils.isBlank(dictCode)){ return ; } Dict dict =redis.getValue(KEY_DICT + dictCode); if (dict == null ){ return ; } redis.delete(KEY_DICT + dictCode); redis.deleteMapValue(KEY_TYPE + dict.getTypeCode(), dictCode); redis.deleteMapValue(KEY_GROUP + dict.getGroupCode(), dictCode); } public void removeType (String typeCode) { if (StringUtils.isBlank(typeCode)){ return ; } Map<String, Dict> dictMap = redis.getMap(KEY_TYPE + typeCode); if (dictMap != null ){ for (Dict dict : dictMap.values()){ redis.delete(KEY_DICT + dict.getDictCode()); redis.deleteMapValue(KEY_GROUP + dict.getGroupCode(), dict.getDictCode()); } } redis.delete(KEY_TYPE + typeCode); redis.deleteMapValue(KEY_GROUP + "dict_group" , typeCode); } public void removeGroup (String groupCode) { if (StringUtils.isBlank(groupCode)){ return ; } Map<String, Dict> TypeMap = redis.getMap(KEY_TYPE + groupCode); if (TypeMap != null ){ redis.delete(KEY_TYPE + groupCode); for (Dict type : TypeMap.values()){ redis.deleteMapValue(KEY_GROUP + "dict_group" , type.getDictCode()); } } Map<String, Dict> dictMap = redis.getMap(KEY_GROUP + groupCode); if (dictMap != null ){ for (Dict dict : dictMap.values()){ redis.delete(KEY_DICT + dict.getDictCode()); } } redis.delete(KEY_GROUP + groupCode); redis.deleteMapValue(KEY_GROUP + "dict_root" , groupCode); }
通常我们定义的字典值都是String类型,但在实际使用时可能实际需要的是一个整形数值,那么就需要类型转换,如果忘记了转换可能会出现一些意料之外的问题。所以我们定义个字典值转换器接口,在放入缓存时就进行转换。
DictValueParser.java 1 2 3 4 public interface DictValueParser { Object parse (String value, String param) ; }
比如下面时提供的一个默认转换器,可以对基本类型做一些转换
DefaultValueParser.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class DefaultValueParser implements DictValueParser { @Override public Object parse (String value, String param) { if (value == null ){ return null ; } return switch (param) { case "short" -> Short.parseShort(value); case "int" -> Integer.parseInt(value); case "long" -> Long.parseLong(value); case "float" -> Float.parseFloat(value); case "double" -> Double.parseDouble(value); case "boolean" -> Boolean.parseBoolean(value); default -> value; }; } }
然后在构造字典缓存之前,会检查字典是否设置了值转换器,如果设置了就转换一下值类型
SysDictCaches.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void refresh () throws Exception { AccessLogger.info("refresh cache of dict ..." ); dict.clear(); List<SysDict> list = sysDictMapper.list(null ,null ); for (SysDict sysDict : list) { String parserClazz = sysDict.getValueParser(); if (StringUtils.isBlank(parserClazz)){ dict.put(sysDict); }else { DictValueParser parser = parserMap.get(parserClazz); if (parser == null ){ parser = (DictValueParser)Class.forName(parserClazz).getDeclaredConstructor().newInstance(); parserMap.put(parserClazz, parser); } sysDict.setDictValue(parser.parse(String.valueOf(sysDict.getDictValue()), sysDict.getValueParam())); dict.put(sysDict); } } }
附件信息 有很多这样的场景,先上传附件得到url,然后再提交表单,将附件url填到业务数据中。但是如果附件上传之后,提交表单被取消或失败了,那么已经上传的附件就会变成脏数据。
SysAttachServiceImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Transactional (rollbackFor = Exception.class ) @Override public SysAttach upload (MultipartFile file , SysAttach sysAttach , boolean isPublic ) throws Exception { String fileName = file.getOriginalFilename(); sysAttach.setAttachName(fileName); sysAttach.setAttachSize(file.getSize()); sysAttach.setCreateTime(new Date()); sysAttach.setIsPublic(isPublic ? 1 : 0 ); String filePath = sysAttach.getAttachType() + "/" + DateUtils.format("yyyy-MM" ) + "/" + IdUtil.randomUUID() + "." + fileName; sysAttach.setAttachPath(filePath); sysAttachMapper.insert(sysAttach); fileService.minioUpload(file, sysAttach.getAttachGroup(), filePath, isPublic); return sysAttach; }
对于这个问题,可以在上传之前先将附件信息入库(上传失败了可以回滚),然后在长传成功之后将附件信息返回给客户端,客户端可以拿到附件上传的url和附件信息的id。这样要求业务在提交数据时根据附件id更新附件记录的宿主信息,所以对于那些没有宿主信息,并且创建超过了一定时间的附件记录,有理由认为它就是脏数据,可以检查出来进行删除,并清理对应的上传文件。
有了附件记录表,就方便做一些结构化的统计处理,比如根据附件的宿主进行分组分类,也方便对上传的文件设置过期时间,可以简单通过定时任务检查并清理过期了的附件。
操作日志 SysUserController.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Log (type = "admin_user" , action = "add" , desc = "新增用户:#{sysUser.userName}" , content = Content.REQ)@PostMapping ("/add" )public Response<Void> add (@Validated @RequestBody SysUser sysUser) { sysUserService.add(sysUser); return Response.success(); } @Log (type = "admin_user" , action = "edit" , desc = "修改用户:#{resp.userName}" , content = Content.ALL)@PostMapping ("/edit" )public Response<SysUser> edit (@Validated @RequestBody SysUser sysUser) { return Response.success(sysUserService.edit(sysUser)); } @Log (type = "admin_user" , action = "delete" , desc = "删除用户" , content = Content.RESP)@GetMapping (value = {"/delete" })public Response<List<SysUser>> delete(@NotNull (message = "user.notnull.id" ) Long[] userId) { return Response.success(sysUserService.delete(userId)); }
以上是我们记录操作日志的方式,这里的type和action需要硬编码,对应的值在字典中定义,desc描述可以使用简单的SPEL表达式,content内容固定,可以选择请求参数或者响应结果。这里不去讨论如何记录日志,我们希望日志展示的可读性能友好一点,并且不同的业务日志可以区分展示。办法是通过表单来控制,将type
字典的值就设置成表单key,然后在表单中再根据action
动作的不同进行区分渲染。
如果将日志内容同步ES中,可以从操作内容的角度对日志进行全文检索,这里还是通过一些指定字段从关系表中进行的列表查询。查询的日志记录会关联字典信息,也就是表单key,然后我们可以在点击进入详情时选择对应的表单
log/ index.vue 1 2 3 4 5 6 7 8 9 10 11 12 <log-detail v-if ="viewKey === 'log-detail'" ref="log-detail" @ok="handleQuery" /> <log-user v-if ="viewKey === 'log-user'" ref="log-user" @ok="handleQuery" /> <log-dept v-if ="viewKey === 'log-dept'" ref="log-dept" @ok="handleQuery" /> <log-role v-if ="viewKey === 'log-role'" ref="log-role" @ok="handleQuery" /> <log-post v-if ="viewKey === 'log-post'" ref="log-post" @ok="handleQuery" /> handleView(row) { this .viewKey = row.viewKey setTimeout(() => { this .$refs[row.viewKey].show(row.id); }); }
将日志对应到表单之后,我们就可以在获取日志内容展示的时候做一些处理了。这样在日志记录时就不需要关心字段含义,或者内容组织之类的事情了,可以放到放到查询展示时处理,对应的表单必然知道字段含义,以及内容要如何展示,甚至它可以自己补充内容。
系统公告 消息公告是很常见的功能,系统用户可以创建一个消息(也可以系统后台生成消息),然后发送给目标用户或群体,并通过websocket推送给当前在线的目标用户(这里用的socketIo有个问题,由于java服务端的依赖包已经太久没有更新,前端的socket.io-client只能选择对应的老版本)。
我们希望对公告做一个已读信息展示,包括接收者的反馈。办法是添加一个read表,在公告发布时将目标全部翻译成具体的用户,并作为未读记录插入到read表,然后更新公告信息的阅读总数,后面就交给目标用户操作时更新就行了。
系统告警 类似于操作日志,每个告警类型也对应一个表单,可以对告警内容进行区分展示,另外,每个类型也对应一套告警级别判断规则,这样可以在界面针对告警类型设置级别阈值;
对于相同的告警(可以通过关键标识信息合并生成md5来判断),则进行次数累计,如果处理了则重新累计;
数据权限 对于数据权限的想法是先定义,后使用。
所以首先是如何定义数据权限,这个定义可能是与业务强相关的,可以类似前面的思路,将数据权限类型定义成字典,然后将字典值设置为表单key,这样不同类型的数据权限酒可以分开设置规则。在设置规则时有两种方式,一种是直接将数据看表结构暴露给用户设置,但这样不太友好,用户并不关心业务数据背后的表字段,所以好点的做法是通过表单设计,让用户能从业务的角度设置筛选条件。
其次是如何匹配数据权限,可以给角色分配数据权限,也可以给角色的某个菜单权限分配,直接在对应界面选择就行。然后在用户访问接口时,我们可以拦截请求,获取到当前用户的角色,以及菜单权限key,接着从缓存中获取数据权限并进行匹配。在匹配时,应该优先匹配角色菜单,如果没有再匹配角色。可能会匹配到多个权限规则,所以加了一个优先级字段,当然根据实际需求场景,也可以取并集的方式,或者在定义权限的时就进行限制。
对于匹配到的数据权限,最终只能交给业务代码自己来实现,因为数据权限本身就没法脱离开业务。我们可以统一接口定义,制定模板,或者通过Mybatis插件来做一些预处理,但是最终还是要将数据权限规则转化成对应的sql查询条件。
国际化处理 对于界面的静态信息,以及服务端的提示信息,可以直接编辑对应的国际化资源,界面可以在Http消息头中设置参数Accept-Language
来告诉服务端当前使用什么语言;
对于动态菜单的国际化,可以在前端编辑好国际化资源,然后在定义菜单名称时使用国际化的key,比如上面角色菜单权限中所示;
对于字典的国际化,只能添加两种语言名称了,界面在展示时根据语言环境进行选择(但是这样只能两种语言,不过一般也够了)
1 2 3 4 5 6 <template slot-scope="{row: {rank}}" > <template v-for ="item in dict.type.post_level" > <span v-if ="rank === item.value && $i18n.locale==='zh'" >{{ item.value }}/{{ item.label }}</span> <span v-if="rank === item.value && $i18n.locale==='en'">{{ item.value }}/ {{ item.labelEn }}</span> </ template></template>
对于服务一些异常的国际化,也可以将message定义成国际化的key,然后交给异常统一处理器获取出来进行转换
1 2 3 4 5 @NotBlank (message = "user.notnull.name" )private String userName;@NotBlank (message = "user.notnull.account" )private String userAccount;
附录:表结构 具体的表结构关系简单如下:
1.部门信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 create table sys_dept( dept_id bigserial primary key , dept_code character varying (64 ), dept_name character varying (128 ), dept_short character varying (64 ), dept_addr character varying (512 ), dept_phone character varying (64 ), remark character varying (200 ), read_only int2 default 0 , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); comment on table sys_dept is '部门信息' ;comment on column sys_dept.dept_id is '部门id' ;comment on column sys_dept.dept_code is '部门编码' ;comment on column sys_dept.dept_name is '部门名称' ;comment on column sys_dept.dept_short is '部门简称' ;comment on column sys_dept.dept_addr is '部门地址' ;comment on column sys_dept.dept_phone is '部门电话' ;comment on column sys_dept.remark is '备注' ;comment on column sys_dept.read_only is '是否只读 1是 0否' ;
2.部门关系 1 2 3 4 5 6 7 8 9 10 create table sys_dept_tree( dept_id int8 not null , parent_id int8 not null , tree_type int2 default 1 , constraint sys_dept_tree_pkey primary key (dept_id, parent_id, tree_type) ); comment on table sys_dept_tree is '部门关系' ;comment on column sys_dept_tree.dept_id is '部门id' ;comment on column sys_dept_tree.parent_id is '上级部门id' ;comment on column sys_dept_tree.tree_type is '关系类型' ;
3.岗位信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 create table sys_post ( post_id bigserial primary key , post_code varchar (64 ), post_name varchar (64 ) not null , post_level int2 default 1 , post_type varchar (64 ), post_status int2 default 1 , remark character varying (200 ), read_only int2 default 0 , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); comment on table sys_post is '岗位信息' ;comment on column sys_post.post_id is '岗位id' ;comment on column sys_post.post_code is '岗位编码' ;comment on column sys_post.post_name is '岗位名称' ;comment on column sys_post.post_level is '岗位级别' ;comment on column sys_post.post_type is '岗位类型' ;comment on column sys_post.post_status is '岗位状态' ;comment on column sys_post.remark is '备注' ;comment on column sys_post.read_only is '是否只读 1是 0否' ;
4.岗位关系 1 2 3 4 5 6 7 8 9 10 create table sys_post_tree( post_id int8 not null , parent_id int8 not null , tree_type int2 default 1 , constraint sys_post_tree_pkey primary key (post_id, parent_id, tree_type) ); comment on table sys_post_tree is '岗位关系' ;comment on column sys_post_tree.post_id is '岗位id' ;comment on column sys_post_tree.parent_id is '上级岗位id' ;comment on column sys_post_tree.tree_type is '关系类型' ;
5.部门岗位 1 2 3 4 5 6 7 8 9 10 create table sys_dept_post( dept_id int8 not null , post_id int8 not null , is_default int2 default 0 , constraint sys_dept_post_pkey primary key (dept_id, post_id) ); comment on table sys_dept_post is '部门岗位' ;comment on column sys_dept_post.dept_id is '部门id' ;comment on column sys_dept_post.post_id is '岗位id' ;comment on column sys_dept_post.is_default is '是否部门默认岗位' ;
6.菜单信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 create table sys_menu( menu_id bigserial primary key , parent_id bigint default 0 , menu_name character varying (64 ) not null , menu_order integer default 0 , menu_permit character varying (255 ), menu_path character varying (255 ) default '#' , menu_param character varying (255 ), menu_type char (1 ) not null , menu_icon character varying (100 ) default '#' , component character varying (255 ), menu_status int2 default 1 , is_frame int2 DEFAULT 1 , is_cache int2 DEFAULT 1 , is_visible int2 DEFAULT 1 , is_protected int2 DEFAULT 1 , remark character varying (200 ), read_only int2 default 0 , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); comment on table sys_menu is '菜单信息' ;comment on column sys_menu.menu_id is '菜单id' ;comment on column sys_menu.parent_id is '父菜单id' ;comment on column sys_menu.menu_name is '菜单名称' ;comment on column sys_menu.menu_order is '菜单顺序' ;comment on column sys_menu.menu_permit is '权限标识' ;comment on column sys_menu.menu_path is '菜单路径' ;comment on column sys_menu.menu_param is '路径参数' ;comment on column sys_menu.menu_type is '菜单类型:M:目录 C:菜单 B:按钮' ;comment on column sys_menu.menu_icon is '菜单图标' ;comment on column sys_menu.component is '组件路径' ;comment on column sys_menu.menu_status is '菜单状态 1启用 2停用' ;comment on column sys_menu.is_frame is '是否内部链接 1是 0否' ;comment on column sys_menu.is_cache is '是否缓存 1是 0否' ;comment on column sys_menu.is_visible is '是否显示 1是 0否' ;comment on column sys_menu.is_protected is '是否受保护的菜单 1是 0否' ;comment on column sys_menu.read_only is '是否只读 1是 0否' ;
7.角色信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 create table sys_role( role_id bigserial primary key , role_code character varying (100 ) not null , role_name character varying (64 ) not null , role_type character varying (64 ), remark character varying (200 ), read_only int2 default 0 , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); create unique index sys_role_role_code on sys_role(role_code);comment on table sys_role is '角色信息' ;comment on column sys_role.role_id is '角色id' ;comment on column sys_role.role_code is '角色编码' ;comment on column sys_role.role_name is '角色名称' ;comment on column sys_role.role_type is '角色类型' ;comment on column sys_role.remark is '备注' ;comment on column sys_role.read_only is '是否只读 1是 0否' ;
8.角色菜单 1 2 3 4 5 6 7 8 create table sys_role_menu( role_id bigint not null , menu_id bigint not null , constraint sys_role_menu_pkey primary key (role_id, menu_id) ); comment on table sys_role_menu is '角色菜单' ;comment on column sys_role_menu.role_id is '角色id' ;comment on column sys_role_menu.menu_id is '菜单id' ;
9.用户信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 create table sys_user( user_id bigserial primary key , user_code character varying (64 ), user_name character varying (64 ) not null , user_account character varying (64 ) not null , user_passwd character varying (256 ) not null , user_sex int2 default 0 , user_phone character varying (11 ), user_email character varying (128 ), user_status int2 default 1 , rank character varying (64 ), remark character varying (200 ), read_only int2 default 0 , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); create unique index sys_user_user_account on sys_user(user_account);comment on table sys_user is '用户信息' ;comment on column sys_user.user_id is '用户id' ;comment on column sys_user.user_code is '用户编码' ;comment on column sys_user.user_name is '用户名称' ;comment on column sys_user.user_account is '用户账号' ;comment on column sys_user.user_passwd is '用户密码' ;comment on column sys_user.user_sex is '用户性别' ;comment on column sys_user.user_phone is '用户电话' ;comment on column sys_user.user_email is '用户邮箱' ;comment on column sys_user.user_status is '用户状态' ;comment on column sys_user.rank is '职级' ;comment on column sys_user.remark is '备注' ;comment on column sys_user.read_only is '是否只读 1是 0否' ;
10.用户关系 1 2 3 4 5 6 7 8 9 10 create table sys_user_tree( user_id int8 not null , parent_id int8 not null , tree_type int2 default 1 , constraint sys_user_tree_pkey primary key (user_id, parent_id, tree_type) ); comment on table sys_user_tree is '用户关系' ;comment on column sys_user_tree.user_id is '用户id' ;comment on column sys_user_tree.parent_id is '上级用户id' ;comment on column sys_user_tree.tree_type is '关系类型' ;
11.用户部门岗位 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 create table sys_user_dept( id bigserial primary key , user_id int8 not null , dept_id int8 not null , post_id int8 default -1 , is_default int2 default 0 , is_leader int2 default 0 ); create unique index sys_user_dept_uk on sys_user_dept(user_id, dept_id, post_id);comment on table sys_user_dept is '用户部门' ;comment on column sys_user_dept.id is '主键' ;comment on column sys_user_dept.user_id is '用户id' ;comment on column sys_user_dept.dept_id is '部门id' ;comment on column sys_user_dept.post_id is '岗位id' ;comment on column sys_user_dept.is_default is '是否用户默认部门' ;comment on column sys_user_dept.is_leader is '是否部门负责人' ;
12.用户角色 1 2 3 4 5 6 7 8 create table sys_user_role( user_id bigint not null , role_id bigint not null , constraint sys_user_role_pkey primary key (user_id, role_id) ); comment on table sys_user_role is '用户角色' ;comment on column sys_user_role.user_id is '用户id' ;comment on column sys_user_role.role_id is '角色id' ;
13.字典信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 create table sys_dict( id bigserial primary key , parent_code character varying (100 ), dict_code character varying (100 ), dict_label character varying (100 ), dict_en character varying (100 ), dict_value character varying (100 ), value_parser varchar (100 ), value_param varchar (100 ), dict_order int2 default 0 , is_default int2 default 0 , css character varying (100 ), status int2 default 1 , remark character varying (200 ), read_only int2 default 0 , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); create unique index sys_dict_uk on sys_dict(dict_code);comment on table sys_dict is '字典数据' ;comment on column sys_dict.id is '主键' ;comment on column sys_dict.parent_code is '父字典编码' ;comment on column sys_dict.dict_code is '字典编码' ;comment on column sys_dict.dict_label is '字典标签' ;comment on column sys_dict.dict_en is '英文标签' ;comment on column sys_dict.dict_value is '字典值' ;comment on column sys_dict.value_parser is '值转换器' ;comment on column sys_dict.value_param is '转换参数' ;comment on column sys_dict.dict_order is '字典排序' ;comment on column sys_dict.status is '字典状态' ;comment on column sys_dict.is_default is '是否默认' ;comment on column sys_dict.read_only is '是否只读 1是 0否' ;comment on column sys_dict.css is '字典展示样式' ;
14.公告信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 create table sys_notice( notice_id bigserial primary key , notice_title character varying (255 ), notice_status int2 default 0 , notice_type int2 default 0 , notice_level int2 default 0 , content text , is_system int2 default 0 , stat_total int4 default 0 , stat_read int4 default 0 , goals_all int2 default 0 , goals_dept int8 [], goals_role int8 [], goals_user int8 [], publish_time timestamp , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); comment on table sys_notice is '系统公告' ;comment on column sys_notice.notice_id is '公告id' ;comment on column sys_notice.notice_title is '标题' ;comment on column sys_notice.notice_status is '公告状态 0草稿 1已发布 2已撤回 3已删除' ;comment on column sys_notice.notice_type is '公告类型 0公告 1通知' ;comment on column sys_notice.notice_level is '公告等级 0普通 1紧急' ;comment on column sys_notice.content is '公告内容' ;comment on column sys_notice.is_system is '是否系统公告 0否 1是' ;comment on column sys_notice.stat_total is '目标数' ;comment on column sys_notice.stat_read is '已读数' ;comment on column sys_notice.goals_all is '是否全员 0否 1是' ;comment on column sys_notice.goals_dept is '目标单位' ;comment on column sys_notice.goals_role is '目标角色' ;comment on column sys_notice.goals_user is '目标用户' ;
15.已读信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 create table sys_notice_read( id bigserial primary key , notice_id int8 , user_id int8 , read_status int2 default 0 , read_back character varying (512 ), read_time timestamp ); create unique index sys_notice_read_uk on sys_notice_read(user_id, notice_id);comment on table sys_notice_read is '公告已读' ;comment on column sys_notice_read.id is '主键' ;comment on column sys_notice_read.notice_id is '公告id' ;comment on column sys_notice_read.user_id is '用户id' ;comment on column sys_notice_read.read_status is '已读状态' ;comment on column sys_notice_read.read_back is '读反馈' ;comment on column sys_notice_read.read_time is '读时间' ;
16.系统配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 CREATE TABLE sys_config( config_id serial primary key , config_name varchar (100 ) DEFAULT '' , config_key varchar (100 ) DEFAULT '' , config_value varchar (500 ) DEFAULT '' , value_parser varchar (100 ), value_param varchar (100 ), is_default int2 DEFAULT 0 , remark varchar (500 ) DEFAULT NULL , create_user int8 , create_dept int8 , create_time timestamp , update_user int8 , update_dept int8 , update_time timestamp ); create unique index sys_config_config_key on sys_config(config_key);comment on table sys_config is '系统配置' ;comment on column sys_config.config_id is '参数id' ;comment on column sys_config.config_name is '参数名称' ;comment on column sys_config.config_key is '参数键' ;comment on column sys_config.config_value is '参数值' ;comment on column sys_config.value_parser is '值转换器' ;comment on column sys_config.value_param is '转换参数' ;comment on column sys_config.is_default is '是否默认 1是 0否' ;
17.附件信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 create table sys_attach( attach_id bigserial primary key , master_id int8 , attach_group character varying (64 ), attach_type character varying (64 ), attach_name character varying (1024 ), attach_size int8 , attach_path character varying (1024 ), attach_status int2 default 0 , is_protected int2 default 0 , create_time timestamp , expire_set int2 default 0 , expire_time timestamp ); create index sys_attach_master on sys_attach(master_id, attach_type, attach_group);comment on table sys_attach is '附件信息' ;comment on column sys_attach.attach_id is '附件id' ;comment on column sys_attach.master_id is '宿主id' ;comment on column sys_attach.attach_group is '附件分组' ;comment on column sys_attach.attach_type is '附件类型' ;comment on column sys_attach.attach_name is '附件名称' ;comment on column sys_attach.attach_size is '附件大小' ;comment on column sys_attach.attach_path is '附件路径' ;comment on column sys_attach.attach_status is '附件状态 0未生效 1已生效' ;comment on column sys_attach.is_protected is '是否受保护的 0否 1是' ;comment on column sys_attach.create_time is '创建时间' ;comment on column sys_attach.expire_set is '是否设置过期时间 0未设置 1设置' ;comment on column sys_attach.expire_time is '过期时间' ;
18.操作日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 create table sys_log( id bigserial primary key , user_id int8 , dept_id int8 , log_group character varying (64 ), log_type character varying (64 ), log_action character varying (64 ), ip character varying (128 ), url character varying (255 ), log_desc character varying (255 ) default '' , log_content json , log_status int2 default 1 , log_time timestamp ); comment on table sys_log is '操作日志' ;comment on column sys_log.id is '日志id' ;comment on column sys_log.user_id is '用户id' ;comment on column sys_log.dept_id is '部门id' ;comment on column sys_log.log_group is '日志模块' ;comment on column sys_log.log_type is '日志类型' ;comment on column sys_log.log_action is '日志动作' ;comment on column sys_log.ip is '访问ip' ;comment on column sys_log.url is '访问url' ;comment on column sys_log.log_desc is '日志描述' ;comment on column sys_log.log_content is '日志详情' ;comment on column sys_log.log_status is '日志状态' ;comment on column sys_log.log_time is '日志时间' ;
19.系统告警 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 create table sys_alarm( id bigserial primary key , alarm_code character varying (128 ) not null , alarm_type int8 , alarm_level int2 default 1 , source_id int8 , source_name character varying (64 ), source_type character varying (64 ), alarm_status int2 default 0 , alarm_times int4 default 1 , first_time timestamp , last_time timestamp , alarm_desc character varying (255 ), alarm_content json default '{}' , resolve_user int8 , resolve_msg character varying (255 ), resolve_time timestamp , resolve_type int2 default 1 ); create index sys_alarm_alarm_code on sys_alarm(alarm_code);comment on table sys_alarm is '系统告警' ;comment on column sys_alarm.id is '告警id' ;comment on column sys_alarm.alarm_code is '唯一编码' ;comment on column sys_alarm.alarm_type is '告警类型' ;comment on column sys_alarm.source_id is '告警来源id' ;comment on column sys_alarm.source_name is '告警来源名称' ;comment on column sys_alarm.source_type is '告警来源类型' ;comment on column sys_alarm.alarm_level is '告警级别:1提示 2普通 3重要 4严重 5灾难' ;comment on column sys_alarm.alarm_status is '告警状态 0未处理 1已确认 2已解决' ;comment on column sys_alarm.alarm_times is '告警次数' ;comment on column sys_alarm.first_time is '首次告警时间' ;comment on column sys_alarm.last_time is '最后告警时间' ;comment on column sys_alarm.alarm_desc is '告警描述' ;comment on column sys_alarm.alarm_content is '告警内容' ;comment on column sys_alarm.resolve_user is '处理人' ;comment on column sys_alarm.resolve_time is '处理时间' ;comment on column sys_alarm.resolve_msg is '处理意见' ;comment on column sys_alarm.resolve_type is '处理方式:1:手动 2:自动' ;
20.告警类型 1 2 3 4 5 6 7 8 9 10 11 create table sys_alarm_type( id bigserial primary key , type_name character varying (128 ), type_view character varying (128 ), description text ); comment on table sys_alarm_type is '告警类型' ;comment on column sys_alarm_type.id is '主键' ;comment on column sys_alarm_type.type_name is '类型名称' ;comment on column sys_alarm_type.type_view is '类型表单' ;comment on column sys_alarm_type.description is '类型描述' ;
21.告警级别 1 2 3 4 5 6 7 8 9 10 11 12 13 create table sys_alarm_level( id bigserial primary key , alarm_type int8 , alarm_level int2, level_rule json default '{}' , description text ); comment on table sys_alarm_level is '告警类型' ;comment on column sys_alarm_level.id is '主键' ;comment on column sys_alarm_level.alarm_type is '告警类型' ;comment on column sys_alarm_level.alarm_level is '告警级别' ;comment on column sys_alarm_level.level_rule is '级别判断规则' ;comment on column sys_alarm_level.description is '级别描述' ;
22.数据权限 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE TABLE sys_scope( scope_id bigserial primary key , scope_name character varying (255 ), scope_type character varying (64 ), scope_status int2 DEFAULT 1 , content json default '{}' , remark varchar (200 ) ); comment on table sys_scope is '数据权限' ;comment on column sys_scope.scope_id is '权限id' ;comment on column sys_scope.scope_name is '权限名称' ;comment on column sys_scope.scope_type is '权限类型(字典值设置表单)' ;comment on column sys_scope.scope_status is '权限状态' ;comment on column sys_scope.content is '权限规则' ;comment on column sys_scope.remark is '备注' ;
23.角色菜单数据权限 1 2 3 4 5 6 7 8 9 10 11 12 CREATE TABLE sys_role_scope( scope_id bigint not null , role_id bigint not null , menu_permit character varying (255 ), priority int2 DEFAULT 1 , constraint sys_role_scope_pkey primary key (role_id, scope_id, menu_permit) ); comment on table sys_role_scope is '角色数据权限' ;comment on column sys_role_scope.scope_id is '数据权限id' ;comment on column sys_role_scope.role_id is '角色id' ;comment on column sys_role_scope.menu_permit is '菜单权限符' ;comment on column sys_role_scope.priority is '优先级' ;
参考: