拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于JVS物联网平台的可视化门户千人千面改造实践

基于JVS物联网平台的可视化门户千人千面改造实践

去年年中的时候我接了一个智慧园区项目,底座选了JVS物联网平台。项目本身不算复杂,真正让我头疼的是一个看似“无理”的需求:业主方说,不想让每个领导登录后台看到的东西都一样。这句话翻译过来就是,可视化门户要做到“千人千面”。当时我还没太当回事,觉得多套几套模板不就行了?结果后面被现实狠狠教育了一轮:运营人员只想看设备告警和工单,部门主管关心能耗曲线,园区总经理要的是当天经营总览,巡检员得第一时间看到自己负责区域的异常。如果所有人登录JVS物联网后台看到的都是同一套可视化门户,那信息要么冗余到没人看,要么关键指标被淹没。这篇文章我打算把整个改造过程完整拆开,从权限模型到动态页面渲染,再到数据权限隔离,一步一步讲清楚,给正在做物联网平台二次开发、或者准备用JVS搭后台门户的同学做个参考。

1. 为什么可视化门户一定要做千人千面

1.1 一个“统一门户”版本把我坑惨了的项目

最早赶工期,我图省事,做了一个“标准版门户”。所有用户登录后,看到的菜单、首页卡片、大屏入口完全一样。当时想的是:反正功能都在,大家自己找呗。结果交付后三个月,问题啪啪打脸。园区运营主管直接跟我说:“我每天只要看设备告警和工单,你给我整这么多能耗曲线干什么?”另一边,财务总监想看电费分项统计,翻了三层菜单才找到入口,打电话吐槽系统难用。更严重的一次,巡检员登录大屏展示页,因为统一门户把管理端删除按钮也暴露了出来,鼠标一抖差点把生产设备给误删了。

这种“一人一套全功能”的做法,还有一个很隐蔽的坑:看起来功能都在,实际上没人愿意用。大屏沦为装饰,后台变成无人问津的数据仓库。我做复盘的时候总结了三个根本问题:一是信息过载,角色和页面之间没有匹配关系,每个用户都被迫看大量和自己无关的内容;二是权限风险,菜单按钮全量展示,低权限用户也能看到高权限入口,误操作和越权访问只隔了一个点击;三是维护成本,业务方每次提“加一个卡片”的需求,我都得去改模板、发版本,累得半死。归根到底,问题不在功能不够,而在于门户的“供给”和用户的“需求”完全错位了。

1.2 千人千面的本质:把页面当数据管

痛定思痛,我开始琢磨“千人千面”到底应该怎么做。传统思路是给每个角色复制一套页面模板,管理员一套、运营一套、访客一套。这个做法一开始看起来简单,但做深了就发现维护成本高得离谱:加一个卡片要改三套模板,加一个菜单要同步配置四次,改到最后连我自己都分不清哪套模板是新的。后来我换了一个思路,把页面本身当成“数据”来管。

具体来说,菜单、首页布局、卡片组件、主题颜色、大屏入口,这些原本写死在代码里的东西,全部改成配置项存到数据库。用户登录后,后端先拉取这个人的“门户配置”,包括他有权限的菜单树、他的首页布局JSON、他的主题偏好,然后前端通过一个渲染引擎把配置变成实际界面。这样千人千面的本质就变了:一份权限数据加一份布局配置,渲染出一个专属门户。你可以把这件事理解成手机桌面:同一个人可以自定义壁纸、图标排列、小组件,换了账号就是另一套桌面。JVS物联网平台本身就有角色权限、菜单管理、大屏设计这些模块,我们要做的不是推翻重来,而是把这些能力串成一条链路,让页面从“写死的前端路由”变成“动态组装的数据产物”。

1.3 别把千人千面和“多套模板”混为一谈

这里要特别强调一下:千人千面不等于做多套模板。模板再多,也是有限的、静态的,而且每套模板之间还需要同步维护。千人千面是配置驱动的,用户可以在权限范围内自己调整布局,管理员可以随时修改某个角色的默认门户,后端配置一变,前端立即生效,不需要发版。我在项目里做完这套机制之后,业务方再提“给运营加一个今日能耗卡片”,我只需要在配置中心拖一个组件进去,绑定数据源,保存,五分钟搞定。这在以前简直不敢想。所以,如果你准备做类似改造,一定要先确立“配置化”这个核心思路,而不是一头扎进去写模板页面。

