news 2026/9/8 13:14:43

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资产信息表';

字段设计上我补充几个关键考虑:

asset_code设唯一索引,这是资产编号,业务上要求全局不重复,录入时后端要做重复性校验。很多新手容易忽略这个约束,结果数据跑一段时间后出现重复编码,盘点时对不上账,非常痛苦。

status用 TINYINT 而非 VARCHAR 存中文状态,查询效率更高,也方便扩展。业务状态只有四个,直接在代码里用枚举类维护即可。

user_iddepartment_id拆开存储,为什么不只存user_id然后通过用户表关联部门?因为在资产流转场景中,资产可能处于“在库”状态,这时没有使用人但有归属部门,字段分离能更准确表达业务语义。

MyBatis 的 mapper 文件我习惯用 XML 方式而非注解方式。资产管理系统的查询条件很多,资产名称模糊查询、分类筛选、状态筛选、部门筛选、购买日期范围,这些条件拼接用 XML 的动态 SQL 非常合适。核心查询语句大致是:

<select id="selectAssetPage" resultType="com.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 test="assetName != null and assetName != ''"> AND a.asset_name LIKE CONCAT('%', #{assetName}, '%') </if> <if test="categoryId != null"> AND a.category_id = #{categoryId} </if> <if test="status != null"> AND a.status = #{status} </if> <if test="departmentId != null"> AND a.department_id = #{departmentId} </if> <if test="startDate != null"> AND a.purchase_date &gt;= #{startDate} </if> <if test="endDate != 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); }

这段代码初看没问题,但压测时暴露了一个严重问题:并发场景下,同一条资产可以被两个人同时领用。原因在selectByIdupdateById之间存在时间窗口,两个请求同时读到“在库”状态,然后各自执行更新,最后资产的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; // 无需权限的方法直接放行 } // 从当前登录用户上下文获取权限码集合 Set<String> permissions = (Set<String>) request.getSession().getAttribute("user_permissions"); if (permissions != null && permissions.contains(annotation.value())) { return true; } response.setContentType("application/json;charset=UTF-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.js

api/目录独立出来的好处是页面组件里不直接写请求路径,统一由 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 后端我定义了一个Result<T>类,包含codemessagedata三个字段,所有 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 class="asset-list"> <!-- 搜索区 --> <el-form :inline="true" :model="queryParams"> <el-form-item label="资产名称"> <el-input v-model="queryParams.assetName" placeholder="请输入资产名称" clearable /> </el-form-item> <el-form-item label="资产状态"> <el-select v-model="queryParams.status" placeholder="请选择状态" clearable> <el-option label="在库" :value="1" /> <el-option label="领用" :value="2" /> <el-option label="维修" :value="3" /> <el-option label="报废" :value="4" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">查询</el-button> <el-button @click="resetQuery">重置</el-button> <el-button type="success" @click="handleAdd">新增资产</el-button> </el-form-item> </el-form> <!-- 表格区 --> <el-table :data="tableData" v-loading="loading" border stripe> <el-table-column type="selection" width="55" /> <el-table-column prop="assetCode" label="资产编号" width="120" /> <el-table-column prop="assetName" label="资产名称" min-width="150" /> <el-table-column prop="categoryName" label="资产分类" width="120" /> <el-table-column prop="status" label="状态" width="90"> <template slot-scope="{ row }"> <el-tag :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag> </template> </el-table-column> <el-table-column prop="departmentName" label="使用部门" width="120" /> <el-table-column prop="realName" label="使用人" width="100" /> <el-table-column label="操作" width="230" fixed="right"> <template slot-scope="{ row }"> <el-button type="text" @click="handleDetail(row)">详情</el-button> <el-button type="text" @click="handleEdit(row)">编辑</el-button> <el-button type="text" @click="handleAssign(row)">领用</el-button> <el-button type="text" class="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total"> </el-pagination> </div> </template>

这里有一个关键的体验细节:状态列不要直接显示数字,应该映射成中文或 Tag 标签。我用statusText()statusTagType()两个方法做映射,el-tag配合type属性呈现不同颜色——在库用蓝色、领用用绿色、维修用橙色、报废用灰色。这种视觉化呈现对用户理解资产状态非常有帮助。

