news 2026/10/3 10:17:28

基于SSM的饰品商城“小饰界”:从需求分析到部署实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SSM的饰品商城“小饰界”:从需求分析到部署实践

做“小饰界”这个基于SSM的线上饰品商城,前后折腾了差不多一个月。刚开始我拿到这个选题时,心里想的是“无非就是增删改查”,但真正把用户、商品、购物车、订单、库存这些模块串起来之后,我发现商城类项目确实是最适合练Java后端功底的题材。如果你正准备做类似的毕业设计、写论文,或者单纯想拿一套SSM项目练手,这篇文章应该能帮你省掉不少弯路。

这个项目最终做下来的体量大概是:用户能浏览商品、搜索、加入购物车、模拟下单、查看订单;管理员能维护商品分类、商品信息、处理订单状态。技术栈是标准的 Java + SSM:Spring 管理业务对象和事务,SpringMVC 处理请求分发,MyBatis 负责数据持久化,前端用 JSP 页面,配合 Bootstrap 做样式,数据库用的是 MySQL。麻雀虽小,但电商的业务链路基本是完整的。

下面我把整个项目从需求到部署的完整思路拆开讲,重点会放在那些“做完一遍才知道”的细节上。

1. 做“小饰界”之前,我先把这个商城的业务逻辑捋清楚了

1.1 一个饰品商城的最小业务闭环

很多同学做商城项目,第一反应是打开脑洞加功能:优惠券、满减、收藏、积分、直播推荐……我一开始也差点把这事儿搞复杂,后来果断砍掉了大半,只保留一个最小闭环:用户看到商品,能一步步走到订单生成,管理员能看到这笔订单并处理状态。这个闭环就是电商系统的心脏。

为什么强调“闭环”?因为论文和面试里别人最关心的不是你有多少个页面,而是你的业务数据是怎么流转的。饰品商城的用户侧流程一般是:

游客浏览首页和商品列表 → 查看商品详情 → 注册/登录 → 加入购物车 → 修改数量、算总价 → 提交订单(模拟支付) → 在“我的订单”里查看订单状态 → 管理员在后台上架/下架商品、处理订单发货。

这串流程走通了,项目的主干就立住了。至于收藏、评论、秒杀这些,全都可以作为后续扩展去写,不会影响核心系统答辩。

1.2 用户端和管理端的功能边界

我把系统拆成两个角色:普通用户和管理员。权限边界从一开始就要清楚,否则后面做 SpringMVC 拦截器的时候会很痛苦。

用户端的功能如下:

  • 注册、登录、修改个人信息
  • 浏览商品,按分类筛选,按关键词搜索
  • 查看商品详情,把商品加入购物车,修改购物车数量,删除购物车项
  • 提交订单,模拟支付,查看自己的订单列表和订单详情
  • 确认收货

管理员端的核心功能:

  • 登录后台管理系统(admin 账号)
  • 商品分类的增删改查
  • 商品上架、编辑、下架、删除
  • 查看用户列表,禁用可疑账号
  • 查看订单列表,给订单设置“已发货”“已完成”等状态

后台功能不需要做得像中后台系统那么重,但每个按钮背后都应该对应一条明确的 SQL 或 Service 层逻辑,不要出现在前端写死状态的情况。比如下架商品,必须真的把 product 表的 status 字段改成 0,而不是在前端把按钮置灰。

1.3 论文视角下的需求描述怎么写不跑偏

如果你是在写毕业论文,第四章“需求分析”往往会要求画用例图、写需求说明书。我建议你在定需求时就用一句话描述每个用例:角色 + 动作 + 结果。比如“用户登录”的完整表述是“已注册用户输入用户名和密码,系统验证通过后进入商城主页”。一句话说清楚谁发起了什么动作、系统返回了什么结果,后面写代码、写测试用例都能对得上。