2. 思路拆解:JVS物联网里实现千人千面的架构路径

2.1 权限模型是地基:RBAC 不够,还要加数据维度

JVS物联网使用的是经典的RBAC权限模型,也就是用户-角色-权限这套体系。用户属于一个或多个角色,角色绑定菜单权限、按钮权限,这套模型是所有门户功能的地基。刚开始我以为只要把菜单权限配好就万事大吉,后来发现远远不够。

举个例子:运营主管和部门经理都能看到“设备管理”这个菜单,但运营主管只能看A车间的设备,部门经理却能看全园区的设备。这种差异在菜单权限层面根本区分不出来。所以我在权限模型里又加了一层“数据权限”,包含两个维度:机构维度和设备分组维度。角色配置里可以设置数据范围,比如本人、本部门、全部,再结合设备分组表做过滤。这样一来,菜单权限解决的是“入口能不能看见”,数据权限解决的是“入口进去了能看多少数据”,两者必须配合使用。

2.2 页面即配置:从静态路由到动态可配置

JVS的前端默认是用路由表静态定义菜单的,菜单长什么样是写死在代码里的。我要做的第一个核心改动,就是把这部分数据化。系统管理里维护一份“资源目录”,资源可以是菜单、页面、卡片、大屏,然后再把资源目录和角色绑定。前端启动时不再直接加载全量路由,而是先请求一个门户配置接口,后端根据当前登录用户的角色返回有权限的菜单树,前端再通过动态路由机制把页面组件挂载上去。

首页也是同样的思路。我把首页的布局存成一份JSON配置,里面包含每个区域放哪个组件、组件的参数是什么、数据源是哪个接口。用户保存布局后,前端根据JSON去渲染。这样做有个直接好处:不同角色登录,看到的首页结构可以完全不同。巡检员的首页可能是告警列表加地图,领导的首页可能是综合态势大屏入口加核心指标卡,而这一切都是配置出来的,不是代码写死的。

2.3 数据联动:看到的东西和能看到的数据必须一致

只做菜单和布局还不够,如果用户能看到“能耗分析”这个卡片,但卡片里的数据是全园区的,那就等于把数据权限绕过了。所以我在设计配置结构时,给每个卡片组件都加了一个“数据域”字段。组件加载数据时必须带上这个数据域,后端接口根据数据域解析出对应设备分组,再拼接权限过滤条件。

举个例子,同一个能耗统计接口,A用户传的是deviceGroupId=1001,B用户传的是deviceGroupId=1002,后端拼出来的SQL不同,返回的数据自然不同。这一步是整个改造里最容易漏的,但恰恰是“千人千面加数据安全”的核心闭环。如果漏掉,就会出现低权限用户改一下前端请求参数,就能看到全园区数据的严重漏洞。

2.4 这套方案的适用范围与边界

也不是所有项目都需要做成千人千面。我后来总结了一下,如果你的系统只有两三种固定角色,页面总共不超过十个,用户群体统一,那做一套模板完全够用,做成千人千面反而是过度设计。但如果系统面向的角色多、数据敏感度高、业务方又经常调整展示内容,那就值得投入做配置化门户。另外,纯对外展示的可视化大屏,比如园区展厅那种固定循环播放的,也不适合做千人千面,因为观看者不是登录用户,没有个性化需求。判断标准就一条:用户需不需要在登录后按自己的身份和偏好看到不同的东西?需要,就做;不需要,别给自己加戏。

3. 实操过程:一步步把 JVS 门户改成千人千面

3.1 基础准备:先把用户体系和资源目录建好

在动手写代码之前,我先花了两天时间把系统里的用户、角色、组织架构彻底梳理了一遍。这个环节看起来不产生代码,但实际上决定了后续所有配置的合理性。我把真实业务角色分成了五类:平台管理员、园区运营、部门主管、设备巡检、访客。然后按角色去划分资源目录。