流程上是:点击查询按钮,把queryParams作为参数传给getAssetList接口,拿到数据后赋值给tableDatatotal,表格自动渲染。分页组件通过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里配置<packaging>war</packaging>,Maven 执行clean package即可。要注意的是打包时需要跳过测试,避免 test 目录下的代码影响构建结果:

mvn clean package -DskipTests

打出的 WAR 包扔到 Tomcat 的webapps目录下,启动 Tomcat 自动解压部署。如果你有 Jenkins 或 GitLab CI,可以把这个过程自动化,这里不展开。

Vue 前端的构建是另一个容易踩坑的点。生产构建前需要检查.env.production文件中的环境变量:

NODE_ENV=production VUE_APP_BASE_API=/prod-api

VUE_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-api,Controller 路由匹配直接 404。这个细节坑过无数人,务必留意。

4.2 高频问题快速排查速查表

我把这个项目开发过程中遇到的高频问题整理了成一张表,按“现象 → 原因 → 解决”的格式,方便后续项目直接对号入座。

问题现象常见原因解决方案
前端页面能打开,接口全部 404生产环境 baseURL 与 Nginx 转发路径不一致检查.env.productionVUE_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存 token,router.beforeEach 中同步校验
上传图片后刷新不显示图片保存了相对路径但未配置静态资源映射后端加资源映射,Nginx 或 Tomcat 配置虚拟目录
操作日志时间少了 8 小时JVM 默认时区与本地不一致设置 JVM 参数-Duser.timezone=GMT+08或代码指定时区

这表里我想再展开说两个。

第一个是路由刷新 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 里加serverTimezone=Asia/Shanghai,同时给 JVM 启动参数加上-Duser.timezone=Asia/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 代码,折腾半天发现是后端空指针,白白浪费时间。前后端分离的项目,定位问题首先分清责任边界,这是最高效的排查思路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:14:26

数据资产联播16期:从概念普及到分行业落地的关键信号

1. 为什么我会坚持做“数据资产联播”这个系列 先交代一下背景。从2025年下半年开始&#xff0c;我在自己的信息流里做了一个固定动作&#xff1a;每天花四十分钟&#xff0c;把当天关于“数据资产”的新闻、报纸评论、政策解读、交易公告、行业研报全部扫一遍&#xff0c;每周…

作者头像 李华
网站建设 2026/9/8 13:12:31

零基础Linux云计算运维学习路线:从命令到云平台完整指南

这类零基础学习路线最怕的不是知识点太多&#xff0c;而是根本不知道先学什么、后学什么。Linux云计算运维这个方向看着很大&#xff0c;实际上拆开就三块&#xff1a;Linux基础、常用服务与自动化、云计算平台与容器。下面这套路线&#xff0c;是我自己带新人时一直用的顺序&a…

作者头像 李华
网站建设 2026/9/8 13:11:10

TensorFlow强化学习控制6轴机械臂:从环境搭建到PPO训练实战

简介&#xff1a;这是一套基于TensorFlow.js的六轴机械臂强化学习测试项目&#xff0c;面向对机器人控制、三维空间寻路与强化学习感兴趣的Web开发者。项目设计了简化的六轴机械臂模型&#xff0c;并以二维正方形地图和三维点阵地图两种场景演示如何通过距离奖励机制训练模型寻…

作者头像 李华
网站建设 2026/9/8 13:09:47

Bun的Rust重写:JavaScript工具链性能优化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 13:08:16

单片机毕设选题推荐:基于 STM32 的多组定时语音播报药盒控制系统设计 基于 STM32 传感器的服药状态监测与移动端交互系统(012907)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/8 13:07:19

像素酒馆V1.4:基于LLM状态机与提示词工程的TRPG跑团系统设计

最近把一个自己维护的在线 AI 酒馆项目升级到了 V1.4&#xff0c;这次更新的主题是“互动跑团”。很多朋友看到版本号第一反应是“又改了 UI 吧”&#xff0c;但实际上&#xff0c;从常规的 AI 角色扮演聊天切换到一张完整的 TRPG 跑团桌&#xff0c;改动面比想象中大得多。角色…

作者头像 李华