我还把一个原则写进了文档:所有业务操作必须经过 Service 层,Controller 只负责参数接收和视图跳转。很多初学者容易把业务逻辑直接写在 Controller 里,页面跑起来是没问题的,但 Service 层空空荡荡,论文里“业务逻辑层设计”一小节就非常难写。所以代码结构和论文结构最好从一开始就对齐,别等写文档了再返工。

2. SSM技术栈为什么适合这个项目:选型逻辑与版本搭配

2.1 Spring、SpringMVC、MyBatis三件套怎么分工

SSM 是 Spring + SpringMVC + MyBatis 三个框架的组合,三者的职责在项目里分得非常清楚:

  • Spring:负责对象的创建和装配,也就是 IoC 容器。我们平时写的 Service、Mapper 这些对象,都不再自己 new,而是交给 Spring 容器管理。同时 Spring 的 AOP 机制承担了事务管理、日志记录等横切逻辑。
  • SpringMVC:负责 Web 层的请求分发。前端来的 URL 先交给 DispatcherServlet,再由处理器映射找到对应的 Controller 方法,最后通过视图解析器渲染 JSP。
  • MyBatis:负责 JDBC 的封装和 SQL 映射。我们可以把 SQL 写在 Mapper.xml 里,MyBatis 负责把数据库里的记录自动映射成 Java 对象,也负责把 Java 参数传入 SQL。

打个比方:Spring 是后勤部,所有组件的“生死”都归它管;SpringMVC 是前台接待,用户的每个请求都要在这里挂号、分流;MyBatis 是数据专员,专门跑数据库那块儿的活儿。三条线互相配合,但边界很清晰。

2.2 为什么没选Spring Boot

这是很多人在选题时都会纠结的问题。说实话,如果是从“快速上线跑通”的角度,Spring Boot 绝对更省事,因为自动配置帮你省掉了一大堆 XML 配置。但为什么“小饰界”这个题目依然选了传统 SSM?

第一,SSM 的年代感适合写成论文。Spring 和 SpringMVC 的配置过程能讲的东西更多:依赖注入怎么配置、事务管理器怎么注册、拦截器怎么映射、MyBatis 的 SqlSessionFactory 怎么构建。这些东西在 Spring Boot 里很多都被自动配置屏蔽了,论文的“系统配置”和“框架整合”章节会写得比较痛苦。

第二,高校的很多毕业设计题目还是固定要求用 SSM。而且 SSM 的底层原理和 Spring Boot 是相通的,把 SSM 的原理搞清楚,后面迁移到 Boot 其实很快,只是从写 XML 改成写注解而已。

第三,SSM 项目对理解 Servlet 容器和数据源管理有直接帮助。我在配置 Druid 数据源和 MyBatis 的时候,能明显感受到 JVM 进程里到底发生了哪些连接操作,这种感知在 Spring Boot 里很容易被吞掉。

当然,如果老师没有硬性要求、且时间很紧,我也会建议用 Spring Boot + MyBatis-Plus。但对“小饰界”这个场景,传统 SSM 的工程量是可控的,而且更贴合论文选题。

2.3 开发环境的版本搭配与配置

版本搭配是必踩的第一个坑。我最终用的是这一套:

组件版本说明
JDK1.8兼容性最好,Tomcat 和 Spring 5.x 都能完美配合
Maven3.6.3依赖管理,仓库用阿里云镜像
Tomcat8.5本地开发和部署都用它,不要用 Tomcat 10(包名变了,SSM 适配麻烦)
MySQL5.75.7 和 8.0 都行,但 5.7 对大部分毕设项目足够
Spring / SpringMVC5.2.x基于注解开发最稳定的一代
MyBatis3.5.x配合 mybatis-spring 2.0.x
Druid1.2.x数据库连接池,官方文档丰富
PageHelper5.3.x分页插件,用起来方便

pom.xml 里有一个容易忽略的点:Spring 相关依赖的版本必须统一,否则会出现 NoSuchMethodError 这种奇怪异常。比如 spring-webmvc 用了 5.2.15,spring-jdbc 却是 5.1.20,运行时大概率报错。最好用一个<properties>标签统一定义 Spring 版本,再全部引用同一个变量。

