1. 项目背景与需求拆解1.1 数据资产登记到底解决什么问题这两年大家都在聊数据资产化企业里数据到底有多少、分布在哪、谁在用、质量怎么样很多公司其实是说不清楚的。我见过不少企业业务系统十几个数据库七八套数据字典散的散、缺的缺新来一个开发要摸清数据家底得花两周。数据资产登记系统就是为了解决这个家底不清的问题——把分散在各业务系统的数据资源统一登记、统一编目、统一管理让企业知道自己的数据资产有哪些、长什么样、归谁管。我做的这个项目是一个典型的企业级数据资产管理基础设施技术栈是Spring Boot Vue的前后端分离架构核心功能围绕登记二字展开数据资产的注册、分类、元数据维护、责任人管理、状态跟踪和检索盘点。适合正在做数据治理相关工作的团队参考也适合需要用完整项目练手的后端或全栈开发者。1.2 需求边界与核心功能清单做这类系统最怕一开始就把需求铺太大。数据资产管理可以延伸出数据质量、数据血缘、数据服务、数据安全分级一大堆方向但登记系统的定位就是先把资产台账做扎实。我在设计阶段把需求收敛成了六个核心模块数据资产登记录入资产基本信息包括资产名称、所属系统、数据类型、存储位置、数据量级、责任人等。资产分类管理支持按业务域、系统来源、数据形态结构化/非结构化多维度分类。元数据管理维护字段级信息包括字段名称、类型、业务含义、敏感级别。资产检索与盘点支持关键字搜索、组合筛选、资产列表导出。审批流程资产登记、变更需要走审批避免随意修改台账。系统管理用户管理、角色权限、操作日志。这个范围看起来不大但每个模块落地起来都不轻松。特别是元数据管理和权限控制这两个部分属于看着简单做起来复杂的典型。2. 技术选型与架构设计2.1 为什么用Spring Boot Vue这个组合这个组合在今天已经不新鲜但依然是最稳妥的选择关键在于它的生态成熟度和人才供给。后端用Spring Boot集成MyBatis Plus操作数据库Apache Shiro或Spring Security做权限控制这部分Java生态里方案非常成熟遇到问题网上一抓一大把。前端用Vue 2 Element UI组件库丰富后台管理系统的常用界面基本都能直接拼出来开发效率非常高。我选择Spring Boot 2.x版本搭配JDK 8不是追求新版本而是因为企业环境里JDK 8的普及率最高部署起来最省事。数据库用MySQL 5.7存储引擎InnoDB这套组合在中小型项目里久经考验稳定性和性能都足够。2.2 前后端分离架构的模块划分项目采用标准的前后端分离结构后端工程server ├── src/main/java │ ├── controller # 接口层 │ ├── service # 业务逻辑层 │ ├── mapper # 数据访问层 │ ├── entity # 实体类 │ ├── config # 配置类权限、跨域等 │ └── common # 公共工具、统一返回结果 ├── src/main/resources │ ├── mapper # MyBatis XML映射文件 │ ├── application.yml │ └── sql # 数据库初始化脚本 前端工程web ├── src │ ├── api # 接口请求封装 │ ├── views # 页面组件 │ ├── router # 路由配置 │ ├── store # 状态管理 │ └── utils # 工具函数前后端通过JSON格式交互接口路径统一以/api开头后端通过拦截器做token校验。前端打包成静态文件后由Nginx托管反向代理转发接口请求到后端服务。2.3 数据库设计与核心表结构数据库设计是整个系统的地基。我设计了九张核心表这里挑几张重点说。资产主表asset_infoCREATE TABLE asset_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, asset_code varchar(50) NOT NULL COMMENT 资产编号, asset_name varchar(200) NOT NULL COMMENT 资产名称, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, source_system varchar(100) DEFAULT NULL COMMENT 来源系统, data_type varchar(20) DEFAULT NULL COMMENT 数据类型结构化/半结构化/非结构化, storage_location varchar(500) DEFAULT NULL COMMENT 存储位置, data_volume varchar(50) DEFAULT NULL COMMENT 数据量级, owner_dept varchar(100) DEFAULT NULL COMMENT 所属部门, owner_user varchar(50) DEFAULT NULL COMMENT 责任人, asset_status varchar(20) DEFAULT draft COMMENT 状态draft/approving/approved/rejected, description text COMMENT 资产描述, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, create_by varchar(50) DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (asset_status) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT数据资产信息表;元数据表asset_metadataCREATE TABLE asset_metadata ( id bigint(20) NOT NULL AUTO_INCREMENT, asset_id bigint(20) NOT NULL COMMENT 关联资产ID, field_name varchar(100) NOT NULL COMMENT 字段名称, field_type varchar(50) DEFAULT NULL COMMENT 字段类型, field_comment varchar(500) DEFAULT NULL COMMENT 业务含义, sensitive_level varchar(10) DEFAULT L1 COMMENT 敏感级别L1-L4, is_primary_key tinyint(1) DEFAULT 0 COMMENT 是否主键, sort_order int(11) DEFAULT 0 COMMENT 排序, PRIMARY KEY (id), KEY idx_asset (asset_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资产元数据表;注意资产编号建议使用规则化编码比如DA-202405-0001这样盘点时一眼就能看出资产的登记时间和序号。我踩过坑一开始用自增id当编号后来发现业务部门根本记不住还是要用有业务含义的编号。权限相关表是标准的五表模型用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。这套模型很多开源项目都有现成实现但实际落地时要注意菜单和按钮权限的粒度划分后面我会详细说。3. 后端核心功能实现3.1 用户认证与权限控制的实现方案认证这块我用了JWT 拦截器的方式没有引入Spring Security那么重的框架。JWT的好处是服务端无状态适合前后端分离的场景。具体实现路径用户登录成功后后端生成token返回给前端前端把token存在本地每次请求都在请求头带上Authorization: Bearer token。后端拦截器拦截所有/api/**请求校验token的合法性和有效期。核心校验逻辑Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String uri request.getRequestURI(); if (uri.contains(/login) || uri.contains(/captcha)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token) || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录或登录已过期); } String jwtToken token.substring(7); // 解析token校验签名和过期时间 Claims claims JwtUtil.parseToken(jwtToken); if (claims null) { throw new BusinessException(401, Token无效); } // 将用户信息放入ThreadLocal供后续业务使用 UserContext.setUserId(Long.valueOf(claims.get(userId).toString())); return true; } }权限控制采用注解方式在需要权限的接口上标注RequiresPermission(asset:add)通过AOP切面拦截来校验当前用户是否拥有对应权限点。这里有一个经验权限点尽量细化到按钮级别而不是只控制菜单。因为在数据资产登记这类系统里有些用户能看列表但不能编辑能编辑但不能删除只控制菜单层级根本做不到这种细粒度管控。3.2 数据资产登记流程的实现登记流程是系统的核心业务。我设计的状态流转是草稿 → 待审批 → 已发布/已驳回 → 变更中。为什么引入审批环节因为数据资产台账是给全公司看的如果谁都能随便改台账很快就不可信了。登记接口的核心逻辑Service public class AssetServiceImpl implements AssetService { Override Transactional(rollbackFor Exception.class) public Long createAsset(AssetDTO dto) { // 1. 生成资产编号 String assetCode generateAssetCode(); // 2. 保存资产主表 AssetInfo entity BeanUtil.copyProperties(dto, AssetInfo.class); entity.setAssetCode(assetCode); entity.setAssetStatus(draft); entity.setCreateBy(UserContext.getUserId().toString()); this.save(entity); // 3. 保存字段级元数据 if (CollectionUtils.isNotEmpty(dto.getMetadataList())) { for (MetadataDTO meta : dto.getMetadataList()) { AssetMetadata md new AssetMetadata(); md.setAssetId(entity.getId()); md.setFieldName(meta.getFieldName()); md.setFieldType(meta.getFieldType()); md.setFieldComment(meta.getFieldComment()); md.setSensitiveLevel(meta.getSensitiveLevel()); metadataMapper.insert(md); } } // 4. 插入审批记录 approvalMapper.insert(new AssetApproval(entity.getId(), create, 待审批)); return entity.getId(); } }关键点整个登记过程必须在一个事务里完成主表、元数据、审批记录要么同时成功要么同时回滚。我之前遇到过主表保存成功但元数据保存失败结果出现资产没有字段信息的脏数据排查了半天才发现是事务没加对。审批环节我设计成了独立的审批记录表asset_approval包含资产ID、审批类型登记/变更/注销、审批意见、审批人等。审批人角色默认是系统管理员也可以配置为数据管理部门的特定账号。3.3 元数据管理的前后端联动设计元数据是数据资产登记系统区别于普通资产管理系统的重要特征。普通资产只管有没有这个东西元数据要管这个东西里面具体是什么。字段级的元数据管理界面是表格形式支持动态增删行前端用Element UI的el-table配合可编辑单元格实现。用户登记资产时可以一个字段一个字段地录入录入完保存后后端批量插入元数据表。批量保存的逻辑Override Transactional(rollbackFor Exception.class) public void batchSaveMetadata(Long assetId, ListMetadataDTO list) { // 先删除原有元数据 metadataMapper.deleteByAssetId(assetId); // 再批量插入 for (int i 0; i list.size(); i) { MetadataDTO dto list.get(i); AssetMetadata entity new AssetMetadata(); entity.setAssetId(assetId); entity.setFieldName(dto.getFieldName()); entity.setFieldType(dto.getFieldType()); entity.setFieldComment(dto.getFieldComment()); entity.setSensitiveLevel(dto.getSensitiveLevel()); entity.setIsPrimaryKey(dto.getIsPrimaryKey()); entity.setSortOrder(i 1); metadataMapper.insert(entity); } }为什么先删后插而不是一条条更新因为前端是整体编辑提交的用户可能删除了某一列也可能调整了顺序。先删后插简单可靠元数据量通常不会太大性能开销可以忽略。敏感级别这个字段值得单独说一下。我设计了L1到L4四个级别L1是完全公开、L2是内部公开、L3是敏感级、L4是机密级。后续做数据安全管控、脱敏策略时这个字段就是基础依据。在做资产登记时就把敏感级别标清楚后面做数据安全评估会省非常多的事。3.4 资产检索与盘点功能的设计检索功能我用的MyBatis Plus的Wrapper组合条件查询。页面提供关键字输入框和多个筛选下拉框后端构造动态查询条件Override public IPageAssetVO searchAsset(AssetQuery query) { PageAssetInfo page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperAssetInfo wrapper new LambdaQueryWrapper(); // 关键字模糊搜索 if (StringUtils.isNotBlank(query.getKeyword())) { wrapper.and(w - w .like(AssetInfo::getAssetName, query.getKeyword()) .or().like(AssetInfo::getAssetCode, query.getKeyword()) .or().like(AssetInfo::getSourceSystem, query.getKeyword())); } // 分类筛选 if (query.getCategoryId() ! null) { wrapper.eq(AssetInfo::getCategoryId, query.getCategoryId()); } // 状态筛选 if (StringUtils.isNotBlank(query.getStatus())) { wrapper.eq(AssetInfo::getAssetStatus, query.getStatus()); } // 时间范围筛选 if (query.getStartTime() ! null) { wrapper.ge(AssetInfo::getCreateTime, query.getStartTime()); } IPageAssetInfo result this.page(page, wrapper); // 转换成VO补充分类名称、审批状态等展示信息 return result.convert(this::toVO); }盘点功能就是把这套查询能力做成资产清单导出。Excel导出用EasyExcel一个注解就能完成字段映射Data public class AssetExportVO { ExcelProperty(资产编号) private String assetCode; ExcelProperty(资产名称) private String assetName; ExcelProperty(来源系统) private String sourceSystem; ExcelProperty(数据类型) private String dataType; ExcelProperty(所属部门) private String ownerDept; ExcelProperty(责任人) private String ownerUser; ExcelProperty(状态) private String assetStatus; }导出时要注意数据量一次性导出全量可能内存溢出。我做了一个限制单次最多导出1万条超过就提示先按条件筛选。4. 前端Vue页面实现与细节处理4.1 前端工程结构与路由设计前端用Vue CLI脚手架创建Vue 2.6版本。路由设计按模块划分const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/layout/Layout.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/Dashboard.vue) }, { path: asset/list, name: AssetList, component: () import(/views/asset/AssetList.vue) }, { path: asset/create, name: AssetCreate, component: () import(/views/asset/AssetCreate.vue) }, { path: asset/detail/:id, name: AssetDetail, component: () import(/views/asset/AssetDetail.vue) }, { path: category, name: CategoryManage, component: () import(/views/category/CategoryManage.vue) }, { path: approval, name: ApprovalList, component: () import(/views/approval/ApprovalList.vue ) }, { path: system/user, name: UserManage, component: () import(/views/system/UserManage.vue) }, { path: system/role, name: RoleManage, component: () import(/views/system/RoleManage.vue) } ] } ]路由守卫用来做登录拦截router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else { if (!token) { next(/login) } else { next() } } })实际项目里路由守卫不要只判断有没有token还要判断用户是否有访问当前页面的权限。我一开始图省事只做了token判断后来发现用户直接改URL就能访问没权限的页面只能再补一层权限判断。4.2 核心登记表单的校验与实践资产登记页面是系统使用频率最高的页面交互体验直接影响使用意愿。我用el-form的表单校验功能加了比较完整的规则rules: { assetName: [ { required: true, message: 请输入资产名称, trigger: blur }, { min: 2, max: 100, message: 长度在2到100个字符之间, trigger: blur } ], categoryId: [{ required: true, message: 请选择资产分类, trigger: change }], dataType: [{ required: true, message: 请选择数据类型, trigger: change }], ownerUser: [{ required: true, message: 请选择责任人, trigger: change }], metadataList: [ { type: array, required: true, min: 1, message: 至少录入一个字段元数据, trigger: change } ] }字段级元数据的录入用了动态表格每行有字段名、类型、描述、敏感级别四个输入控件。这里有个交互细节新增行时自动给字段名做非空校验如果用户填了一半切到别的行就提示避免提交时才发现一堆空行。表单提交时的整体校验逻辑handleSubmit() { this.$refs.assetForm.validate((valid) { if (valid) { // 额外校验元数据表 if (this.metadataList.length 0) { this.$message.warning(请至少录入一个字段元数据) return } submitAsset(this.formData).then(res { this.$message.success(登记成功待审批) this.$router.push(/asset/list) }) } else { this.$message.error(请检查表单填写是否完整) } }) }4.3 仪表盘与资产态势展示首页仪表盘用了ECharts做可视化展示四块内容资产总量、分类分布饼图、近期登记趋势、状态分布。这些数据通过一个聚合接口返回Override public DashboardVO getDashboardData() { DashboardVO vo new DashboardVO(); // 资产总数 vo.setTotalCount(assetMapper.selectCount(null)); // 按状态分组统计 ListMapString, Object statusStats assetMapper.countByStatus(); vo.setStatusStats(statusStats); // 按分类分组统计 ListMapString, Object categoryStats assetMapper.countByCategory(); vo.setCategoryStats(categoryStats); // 最近7天登记趋势 ListMapString, Object trend assetMapper.countLast7Days(); vo.setTrendData(trend); return vo; }ECharts初始化时有个坑数据是异步加载的如果图表实例在数据回来之前就渲染会拿不到数据。正确做法是先初始化一个空实例再用setOption把异步加载的数据塞进去。另外容器要有明确的宽高否则图表显示不出来。关于图表类型的选择我的经验是分类分布用饼图趋势变化用折线图状态对比用横向柱状图这样业务方看着最直观。5. 部署流程与演示环境搭建5.1 后端打包与环境配置部署文件我一般整理成一套标准目录拿到手就能用deploy/ ├── server.jar ├── application-prod.yml ├── init.sql ├── nginx.conf └── README.md生产环境配置单独放在application-prod.yml里关键配置用${}占位符从环境变量读取避免把密码写死在配置文件里spring: datasource: url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:data_asset_db}?useUnicodetruecharacterEncodingutf8useSSLfalse username: ${DB_USER:root} password: ${DB_PASSWORD:root}打包命令用Maven跳过测试加快打包速度mvn clean package -DskipTests打完包确认target目录下生成了server.jar用java -jar server.jar --spring.profiles.activeprod启动测试。5.2 Nginx配置与前后端联调部署前端npm run build产物在dist目录把这个目录放到Nginx的html路径下。Nginx配置要做两件事静态资源托管和接口反向代理server { listen 80; server_name localhost; # 前端静态资源 location / { root /opt/data-asset/web; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件等静态资源 location /files/ { alias /opt/data-asset/files/; } }try_files $uri $uri/ /index.html;这行必须加否则刷新前端路由会404。因为Vue是单页应用路由是前端自己控制的后端只返回一个index.html具体显示哪个页面由前端路由决定。后端服务用systemd配置成开机自启[Unit] DescriptionData Asset Server Afternetwork.target [Service] Userdataasset ExecStart/usr/bin/java -jar /opt/data-asset/server.jar --spring.profiles.activeprod Restartalways [Install] WantedBymulti-user.target5.3 演示环境准备与注意事项给甲方或者领导演示的时候我建议准备一份带真实感的演示数据。我通常会准备30条资产记录覆盖四五个业务系统来源每条资产配3到8个字段元数据分类和状态分散一些这样列表页和统计页看起来饱饱满满的。演示机上内存至少2G因为MySQL和Java服务本身就要占用不少内存。用free -m确认可用内存不够的话Java启动参数调小点。如果演示环境是Windows需要注意路径分隔符的问题。打包后的配置文件里如果写了Linux的路径格式在Windows上会启动失败。我一般在application.yml放公共配置各环境配置放在application-prod.yml和application-dev.yml里分开维护避免来回改。6. 常见问题与排查技巧实录6.1 前后端联调中的典型问题前后端联调最常见的问题是跨域。开发环境下前端跑在8080端口后端跑在9090端口必然跨域。我在后端加了一个CORS配置类开发环境放行所有来源生产环境通过Nginx同源部署就不存在跨域了。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }排查跨域问题的小技巧先看浏览器Network里请求是否发出如果请求状态是CORS error看看响应头里有没有Access-Control-Allow-Origin。如果没有说明后端拦截器拦截了请求并返回了错误导致CORS配置没生效。这里还要注意如果用了自定义拦截器预检请求OPTIONS必须放行否则前端根本调不到接口。6.2 权限设计中的细节坑权限设计我遇到过三个比较隐蔽的问题第一个是角色修改后需要重新登录否则权限缓存不生效。我的方案是前端在角色管理页面修改完角色权限后调用后端接口清理该角色关联用户的权限缓存并提示用户重新登录。第二个是数据权限没考虑。前期的权限只做了菜单和按钮的控制后来业务方提出不同部门只能看到自己部门的资产。这就涉及到数据行级权限了。我的实现方案是在资产查询接口里判断当前用户的角色如果是普通用户就自动加上owner_dept 当前用户部门的条件管理员不加限制。第三个是默认密码必须强制修改。系统初始化的管理员账号密码我在README里写得很清楚首次登录会弹出修改密码的引导框防止系统上线后默认密码一直没改造成安全风险。6.3 性能优化与安全加固经验数据资产登记系统数据量通常不会特别大但接口响应速度依然要注意。我做过的优化有几个点列表页接口加上索引。如果资产表数据量超过十万条asset_name的模糊搜索不建议用like %关键字%会全表扫描。要么用前缀匹配走索引要么引入ES做全文检索。对于登记系统这个量级前缀匹配加组合条件筛选基本够用。敏感数据的处理。字段级敏感级别是L3和L4的在列表页不展示具体值只显示已脱敏的提示。后端返回数据时统一做脱敏处理前端不做脱敏因为前端脱敏可以绕过接口直接拿到原始数据。操作日志记录。所有对资产数据的增删改操作通过AOP切面记录操作人和操作内容到日志表。出现数据被误改的情况时好溯源。7. 部署文档与应用扩展方向7.1 部署文档的核心内容清单项目配套的部署文档我整理得非常详细因为做这类系统交付时文档和代码同样重要。一份合格的部署文档至少应该包含以下内容环境要求JDK版本、Maven版本、Node版本、MySQL版本。数据库初始化步骤导入init.sql的操作、默认账号信息。后端打包启动步骤Maven打包命令、启动命令、停止命令。前端打包部署步骤npm install、npm run build、Nginx配置说明。配置文件说明application.yml中每个配置项的作用。常见问题启动失败、端口占用、数据库连接失败的排查方法。文档里我会放一个从零开始的操作示例包括每步输入的完整命令。因为实际交付中有些运维同事习惯直接复制命令执行不太愿意看大段的原理说明。7.2 这个系统还能怎么扩展数据资产登记系统做完后后续扩展空间其实很大。顺着数据治理这条线可以往以下方向延伸资产详情页展示字段级数据血缘让用户看清数据从哪来、往哪去。对接数据质量监控平台在资产详情页展示质量评分和校验结果。增加资产申请与授权管理让数据使用者可以在线申请数据访问权限。引入数据分类分级标准与安全合规体系打通。我在实际项目中第一批就接入了数据质量评分卡功能对每个资产按完整性、准确性、及时性等维度打分首页展示评分排行。这个功能和登记系统结合得很好因为它不改变登记流程只是在资产详情页多展示一个评分结果。最后说几句这个项目我从需求梳理到部署交付前后花了大半个月。回头总结印象最深的一点是数据资产登记系统看似是个简单的CRUD系统真正做起来难点全在细节上——资产编码规则怎么定、元数据字段怎么组织、审批流状态怎么控制、权限粒度怎么划分每一项都需要和实际业务场景反复对齐。我自己在实际操作中养成了一个习惯每个核心模块完成后先自己把操作流程完整走一遍再写对应的使用说明。因为很多问题只有自己操作的时候才会暴露出来比如某个按钮权限没配、某条数据状态不对、某个接口参数校验不过。我也建议准备接手这类系统的朋友先别急着写代码抽一周时间把数据资产管理这个领域的基本概念、常见标准、主流产品设计方案看一遍。技术实现本身并不难难的是理解业务——数据资产该怎么分类、管理员和业务用户各自关心什么、资产从登记到注销这个生命周期怎么设计。业务模型想清楚了代码只是把业务模型翻译成系统功能而已。
阅读完成 · 觉得有帮助?