资源目录我建议这样组织:一级资源是功能模块,比如工作台、可视化大屏、设备管理、告警中心、能耗分析、系统设置;二级资源是模块下的具体页面,比如大屏下面的综合态势、能耗大屏、安防大屏;三级资源是页面上的操作按钮,比如新增、删除、导出。这些资源最终都会绑定到角色上。JVS系统管理里本身就有角色权限配置功能,直接勾选菜单目录和按钮权限就行,不需要改一行代码。这里我踩了一个坑:一开始我把大屏页面和后台页面混在一个菜单里,结果访客角色也能看到系统设置。后来我把资源目录分成了PC门户资源和大屏展示资源两个独立分组,权限才真正好配。

3.2 配置角色权限:把菜单和按钮先收口

角色权限配置我强烈推荐“最小授权”原则。操作路径很简单:系统管理、角色管理、找到对应角色、分配权限。但一定不要图省事直接全选,而是先全部取消,再一项一项按需添加。我给巡检员角色只勾选了工作台、告警中心、设备管理下的设备列表,以及告警查看按钮,设备编辑、导出Excel这些权限一律不给。这样做的直接收益有两个:一是页面干净了,巡检员登录后第一眼看到的就是自己负责区域的告警卡片;二是误操作入口被彻底堵死,想删设备都没有按钮。

配置完成后一定要记得清缓存。JVS的权限缓存默认是登录态内存缓存,改了角色权限不清理,就会出现“权限改了但用户还是能访问”的假象。我在项目里踩过这个坑,当时以为代码逻辑有问题,排查了半天,最后发现是缓存没刷新,差点把自己整崩溃。

3.3 设计首页布局:拖拽卡片与布局JSON

JVS可视化门户的首页是支持自定义布局的,在门户设置里可以拖拽卡片、调整大小和区域。我用这个功能把默认首页改造成了几套典型的角色布局:运营管理首页是顶部告警摘要、中间设备在线率、底部今日工单;领导视察首页是全屏大屏入口加园区综合指标;访客首页则是园区介绍、开放区域地图和欢迎页。

拖拽完成后,页面会生成一个布局JSON,这个JSON我建议一定要保存到数据库,而不是留在浏览器本地。因为不同用户登录时,后端要根据角色下发不同的布局。在JVS中,用户保存的布局会存在用户偏好表里,这里要特别注意一个优先级逻辑:默认布局和用户自定义布局是两套数据,如果用户没有自定义过,就走角色默认布局;如果用户自己调整过,就优先取用户自己的配置。这个优先级关系千万不要搞反,否则会出现“管理员改了默认布局,但所有用户都不变”的诡异现象。

3.4 绑定数据源与设备分组:让卡片“各回各家”

布局只是骨架,真正让门户千人千面的是卡片背后绑定的数据。每个首页卡片,比如设备在线率、告警数量、能耗趋势,在JVS大屏设计器里都对应一个可视化组件,组件绑定数据接口。我在创建设计器组件时,会把数据源参数设为带设备分组的接口,类似 /iot-data/dashboard/overview?deviceGroupId=xxx。

后端的逻辑是:先通过当前登录人的数据权限解析出允许访问的设备分组ID集合,再和请求参数里的ID求交集。如果交集为空,直接返回空数据,而不是报错。这样即使有人手动去改URL参数,也拿不到越权数据。我在这个环节专门做了测试,用低权限账号试着访问高权限分组ID,结果返回的都是空,心里才踏实。

3.5 主题皮肤与偏好设置:最后的印象分

千人千面如果只做到菜单和数据,用户还是会觉得“换汤不换药”。JVS系统设置里有主题配置,深色、浅色、品牌色都有。我把它拆成了用户级偏好,用户在门户右上角可以直接切换主题,切换时选择记住偏好,之后每次登录都按用户偏好渲染。另外,我还把默认展示的大屏也做成了用户级配置:巡检员登录默认跳转设备告警大屏,领导登录默认跳转综合态势大屏。这两个小细节一加,不同角色的第一屏体验立刻不一样,领导们的印象分一下子拉高了。

实现上很简单,登录接口返回的UserInfo里加一个portalPreference字段,前端拿到后优先应用;如果用户没设置过,就使用后台的角色默认值。这一步没有技术难度,但产品体验的提升非常明显,属于典型的高性价比改造。