另外,JDK 1.8 和 Tomcat 8.5 的组合是最省心的。Tomcat 10 把 javax.servlet 换成了 jakarta.servlet,Spring 5.x 默认还是旧的包名,如果直接用 Tomcat 10,启动时会因为类找不到直接给你抛 NoClassDefFoundError。这个坑我表述得直接一点:别挑战高版本,毕设项目求稳。

3. 数据库设计:一个商城的表结构是怎么从零到一的

3.1 核心表清单与关系

数据库设计是商城项目的灵魂。我的“小饰界”最终设计了 8 张核心表,整体关系可以用一句话描述:用户买商品产生订单,订单拆成订单明细,商品挂在分类下面。

表名作用关键字段
user用户表id, username, password, nickname, phone, role
category商品分类表id, name, parent_id, sort
product商品表id, category_id, name, price, stock, sales_count, status
cart购物车表id, user_id, product_id, quantity, checked
orders订单表id, order_no, user_id, total_price, status, receiver_address
order_item订单明细表id, order_id, product_id, product_name, price, quantity
address收货地址表id, user_id, receiver, phone, detail
admin管理员表id, username, password, role

有人会问为什么要 address 单独一张表?因为“小饰界”支持用户管理多个收货地址,下单时选一个。如果把地址写死在 user 表里,用户想换个地址就得覆盖原字段,没法保留历史订单的收货信息。而订单表里的 receiver_address 这个字段,是用来做“地址快照”的——用户下单那一刻的地址会原样存进订单,后面用户再改地址,历史订单不受影响。

3.2 用户表与商品表的字段设计

用户表的设计,重点在校验和安全:

CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL COMMENT '登录名', `password` varchar(64) NOT NULL COMMENT '密码(加盐后MD5)', `nickname` varchar(32) DEFAULT NULL, `phone` varchar(11) DEFAULT NULL, `role` tinyint DEFAULT '0' COMMENT '0普通用户,1管理员', `status` tinyint DEFAULT '1' COMMENT '1启用,0禁用', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

密码字段这里我多说一句:明文存密码是绝对不可以的。哪怕只是毕业设计,也要用 MD5 加盐(或至少是 MD5)存进去。加盐的意思就是在用户密码后面拼接一段随机字符串,再一起做哈希。比如用户密码是123456,随机盐是s1a2t3,实际加密的对象是123456s1a2t3。这样即使两个用户密码相同,如果盐不同,密文也不同。

商品表要注意的则是价格字段不能用 float 或 double。Java 的 double 在计算金额时会有精度丢失,0.1 + 0.2 这种问题在订单总价计算时会非常恶心。商品和订单相关的金额全部用 Decimal,数据库用decimal(10,2),Java 端用java.math.BigDecimal。在代码里也别直接对 BigDecimal 做加法,要用add方法。

商品的状态字段我用了status来标识上架和下架,而不是物理删除。原因很简单:商品下架后,历史订单的明细里还需要保留商品名称和价格快照,如果直接删掉商品记录,订单明细就变成了孤儿数据。设计数据库时想明白“哪些数据必须留下痕迹”,基本上就能避开级联删除的坑。

3.3 购物车和订单表的关联设计

购物车为什么单独建表而不是存 Session?我一开始也想过用 Session 存购物车,开发速度快、不用建表。但后来很快否了:Session 是依赖于客户端浏览器的,用户换一台设备购物车就没了;而且服务端集群部署时,Session 同步问题很麻烦。单独的表结构虽然多写几个 SQL,但符合真实系统的做法,论文里也好展开。

购物车表的核心是user_id和product_id联合唯一。也就是说,同一个用户对同一个商品,购物车里只可能有一条记录。如果用户再次点击“加入购物车”,正确的做法是让这条记录的quantity + 1,而不是再插入一行。这个业务逻辑写在 Service 层,而不是数据库约束。

