ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SSM+Vue资产管理系统实战:从框架选型到部署全解析

2026/9/8 13:14:49 拓冰建站 浏览量
SSM+Vue资产管理系统实战:从框架选型到部署全解析 1. 整体设计与技术选型思路最近带了个资产管理系统项目技术栈锁定为“SSM Vue”也就是后端用 Spring SpringMVC MyBatis前端用 Vue 2 Element UI。先说结论这套组合放在今天看起来不算新潮但在企业信息化、高校实验室管理、中小企业内部资产管理这类场景里依然是非常能打的一套方案。原因也简单SSM 足够轻量上手门槛低招人容易Vue 2 生态成熟Element UI 组件库几乎能覆盖后台管理系统 80% 以上的页面需求开发效率非常高。1.1 为什么选 SSM 而不是 Spring Boot很多朋友一上来就问现在新项目谁还用 SSM直接 Spring Boot 不香吗这里我要给个客观的说法。Spring Boot 确实在配置简化、内嵌容器、自动装配上碾压 SSM 手工配置但这个项目选 SSM有三个实际原因第一客户现有系统就是基于 SSM 构建的团队里几位老工程师写了三年多的 SSM 代码仓库里沉淀了大量可直接复用的 Dao、Service 层代码硬迁到 Spring Boot 反而增加回归风险。第二SSM 的 XML 配置虽然繁琐但它的“显式”特性在某些场景下是优点。一个资产业务涉及审批流、多表关联、权限拦截配置全部摊开在 XML 里新来的同事翻一遍配置就能理清项目脉络。Spring Boot 的自动配置虽然省事但出了问题排查起来反而要翻更多源码。第三部署环境比较特殊。客户机房里的 Tomcat 版本和 JDK 版本偏老Spring Boot 2.x 要求 JDK 8虽然也能跑但 SSM 的 WAR 包直接扔进 Tomcat 7 就能跑兼容性毫无压力。我的建议是如果你是从零开始的全新项目没有历史包袱直接上 Spring Boot 没问题但如果要做系统重构、或者接手的项目本身就是 SSM 底座不要在技术栈上盲目求新先把业务跑通比什么都重要。Vue 端同理。Vue 2 已经停止官方维护这件事我们知道但这个项目用的是 Vue 2.6.14 Element UI 2.15.x组件生态稳定社区踩坑资料最全无论是 vue-router 还是 vuex任何一个报错都能搜到现成解决方案。在项目工期紧张的前提下选成熟的、团队最熟悉的技术栈才是理性的决策。1.2 资产管理系统的功能边界资产管理系统这个赛道很宽从几万块的通用型 SaaS到几百万的定制化 EAM功能差异极大。我们这个项目的核心需求是管住资产的基本信息、流转过程和维护状态。具体拆下来功能边界包括这么几个区块资产台账管理资产的入库登记、信息维护、批量导入导出是整个系统的基础数据层。领用与归还员工领用资产、归还资产系统记录完整的流转轨迹并做好状态联动。资产盘点按部门、按资产分类发起盘点任务支持扫码盘点和手工盘点两种方式。维修与报废资产报修、维修记录、报废审批最终形成资产生命周期的完整闭环。系统管理用户管理、角色权限、操作日志这是所有后台系统的标配但也往往是数据安全的关键防线。首页 Dashboard 展示资产总量、部门资产分布、资产状态占比、本月新增/报废数量用 ECharts 图表呈现。这个页面看似简单但在实际开发中其实是打通前后端数据聚合能力的第一道关口。2. 后端 SSM 框架核心实现细节2.1 数据库设计与 MyBatis 映射实践资产管理系统最核心的表是asset_info资产主表我当时的表结构设计大致如下CREATE TABLE asset_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, asset_code VARCHAR(64) NOT NULL COMMENT 资产编号, asset_name VARCHAR(128) NOT NULL COMMENT 资产名称, category_id BIGINT NOT NULL COMMENT 资产分类ID, specification VARCHAR(255) DEFAULT NULL COMMENT 规格型号, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1在库 2领用 3维修 4报废, department_id BIGINT DEFAULT NULL COMMENT 使用部门ID, user_id BIGINT DEFAULT NULL COMMENT 当前使用人ID, purchase_date DATE DEFAULT NULL COMMENT 购买日期, original_value DECIMAL(12,2) DEFAULT NULL COMMENT 原值, remark VARCHAR(500) DEFAULT NULL COMMENT 备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_asset_code (asset_code), KEY idx_category (category_id), KEY idx_status (status), KEY idx_department (department_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT资产信息表;字段设计上我补充几个关键考虑asset_code设唯一索引这是资产编号业务上要求全局不重复录入时后端要做重复性校验。很多新手容易忽略这个约束结果数据跑一段时间后出现重复编码盘点时对不上账非常痛苦。status用 TINYINT 而非 VARCHAR 存中文状态查询效率更高也方便扩展。业务状态只有四个直接在代码里用枚举类维护即可。user_id和department_id拆开存储为什么不只存user_id然后通过用户表关联部门因为在资产流转场景中资产可能处于“在库”状态这时没有使用人但有归属部门字段分离能更准确表达业务语义。MyBatis 的 mapper 文件我习惯用 XML 方式而非注解方式。资产管理系统的查询条件很多资产名称模糊查询、分类筛选、状态筛选、部门筛选、购买日期范围这些条件拼接用 XML 的动态 SQL 非常合适。核心查询语句大致是select idselectAssetPage resultTypecom.example.entity.AssetInfo SELECT a.*, c.category_name, d.dept_name, u.real_name FROM asset_info a LEFT JOIN asset_category c ON a.category_id c.id LEFT JOIN sys_department d ON a.department_id d.id LEFT JOIN sys_user u ON a.user_id u.id where if testassetName ! null and assetName ! AND a.asset_name LIKE CONCAT(%, #{assetName}, %) /if if testcategoryId ! null AND a.category_id #{categoryId} /if if teststatus ! null AND a.status #{status} /if if testdepartmentId ! null AND a.department_id #{departmentId} /if if teststartDate ! null AND a.purchase_date gt; #{startDate} /if if testendDate ! null AND a.purchase_date lt; #{endDate} /if /where ORDER BY a.create_time DESC /select一个重要的优化点是分页。这里我用了 PageHelper 插件一行代码PageHelper.startPage(pageNum, pageSize)就能完成物理分页底层自动拼接 LIMIT比手写拦截器或每张表各写一套分页 SQL 靠谱得多。实际使用时有个坑PageHelper 只对紧随其后的第一条 SQL 生效所以不要在调用startPage()和真正执行查询之间穿插其他数据库操作否则分页会失效。2.2 资产领用与状态流转机制资产领用是整个系统里最容易出 bug 的业务环节。表面上看就是一个 UPDATE 操作把资产状态从“在库”改成“领用”再更新使用人。但真正落地时还需要同时处理资产领用记录表的 INSERT而且两个操作必须在一个事务里完成。我先说一个真实踩过的坑。第一版代码领用逻辑是这么写的Override Transactional(rollbackFor Exception.class) public void assignAsset(AssetAssignDTO dto) { AssetInfo asset assetMapper.selectById(dto.getAssetId()); if (asset null) { throw new BizException(资产不存在); } // 更新资产状态 asset.setStatus(AssetStatusEnum.IN_USE.getCode()); asset.setUserId(dto.getUserId()); asset.setDepartmentId(dto.getDepartmentId()); assetMapper.updateById(asset); // 插入领用记录 AssetAssignRecord record new AssetAssignRecord(); record.setAssetId(asset.getId()); record.setUserId(dto.getUserId()); record.setAssignType(1); // 1领用 2归还 record.setRemark(dto.getRemark()); assignRecordMapper.insert(record); }这段代码初看没问题但压测时暴露了一个严重问题并发场景下同一条资产可以被两个人同时领用。原因在selectById到updateById之间存在时间窗口两个请求同时读到“在库”状态然后各自执行更新最后资产的user_id归属后提交的那个人前一个人的领用记录却已经生成数据就乱了。解决方案是加锁或加乐观锁。我当时选了乐观锁方案在asset_info表加version字段ALTER TABLE asset_info ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号;更新语句改为UPDATE asset_info SET status 2, user_id #{userId}, department_id #{departmentId}, version version 1 WHERE id #{assetId} AND version #{version}如果UPDATE返回影响行数为 0说明数据已被其他事务修改直接抛出“操作过于频繁请刷新后重试”的提示。同时在asset_code上做唯一索引已经挡住了重复资产编号的插入但状态流转这一层乐观锁才是真正能兜住并发问题的防线。细心的读者可能还会问领用记录表要不要也做唯一约束我的建议是加一个联合唯一索引uk_asset_active (asset_id, assign_type)但 MySQL 不支持“只对未归还记录唯一”这种部分索引。所以我的做法是在应用层查询发起领用前SELECT COUNT(*) FROM asset_assign_record WHERE asset_id ? AND return_time IS NULL防住业务层面的重复领用。真正压测通过后这个查询加锁也能接受毕竟资产领用的并发量远远到不了数据库瓶颈。2.3 权限拦截与操作日志的落地写法SSM 项目里的登录拦截很多人第一反应就是上 Shiro 或者 Spring Security。但说实话资产管理系统的权限模型没那么复杂用拦截器 注解就能实现得很干净而且代码量少、容易维护。我当时的做法是这样自定义一个RequiresPermission注解标注在 Controller 方法上标明需要的权限编码然后在 SpringMVC 的拦截器里做统一校验。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermission { String value(); }拦截器核心逻辑Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; RequiresPermission annotation handlerMethod.getMethodAnnotation(RequiresPermission.class); if (annotation null) { return true; // 无需权限的方法直接放行 } // 从当前登录用户上下文获取权限码集合 SetString permissions (SetString) request.getSession().getAttribute(user_permissions); if (permissions ! null permissions.contains(annotation.value())) { return true; } response.setContentType(application/json;charsetUTF-8); response.getWriter().write(JSON.toJSONString(Result.error(无操作权限))); return false; }这个方法比引入 Shiro 轻量得多而且权限码存在 Session 里也符合中小系统的实际情况。权限码的分配在角色管理功能里做超级管理员编辑角色时勾选权限树权限树用 ZTree 或 Element UI 的 Tree 组件渲染都行。操作日志我用的是 Spring AOP 切面配合自定义注解在需要记录日志的方法上打OpLog(领用资产)切面里统一记录操作人、操作方法、操作参数、IP 地址和执行耗时。这个设计的好处是业务代码不用到处写日志逻辑新增操作时只要加注解即可。3. 前端 Vue 核心模块与接口联调3.1 Vue 项目的初始化与工程目录结构前端这边我用的是 Vue CLI 4.x 创建的项目脚手架命令vue create asset-web选择 Manually select features勾选 Router、Vuex、CSS Pre-processors我选的是 Sass。目录结构基本遵循 Vue 官方推荐的风格src/ ├── api/ # 接口请求封装 │ ├── asset.js │ ├── auth.js │ └── dashboard.js ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── Pagination.vue │ ├── ImageUpload.vue │ └── DepartmentTree.vue ├── router/ # 路由配置 │ └── index.js ├── store/ # Vuex 状态管理 │ ├── modules/ │ └── index.js ├── utils/ # 工具函数 │ ├── request.js # axios 封装 │ └── auth.js ├── views/ # 页面组件 │ ├── dashboard/ │ ├── asset/ │ │ ├── AssetList.vue │ │ ├── AssetForm.vue │ │ ├── AssetDetail.vue │ └── system/ │ ├── UserManage.vue │ └── RoleManage.vue ├── App.vue └── main.jsapi/目录独立出来的好处是页面组件里不直接写请求路径统一由 API 模块管理后端接口一旦调整路径只需要改一个文件。这是很多人容易忽略的工程规范项目大了之后收益极其明显。3.2 axios 封装与跨域问题实战axios 封装是前后端联调的第一道坎。先说配置我习惯在utils/request.js里统一做这么几件事创建 axios 实例、配置baseURL、设置请求超时、请求拦截器里带 Token、响应拦截器里统一处理错误码。import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) // 请求拦截器 service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { const res response.data // 后端统一返回结构{ code, message, data } if (res.code ! 200) { Message.error(res.message || 系统错误) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message || 系统错误)) } return res }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } ) export default service这里特别要说明后端统一返回结构的设计。SSM 后端我定义了一个ResultT类包含code、message、data三个字段所有 Controller 都返回这个包装类型。这样做的好处是前端 axios 拦截器只需要处理一种结构判断code 200就走成功逻辑否则弹错误提示。不用每个页面单独try-catch代码干净非常多。跨域问题让很多人头疼但这个问题本质上有三层解法第一后端开启 CORS。SpringMVC 里加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }第二前端开发环境配代理。Vue CLI 项目在vue.config.js里设devServer.proxy把/api前缀的请求代理到后端地址开发时完全规避跨域。第三生产环境部署时前端打包后的静态文件由 Nginx 托管Nginx 反向代理后端接口从根本上解决跨域。这也是最推荐的生产方案。我实际开发中最常用的是第二种方式vue.config.js配置如下devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } }如果你的后端 Context Path 是/ssm-asset那pathRewrite要对应调整。我这里后端没有统一加前缀所以把/api去掉了。生产环境 Nginx 同样做一层路径转发一年下来没遇到跨域问题。3.3 资产列表页与 Element UI 表格实践资产列表页是整个系统使用频率最高的页面。我用了 Element UI 的el-table组件开启多选、排序、列筛选分页组件用 el-pagination和后台 PageHelper 的分页格式完美契合。列表页核心逻辑template div classasset-list !-- 搜索区 -- el-form :inlinetrue :modelqueryParams el-form-item label资产名称 el-input v-modelqueryParams.assetName placeholder请输入资产名称 clearable / /el-form-item el-form-item label资产状态 el-select v-modelqueryParams.status placeholder请选择状态 clearable el-option label在库 :value1 / el-option label领用 :value2 / el-option label维修 :value3 / el-option label报废 :value4 / /el-select /el-form-item el-form-item el-button typeprimary clickhandleQuery查询/el-button el-button clickresetQuery重置/el-button el-button typesuccess clickhandleAdd新增资产/el-button /el-form-item /el-form !-- 表格区 -- el-table :datatableData v-loadingloading border stripe el-table-column typeselection width55 / el-table-column propassetCode label资产编号 width120 / el-table-column propassetName label资产名称 min-width150 / el-table-column propcategoryName label资产分类 width120 / el-table-column propstatus label状态 width90 template slot-scope{ row } el-tag :typestatusTagType(row.status){{ statusText(row.status) }}/el-tag /template /el-table-column el-table-column propdepartmentName label使用部门 width120 / el-table-column proprealName label使用人 width100 / el-table-column label操作 width230 fixedright template slot-scope{ row } el-button typetext clickhandleDetail(row)详情/el-button el-button typetext clickhandleEdit(row)编辑/el-button el-button typetext clickhandleAssign(row)领用/el-button el-button typetext classdanger clickhandleDelete(row)删除/el-button /template /el-table-column /el-table !-- 分页 -- el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagequeryParams.pageNum :page-sizes[10, 20, 50, 100] :page-sizequeryParams.pageSize layouttotal, sizes, prev, pager, next, jumper :totaltotal /el-pagination /div /template这里有一个关键的体验细节状态列不要直接显示数字应该映射成中文或 Tag 标签。我用statusText()和statusTagType()两个方法做映射el-tag配合type属性呈现不同颜色——在库用蓝色、领用用绿色、维修用橙色、报废用灰色。这种视觉化呈现对用户理解资产状态非常有帮助。流程上是点击查询按钮把queryParams作为参数传给getAssetList接口拿到数据后赋值给tableData和total表格自动渲染。分页组件通过current-change事件触发列表重新加载页码变化时后端利用 PageHelper 重新查询。3.4 资产盘点模块与扫码逻辑盘点模块是这个系统里比较出彩的功能。业务逻辑是管理员发起盘点任务选择盘点的部门或分类生成盘点单盘点人用手机扫资产上的二维码或条形码系统自动匹配资产编号核对资产是否存在、位置是否准确。二维码怎么生成这里我用了qrcodejs2这个前端库在资产详情页展示一个二维码内容就是资产编号。盘点时用手机摄像头扫码系统读取到资产编号前端页面立刻跳转到资产确认页。扫码盘点页面的核心逻辑// 监听扫码枪输入扫码枪本质上是模拟键盘输入以回车结尾 mounted() { window.addEventListener(keydown, this.handleScanInput) }, beforeDestroy() { window.removeEventListener(keydown, this.handleScanInput) }, methods: { handleScanInput(e) { if (e.key Enter) { if (this.scanBuffer.length 0) { this.verifyAsset(this.scanBuffer) this.scanBuffer } } else { // 过滤掉功能键 if (e.key.length 1) { this.scanBuffer e.key } } }, async verifyAsset(code) { const res await verifyAssetByCode({ assetCode: code }) if (res.code 200) { this.$alert(资产【${res.data.assetName}】盘点成功, 提示, { type: success }) } else { this.$alert(res.message, 提示, { type: error }) } } }这个实现的妙处在于不需要额外对接摄像头或扫码 SDK普通的扫码枪插上 USB 就能用系统当键盘输入处理就行。如果你要用手机摄像头扫码则需要引入vue-qrcode-reader组件调用摄像头识别二维码识别结果同样走verifyAsset方法。两种方式我都试过扫码枪在仓库场景下更稳定响应更快。4. 打包部署与常见问题排查4.1 后端打包与前端构建SSM 项目的打包没有什么玄机传统的 WAR 包方式在pom.xml里配置packagingwar/packagingMaven 执行clean package即可。要注意的是打包时需要跳过测试避免 test 目录下的代码影响构建结果mvn clean package -DskipTests打出的 WAR 包扔到 Tomcat 的webapps目录下启动 Tomcat 自动解压部署。如果你有 Jenkins 或 GitLab CI可以把这个过程自动化这里不展开。Vue 前端的构建是另一个容易踩坑的点。生产构建前需要检查.env.production文件中的环境变量NODE_ENVproduction VUE_APP_BASE_API/prod-apiVUE_APP_BASE_API这个变量直接决定 axios 请求的 baseURL。在开发环境配的是/api本地代理到后端生产环境我配的是/prod-api由 Nginx 把/prod-api前缀的请求反向代理到后端服务。如果没有配好这个变量最常见的结果就是打包后所有接口请求 404——前端静态文件能打开但数据全部加载不出来。构建命令npm run build构建产物在dist/目录。Nginx 配置我给出一个可用版本server { listen 80; server_name asset.example.com; root /opt/asset-web/dist; index index.html; # 前端路由 history 模式的回退 location / { try_files $uri $uri/ /index.html; } # 接口反向代理 location /prod-api/ { proxy_pass http://127.0.0.1:8081/; 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 ~* \.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$ { expires 7d; add_header Cache-Control public, no-transform; } }这里proxy_pass http://127.0.0.1:8081/;末尾的/极其重要它表示去掉/prod-api/前缀后直接转发。如果忘了末尾的斜杠后端接收到的 URL 会多出/prod-apiController 路由匹配直接 404。这个细节坑过无数人务必留意。4.2 高频问题快速排查速查表我把这个项目开发过程中遇到的高频问题整理了成一张表按“现象 → 原因 → 解决”的格式方便后续项目直接对号入座。问题现象常见原因解决方案前端页面能打开接口全部 404生产环境 baseURL 与 Nginx 转发路径不一致检查.env.production的VUE_APP_BASE_API和 Nginxlocation前缀是否匹配本地开发跨域报错未配置 devServer.proxy或后端未开 CORS优先配代理必要时后端加 CORS 配置分页数据不正确重复或缺失PageHelper 使用不当startPage()后执行了其他 SQL确保PageHelper.startPage()后面紧跟目标查询资产领用并发时数据错乱缺少乐观锁或行锁加version字段UPDATE 时校验版本号Vue 打包后路由刷新 404前端路由使用 history 模式Nginx 未做回退在 Nginxlocation /中添加try_files $uri $uri/ /index.html;登录后跳转路由再次刷新却回登录页Token 存储位置不一致或路由守卫逻辑问题统一用localStorage存 tokenrouter.beforeEach 中同步校验上传图片后刷新不显示图片保存了相对路径但未配置静态资源映射后端加资源映射Nginx 或 Tomcat 配置虚拟目录操作日志时间少了 8 小时JVM 默认时区与本地不一致设置 JVM 参数-Duser.timezoneGMT08或代码指定时区这表里我想再展开说两个。第一个是路由刷新 404 问题。Vue 2 的 history 模式在开发环境一切正常但部署到 Nginx 后一刷新页面就 404本质是 Nginx 收到/asset/list这个路径请求后去文件系统找asset/list.html找不到就返回 404。加一行try_files $uri $uri/ /index.html;的意思是如果找不到对应路径就返回index.html由前端路由接管。说白了就是让 Nginx 把未知路径全部交给前端框架去处理。这个配置不加部署后百分之百出问题。第二个是时间少 8 小时。MyBatis 从数据库查出来的是java.util.Date前端展示时会使用浏览器所在时区格式化。如果后端服务器时区设置不正确MySQL 连接的serverTimezone参数没配就会出现时间偏移。我当时的处理是在 JDBC URL 里加serverTimezoneAsia/Shanghai同时给 JVM 启动参数加上-Duser.timezoneAsia/Shanghai双保险。时间问题虽然定位不难但不出问题你永远不会意识到它有多烦人。4.3 Vue 和 SSM 联调时的一些心得前后端联调阶段最容易消耗时间的问题其实是接口字段不一致。后端定义的返回字段叫assetName前端写成了name结果页面永远显示空白但浏览器 Network 面板里明明有数据。这种问题看后台日志很难发现得打开 DevTools 逐步排查。所以项目开始前最好用 Swagger 或 Postman 把接口文档先定好字段名和类型全部对齐再开工。我实际操作中的另一个技巧是给 axios 加debug模式。在开发环境里请求拦截器顺便打一条 console 日志输出完整请求 URL 和参数响应拦截器输出返回数据。联调阶段打开控制台一目了然能省下大量沟通时间。上线前把 debug 关闭即可。还有就是 Vue 项目的依赖版本问题。Element UI 的 2.15.x 系列和 Vue 2.6.x 是兼容的但如果你在package.json里用了^2.15.0这种范围版本npm 安装时会拉最新小版本某些小版本可能有细微行为差异。我的习惯是用package-lock.json锁定版本团队协作时统一npm ci安装避免大家装的依赖不一致导致行为不同。5. 一些没有写在需求文档里的经验项目上线后回头看有几点是当时没预料到但最终影响很大的。第一资产编号规范最好在项目启动时就定好。我们一开始用的编号是ZC001这种纯流水号后来客户提出按“资产分类购置年月流水号”重新生成结果历史数据全部要刷一遍。如果前期就设计好编号规则并且建唯一索引后面能省事太多。第二列表页的大数据量问题一定要提前考虑。资产数量到十万级后不做分页优化的话即使 PageHelper 物理分页没问题COUNT(*)查询也会变慢。我给资产主表建了几个常用查询的联合索引比如(status, category_id)和(department_id, status)把列表页的响应时间从 800ms 降到了 120ms 左右。第三不要把业务逻辑全写在 Controller 里。我在这个项目里强制要求 Controller 只做参数接收和结果包装具体逻辑全部下沉到 Service 层。这个规范带来的好处是后期加单元测试时非常方便。现在很多人图省事直接在 Controller 里写 SQL 操作等到业务复杂度上来代码就变成一坨不可维护的面条。第四前端路由懒加载一定要做。Vue 项目首屏加载时间直接影响用户体感。路由配置里用component: () import(/views/asset/AssetList.vue)的写法Webpack 会自动代码分割按需加载页面资源。这个项目的首屏从优化前的 4.8 秒降到 1.9 秒效果立竿见影。第五日志策略要在一开始就规划好。SSM 项目我用的 logback日志级别生产环境设为 INFO资产领用、报废、审批这类关键写操作要单独输出业务日志。排查问题时没有日志就只能瞎猜有条件尽量把操作前后的数据快照打出来。整个项目做了大概五个月从需求梳理、数据库设计到前后端联调上线走了一遍完整的 SSM Vue 开发流程。虽然这套技术栈被很多人称为“老古董”但我始终觉得技术选型要服务于业务场景和团队实际能力SSM Vue 的组合在中小型管理系统里开发效率、稳定性、人才供给都是有保障的。最后分享一个实用的调试技巧开发阶段遇到后端接口报错先用 Postman 直接调接口看后端返回的原始 JSON确认后端没问题后再排查前端代码。很多人一上来就盯前端 Vue 代码折腾半天发现是后端空指针白白浪费时间。前后端分离的项目定位问题首先分清责任边界这是最高效的排查思路。