4. 核心代码与配置细节(这里最干)

4.1 后端动态菜单接口:一次登录,把门户配置拉齐

我在JVS的登录成功处理器里加了一个门户聚合接口,把菜单、首页布局、主题偏好一次性返回给前端,避免前端发四五次请求去拼页面。接口大致长这样:

@GetMapping("/portal/config") public R<PortalConfigVO> getPortalConfig() { LoginUser user = SecurityUtils.getLoginUser(); PortalConfigVO vo = new PortalConfigVO(); // 1. 拉取菜单树(按角色过滤) vo.setMenus(menuService.listMenuByUserId(user.getUserId())); // 2. 拉取角色默认首页布局,用户自定义优先 LayoutConfig layout = layoutService.getUserLayout(user.getUserId()); if (layout == null) { layout = layoutService.getRoleLayout(user.getRoleIds()); } vo.setLayout(layout); // 3. 主题偏好 vo.setTheme(userService.getThemePreference(user.getUserId())); // 4. 数据权限范围,供前端做按钮级控制 vo.setDataScope(permissionService.getDataScope(user.getUserId())); return R.ok(vo); }

前端拿到这份配置后,先注册菜单路由,再渲染首页布局,再应用主题,一次把门户完整拼出来。这个接口最大的好处是,在登录后的第一秒就能决定用户看到什么,不会出现长时间的白屏闪烁。

4.2 布局 JSON 的结构设计:别把所有东西塞一个数组

首页布局到底怎么存?我经历了三次迭代,最终确定用“栅格加卡片”的结构。栅格负责位置和大小的描述,卡片负责具体内容和数据源。一个典型的布局JSON长这样:

{ "rowNum": 12, "gutter": 16, "widgets": [ { "id": "widget-alarm-sum", "type": "alarmSummary", "title": "今日告警", "x": 0, "y": 0, "w": 6, "h": 2, "dataSource": { "api": "/iot-data/dashboard/alarm-summary", "deviceGroupId": "1001", "refreshInterval": 30 }, "visibleRoleIds": ["role_operator", "role_manager"] }, { "id": "widget-energy-trend", "type": "lineChart", "title": "能耗趋势", "x": 6, "y": 0, "w": 6, "h": 2, "dataSource": { "api": "/iot-data/dashboard/energy-trend", "deviceGroupId": "1002", "refreshInterval": 60 } } ] }

关键点有两个。第一,每个widget都有独立的dataSource,包括接口地址、设备分组、刷新间隔,这样卡片级的数据权限才能落地。第二,widget里带有visibleRoleIds,前端渲染布局时会再筛一遍,把没有权限的卡片直接隐藏。后端保存布局时我也会校验权限,防止低权限角色把高权限的卡片加到自己的布局里再保存。

4.3 前端动态渲染:Vue 路由 + 动态组件

JVS前端用的是Vue技术栈,动态路由我参考了常见的路由守卫方案。每次路由跳转前,检查当前监听的路由表是否已经注册了该用户的路由,如果没有,就用门户接口返回的菜单数据生成路由追加进去。核心逻辑大概是这样:

router.beforeEach((to, from, next) => { const portalConfig = store.getters.portalConfig; if (!portalConfig) { // 尚未拉取门户配置,先请求并写入store return dispatch('loadPortalConfig').then(() => { next({ ...to, replace: true }); }); } next(); });

动态组件渲染首页卡片时,不要预先引入所有图表组件,而是用Vue的异步组件加映射表按需加载。比如type为lineChart时,加载components/dashboard/LineChart.vue;type为alarmSummary时,加载AlarmSummary.vue。这样做的好处是首屏不会把所有大屏组件打包进来,页面加载速度能明显提升,尤其是组件库比较重的时候,这个优化效果非常可观。

4.4 数据权限的 SQL 拦截:一个注解搞定

数据权限我是在MyBatis层做的拦截,给查询接口加了一个自定义注解@DataScope,拦截器解析当前用户的数据权限范围,然后自动拼接SQL条件。核心思路是:

@Aspect @Component public class DataScopeAspect { @Before("@annotation(dataScope)") public void doBefore(JoinPoint point, DataScope dataScope) { // 解析当前用户的设备分组ID集合 List<Long> groupIds = permissionService.getDeviceGroupIds(); // 放入ThreadLocal,供SQL拦截器使用 DataScopeContext.setGroupIds(groupIds); } }

配合MyBatis拦截器,在SQL解析阶段发现表名匹配设备关联表,就自动追加where条件。这样就算开发人员写SQL时忘了加过滤条件,数据权限依然生效。这个做法在交付后的安全测试里帮了大忙,测试人员拿着低权限账号试了一下午,也没越权查出任何一条数据。

5. 常见问题与排查技巧实录

做这套东西过程中踩过的坑不少,我把最典型的几个问题和排查思路整理成了速查表,方便你直接对着查:

现象排查顺序常见根因
权限改了但用户还是老界面清缓存、退登录、重新登录权限缓存、会话未刷新
刷新页面后白屏看动态路由初始化、看菜单权限路由表重建逻辑缺失
卡片能渲染但数据为空看接口、看参数、看数据权限数据权限与分组冲突
门户首屏加载慢看接口次数、组件体积、SQL慢日志配置查询多、没有按需加载

5.1 权限配置改了,用户登录还是老界面

这个问题几乎每个项目都会遇到。JVS的权限是登录时就加载到会话里的,后端改了角色权限,已经在线的用户不会自动刷新,必须退出重新登录,或者等会话过期才能看到变化。我在项目里加了一个“强制刷新权限”的接口,调用后清除该用户的登录态缓存,再配合前端重新拉取portalConfig,就能让配置立即生效。另外,如果系统里用了Redis做缓存,还要把角色权限对应的缓存键一起清理,否则后端判断按钮权限时还是会命中旧数据。

5.2 动态路由导致刷新白屏

动态路由一个最常见的坑是:用户直接刷新浏览器页面时,地址栏里的路由还在,但动态注册的路由表在内存里还没初始化,前端匹配不到组件,于是白屏。解决方法是全局路由守卫里加一个判断:如果当前路由的name在权限菜单里找不到,就重新从后端拉取portalConfig并重新注册动态路由,再重定向到目标路由。这个判断必须做“找得到就不管,找不到才重新拉取”,否则会陷入无限重定向循环,页面直接卡死。

5.3 卡片显示了,但数据全是空的

布局里的卡片能正常渲染,但数据一直为空,这个问题的排查顺序一般是:先看数据源接口是否报错,再看设备分组ID参数是否传对,最后看数据权限是否把该分组过滤掉了。我遇到最多的是第三种情况:巡检员角色配置的数据权限只包含本部门,但卡片绑定的设备分组属于另一个部门,后端计算交集为空,自然返回空数据。这种问题不是开发Bug,而是配置冲突。解决办法是在角色数据权限配置页把对应设备分组加上,或者把卡片改成绑定到该用户有权限的设备分组上,二选一。

5.4 门户页面加载慢,首屏要等好几秒

门户加载变慢,大概率是三个原因叠加:门户配置接口查询了太多表、前端一次性注册了几十个大屏路由、卡片接口并发请求太多。我优化时做了三件事:门户配置接口增加Redis缓存,五分钟过期;前端路由改成按需注册,打开哪个大屏再注册哪个;卡片接口统一改成批量接口,一个接口返回一整屏所需的数据,减少HTTP连接数。优化完之后,首屏从四秒多降到了1.5秒以内,领导打开大屏再也没吐槽转圈了。

我自己做完这个改造,最深的体会是:千人千面不是炫技功能,它本质上是权限模型、配置数据和前端渲染三者协同的结果。如果你只把精力放在拖拽组件和页面样式上,忽略了数据权限和动态菜单这两层地基,那做出来的门户一定会在真实业务里出各种幺蛾子。JVS物联网平台给了很好的底座,菜单、角色、大屏、可视化组件都是现成的,关键是把它们串起来的这条链路设计清楚。最后分享一个小技巧:做门户配置前,先把每个角色的“第一屏”用纸质原型画出来,让业务方签字确认,再回填到配置中心。这样能少走很多返工的弯路,毕竟业务眼里的“改一下”和开发眼里的“改一下”,往往不是同一个工作量。

返回列表