订单表orders和order_item是一对多的关系,为什么要拆两表?因为订单要记录“用户一次购买了哪些东西”,而订单明细要把每件商品的信息单独存档。订单主表只存总金额、订单状态、收货人等整体信息;明细表存每件商品的名称、单价、数量、小计。这样拆分后,“一笔订单金额是多少”和“这笔订单由哪些商品组成”是两次独立的查询,逻辑清楚得多。

订单主表的order_no字段是业务单据号,我建议不要直接用自增 id 当订单号对外展示。自增 id 会暴露销量、容易被遍历,而且没有业务含义。我记得自己用的是时间戳 + 随机数拼接成 20 位以内的字符串,比如202406011030123456。虽然格式简单,但足以应付毕业设计场景。

3.4 数据库层面的防超卖设计

防超卖是商城系统最有含金量的一个点。如果只靠查询库存数量再判断,在高并发场景下一定会出问题。假设库存只有 1 件,A 和 B 两个用户同时查库存,都看到剩余 1 件,然后都去扣减库存,最后库存变成 -1,或者多卖出去一件。

正确做法之一,是直接在 SQL 层做条件更新:

UPDATE product SET stock = stock - 1 WHERE id = #{productId} AND stock >= 1;

这条 SQL 的意思是:只有库存大于等于购买数量时,才执行扣减,并且扣减是原子操作。如果影响行数为 0,说明库存不足,直接抛出“库存不足”异常。这样不需要加锁,就能从数据库层面天然地避免超卖。配合事务,订单生成和库存扣减才能保持一致。

在数据库设计阶段就把这个查询条件放进去,比后面代码写半天乐观锁、悲观锁要省事得多。

4. 核心功能编码实现:从Controller到Mapper的完整链路

4.1 用户登录与拦截器的权限控制

用户登录的流程是基本功:提交用户名密码 → Service 层校验 → 成功后把用户对象放入 Session → 跳转首页。我用的密码校验方式是比较加密后的密文,而不是查数据库明文。

登录成功后,要把用户信息放在 Session 里,之后所有需要登录的接口都通过拦截器统一判断,而不是在每个 Controller 里写 if 判断。SpringMVC 拦截器实现起来很简单,继承HandlerInterceptor:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/user/login"); return false; } return true; } }

然后在 springmvc.xml 里配置拦截路径。这里有个细节:放行静态资源和登录、注册、商品浏览等公开页面,拦截/cart/**、/order/**这些需要登录的路径。放行的写法不要写错,否则页面样式会全部丢失:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/cart/**"/> <mvc:mapping path="/order/**"/> <mvc:exclude-mapping path="/static/**"/> </mvc:interceptor> </mvc:interceptors>

管理员后台的权限控制同样用拦截器,但是要判断 Session 里的角色字段。普通用户和管理员的用户表本来可以共用,我在 user 表里加了一个 role 字段,登录时判断 role 是否为 1。如果是后台管理系统,再单独写一个 AdminInterceptor,拦截/admin/**,检查角色权限,防止普通用户直接访问后台 URL。

4.2 商品列表与分页查询

商品列表是商城流量最大的页面,所以分页查询是必须的。我用的是 PageHelper 插件,在 Service 层调用之前设置页码和每页条数,MyBatis 会帮我们在 SQL 后面自动拼接 LIMIT:

@Override public PageInfo<Product> getProductList(int categoryId, String keyword, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<Product> list = productMapper.selectProductList(categoryId, keyword); return new PageInfo<>(list); }

对应 Mapper.xml 里的 SQL,我用了动态 SQL 处理筛选条件:

<select id="selectProductList" resultType="com.xiaoshi.entity.Product"> SELECT * FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> AND status = 1 </where> ORDER BY create_time DESC </select>

动态 SQL 里的<where>标签会自动处理掉多余的 AND,省去写WHERE 1=1这种不太体面的写法。LIKE 查询用 CONCAT 拼接,而不是直接在 Java 里拼好再传进来,这样能减少 SQL 注入风险。商品列表返回的结果用 PageInfo 包装,前端可以直接取 total 来渲染分页条。

4.3 购物车核心逻辑:加购、修改数量、计算总价

购物车这块,我最开始容易犯的错误是“每次操作都走一遍完整链路”——加购时判断商品是否存在、然后 insert。后来发现直接先查一次,再根据结果决定 update 或 insert,代码更清晰。

核心 Service 方法大概长这样:

@Transactional public void addToCart(Integer userId, Integer productId, Integer quantity) { Cart cart = cartMapper.selectByUserIdAndProductId(userId, productId); if (cart == null) { Cart newCart = new Cart(); newCart.setUserId(userId); newCart.setProductId(productId); newCart.setQuantity(quantity); cartMapper.insert(newCart); } else { cart.setQuantity(cart.getQuantity() + quantity); cartMapper.updateById(cart); } }

注意加购前还要判断商品是否上架、库存是否足够。这两个判断看起来简单,但很容易被忽略。我记得有一次测试人员(其实就是我自己)在没有判断库存的情况下加了 99 件商品,结果下订单时才发现库存不够,整个下单流程卡住。后来统一在加购、修改数量、下单三个环节都检查了库存,问题才彻底解决。

购物车总价的计算逻辑我是放在 Service 层,遍历购物车项,关联查询商品价格,然后累加每个商品的小计。这里强调一点:价格要以数据库商品表的实时价格为准,不要用购物车里冗余存储的价格字段。原因是你存的价格可能已经过期,用户下单时看到的却是老价格,会造成对不上的尴尬。

4.4 下单与库存扣减的事务控制

下单是商城系统里事务最重的一个方法。传统写法很容易犯的错误是“把每个步骤拆成多个 Service 方法,然后在 Controller 里挨个调用”。这样一来,一旦中间步骤失败,前面的数据已经提交,数据就乱了。正确做法是:把下单的整个流程封装在一个事务方法里,要么全部成功,要么全部回滚。

我的createOrder方法大概步骤如下:

  1. 校验购物车状态,获取用户选中的购物车商品列表
  2. 根据商品 id 批量查询商品信息,计算订单总金额
  3. 生成订单号,插入 orders 主表,拿到订单 id
  4. 遍历购物车商品,逐个写入 order_item 明细表
  5. 执行条件更新库存 SQL(stock >= quantity)
  6. 如果库存扣减失败,抛出异常触发回滚
  7. 删除购物车中已下单的商品记录

这个流程我放在一个方法里,并且开启了事务:

@Transactional(rollbackFor = Exception.class) public Order createOrder(OrderVO orderVO) { // 1. 查询选中的购物车项 // 2. 计算总价 // 3. 插入订单主表 // 4. 插入订单明细 // 5. 扣减库存 // 6. 清空购物车 return order; }

这里特别注意:rollbackFor = Exception.class一定不要省。Spring 默认只对运行时异常(RuntimeException)回滚,如果抛的是受检异常(Exception),事务不会回滚。在很多业务场景下,库存不足、参数错误这些我们往往抛的是自定义运行时异常,问题不大;但如果你在代码里 catch 了异常又包了一层throw new Exception(...),没有 rollbackFor 的话,事务就会悄悄提交,那真的是灾难现场。

另外还要留意自调用失效的问题:同一个类里,A 方法调用同类 B 方法,B 方法上的 @Transactional 是不生效的。因为在 Spring 的代理机制里,内部自调用没有经过代理对象。所以我的下单事务方法是写在 Controller 调用的 Service 接口上,而不是在 Service 里自己调自己。这个细节我在后面踩坑部分再详细说。

5. 开发中真正让我头疼的五个问题

5.1 MyBatis resultMap字段映射丢失

第一个让我卡了两天的问题是:数据库里的user_id字段,Javabean 里对应的是userId,但查询出来userId一直是 null。原因是默认情况下 MyBatis 不会自动把下划线转驼峰。

解决方式有两种:一是在每个 resultMap 里手动映射 column 和 property;二是在 mybatis-config.xml 里加全局配置:

<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>

加了这一行之后,user_id会自动映射到userId,create_time自动映射到createTime,省去大量 resultMap 的重复劳动。我建议项目一上来就配好这个全局设置,否则后期写几百行 resultMap,维护成本特别高。

还有一种情况是联表查询时字段名冲突。比如订单表和用户表都有create_time,如果直接SELECT *,MyBatis 会取到别名位置靠后的值,结果可能把订单创建时间映射成了用户创建时间。我的习惯是联表查询时显式写别名:订单的create_time写成o.create_time AS order_create_time,再配一个 resultMap 做映射。这样字段来源一目了然,问题排查也快。

5.2 Spring事务配置不生效的坑

事务不生效,是我在这个项目里面最踩的一次坑。现象很简单:我故意让订单明细插入失败,希望整个下单回滚,但订单主表还是被插入进去了。排查下来原因有三层,很多文章只说其中一层,我在这里把三层都列出来:

第一层:spring.xml 里没有配置事务管理器。光在方法上写 @Transactional 是不行的,必须有:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>

然后再加<tx:annotation-driven transaction-manager="transactionManager"/>,让 Spring 认识注解。

第二层:Service 类或方法没有被 Spring 管理。如果 Service 类没有加@Service注解、或者没在 XML 里扫描,事务管理器找不到这个 Bean,注解自然无效。

第三层:自调用问题。有时候方法确实加了@Transactional,但试着调试发现事务没生效,仔细一看原来是 Controller 里注入的 Service 接口 A 方法调用了同一个实现类里的 B 方法,B 方法上的事务注解被 A 直接绕过。这是最容易忽略的一层。解决办法也很简单:把 B 方法抽到另外一个 Service 类里,或者新建一个 Bean 来调用,确保调用关系经过代理。

我后来养成了一个习惯:项目里涉及多表写入的操作,全部走独立的 Service 方法,绝不从 Controller 里把顺序流程调一串方法。

5.3 商品图片上传与静态资源访问

饰品商城这种卖颜值的项目,商品图是刚需。图片上传本身不难,麻烦的是浏览器访问不到图片。

我的方案是:商品图片存储在本地磁盘的E:/xiaoshi/upload/目录,数据库里只存相对路径,比如/upload/xxl.jpg。然后在 SpringMVC 里配置一个虚拟映射,把/upload/**映射到本地磁盘目录:

<mvc:resources mapping="/upload/**" location="file:E:/xiaoshi/upload/"/>

这样用户访问http://localhost:8080/upload/xxl.jpg,Tomcat 就会去E:/xiaoshi/upload/找文件。

这个方案有几个注意点:第一,绝对路径不要写死在代码里,我后来改成了配置项,因为换一台电脑部署,路径就废了;第二,上传时要做文件类型和白名单校验,只允许 jpg、png、gif、webp,大小限制在 5MB 以内,不然容易被传一些奇奇怪怪的文件上来;第三,商品列表页能出图之后,记得检查一下浏览器控制台有没有 404,大概率是虚拟映射没生效,而不是文件没传上去。

5.4 并发环境下库存超卖

我前面在数据库设计部分提到过条件更新库存,这里再补充一个真实场景。我第一次没有条件更新,代码逻辑是:

  1. 查库存
  2. 判断库存是否充足
  3. 充足就执行 UPDATE 扣减

在单用户测试时没问题,但我开了两个浏览器窗口,同时点“立即购买”,库存直接变成了负数。原因很好理解:两个请求都走到了“查库存”这一步,都看到库存有 1 件,然后都执行了扣减。

后来我改成直接执行条件更新,判断受影响行数:

int rows = productMapper.deductStock(productId, quantity); if (rows == 0) { throw new RuntimeException("库存不足"); }

对应的 SQL 就是前面说的:

UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

改完之后,我用两个线程同时下单做了简单压测,库存保持了正确值。这里我还要强调一个观点:毕业设计不一定需要 Redis 分布式锁、消息队列这些重型方案,用数据库最朴素的原子 UPDATE 就能解决超卖,关键是思路要对。如果论文里把“为什么不用分布式锁”也写清楚,反而更能体现你对并发控制的权衡思考。

5.5 Tomcat部署时本地路径问题

最后一个让我在答辩前夜差点崩溃的坑,是部署到另一台电脑或不同操作系统时,路径全部失效。比如我在 Windows 开发机上配置的图片路径是E:/xxx/ffff/,拷到 Linux 服务器后找不到目录。

解决方式是:把路径从代码和配置 XML 中抽出来,放到一个 properties 文件里,然后在 Spring 配置里读取。部署时只需要改一个文件,不用动代码。

upload.path=/data/xiaoshi/upload/

顺便说一句,MyBatis 的 mapper XML 文件路径也是类似问题。打包时如果发现找不到 mapper 文件,多半是 maven 默认不打包 xml 文件,需要在 pom.xml 里指定resources目录。这个细节不大,但一旦踩到,启动就会报 Invalid bound statement。

6. 测试、部署与上线前的检查清单

6.1 功能测试用例设计

我不太建议在这个阶段只靠“点一点”来测,最好写一张简单的测试用例表,把核心交易链路的关键场景都覆盖到。下面是我自己用的用例表,你可以直接参考:

编号用例前置条件预期结果
T01注册新用户无注册成功,密码非明文
T02登录错误密码已注册用户提示密码错误,不进入系统
T03未登录直接访问购物车无跳转登录页面
T04加购同款商品两次商品已上架购物车数量累加
T05下单选中的购物车商品购物车有选中商品订单生成,库存减少,购物车清空
T06库存不足时下单商品库存小于购买数量下单失败,库存不变
T07普通用户访问后台已登录普通用户拦截并跳转,无权限
T08管理员下架商品商品处于上架状态商品前端不可见

每一个用例我都建议手动跑一遍,并且记录结果。答辩老师如果让你演示“异常流程”,你直接把 T03 或 T06 拿出来,会比只演示正常流程显得扎实很多。

6.2 打包部署到Tomcat的操作记录

SSM 项目最终打的是 war 包。我用的命令是:

mvn clean package -DskipTests

打出来的 war 包在target/目录下,文件名取决于 pom.xml 里的 artifactId。我把它复制到 Tomcat 的webapps/目录下,然后启动 Tomcat:

bin/startup.sh

Tomcat 会解压 war 包,然后通过http://localhost:8080/你的项目名/访问。如果你的上下文路径不是默认的ROOT,URL 要带上项目名。

部署前还有三件事要确认:MySQL 数据库是否创建好并执行了初始化 SQL;数据库连接密码是否改成了服务器上的密码;上传目录是否提前创建并且有写权限。这三件事少一件,启动都会报错,而且是那种要翻日志才能看懂的错。我建议部署前先看一遍logs/catalina.out再问为什么。

6.3 上线前要检查的细节

最后分享几个很容易被忽略的检查项:

  • 字符编码。JSP 页面、Controller 的request.setCharacterEncoding("UTF-8")、MySQL 连接 URL 里的characterEncoding=utf8要一致,不然中文乱码会出现在商品名、订单地址里。
  • 数据库连接池。Druid 的连接验证 SQL 要配置validationQuery,避免数据库重启后连接失效。
  • Session 超时。Tomcat 默认 Session 超时时间我调整到了 30 分钟,防止用户逛着逛着就被踢下线。
  • 日志。不要只依赖 System.out.println,我把关键操作(登录、下单、支付)都用 slf4j 打到了日志文件,排查问题时特别好用。

7. 最后再分享几点体会

做完“小饰界”这个项目之后,我最大的感受是:商城系统表面上是一堆增删改查,但真正要把数据一致性、权限边界、异常流程都处理好,需要的是系统思维。比如库存超卖,看起来是代码问题,其实是数据库设计和事务边界的问题;购物车存 Session 还是存表,看起来是技术选型,其实是业务场景决定的数据存储问题。

如果你正在写论文,我建议在“系统实现”章节里,不要大段贴全部代码,而是把核心业务方法的数据流转讲清楚:前端发生什么、Controller 接住什么、Service 怎么处理、Mapper 怎么写 SQL、数据库返回什么。这个思路比堆代码代码量更能体现你对项目的理解。

再给你一个小建议:把这个项目本地跑通之后,尝试自己重构一次,把 Controller 里的业务逻辑全部抽到 Service 层,把你用过的人为操作全部清理一遍。这个过程比多看十篇教程都管用。我当时重构完之后,才发现代码优雅不优雅不是审美问题,而是维护成本问题——写论文时几乎每一个方法都能直接挪到文档里去讲。

如果你也想做一个类似的商城练手,从“小饰界”这个模型出发是挺合适的路径。遇到问题别急着换框架,先把 SSM 的每一层处理逻辑吃透,后面再去碰 Spring Boot 或者微服务,你会发现自己对 Java 后端的理解完全不一样。

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

Qt多媒体模块开发全攻略:从架构到播放器与摄像头实战

Qt 多媒体模块是个很有意思的领域&#xff0c;凡是把它当“Qt 里那个能放视频的控件”来用的&#xff0c;基本都踩过坑。这个模块真正能做的&#xff0c;远不止弹个视频窗口那么简单——音频播放、摄像头采集、录像、录音、视频帧实时抓取&#xff0c;甚至机器视觉的数据接入&a…

作者头像 李华
网站建设 2026/10/3 10:16:28

AI辅助论文大修全流程:从意见拆解到回复信生成的高效指南

1. 大修流程为什么值得用AI重做——先搞清楚效率瓶颈在哪 先聊一个我自己的真实经历。去年年底我帮一个师弟处理一篇医学信息学期刊的major revision&#xff0c;三个审稿人&#xff0c;加起来47条意见&#xff0c;其中还有一条是审稿人直接抄了一整页参考文献来"建议引用…

作者头像 李华
网站建设 2026/10/3 10:15:11

大模型应用落地指南:从模型选型到本地部署与微调实践

1. 为什么从模型和应用两个维度来盘点大模型站在2026年9月这个时间点往回看&#xff0c;大模型行业早就过了“今天发了几个新模型”的阶段。现在你问一个正在做产品的朋友&#xff0c;他在用什么模型&#xff0c;他大概率会反问一句&#xff1a;你要解决什么问题&#xff1f;这…

作者头像 李华
网站建设 2026/10/3 10:14:51

海南省市县乡村五级行政区划SHP数据全解析

简介&#xff1a;海南省五级行政区划SHP矢量数据面向GIS开发者、城乡规划与空间分析研究人员&#xff0c;涵盖省、市、县、乡镇&#xff08;街道&#xff09;、社区&#xff08;村界&#xff09;完整层级&#xff0c;可支撑宏观规划到基层精细化管理的地图制作与空间分析。压缩…

作者头像 李华
网站建设 2026/10/3 10:14:36

Vue3组件化开发实战:从脚手架搭建到工程落地

1. 组件化编程的认知重构与脚手架的价值先说点实在的。很多人学Vue&#xff0c;前一周还在看模板语法、指令、计算属性&#xff0c;一到"组件化"这三个字就懵了——组件到底是什么&#xff1f;为什么要拆&#xff1f;拆到什么程度算合理&#xff1f;说白了&#xff0…

作者头像 李华
网站建设 2026/10/3 10:14:35

英伟达芯片级智能体安全平台:GPU看门狗守护Agent运行时

英伟达最近发布的智能体安全平台&#xff0c;把安全监控的答案放到了“芯片”这个层级上。很多人乍一看觉得这是硬件厂商在秀肌肉&#xff0c;但如果你真正做过Agent落地&#xff0c;就会明白这个方向比软件层打补丁靠谱得多。过去一段时间&#xff0c;我一直在帮客户做企业级A…

作者头像 李华