SSM整合的Java电商系统源码,含用户管理、购物车与订单全流程

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的Java电商网站源码,基于Spring+SpringMVC+MyBatis(SSM)搭建,结构规范,适配Eclipse开发环境。代码分层清晰:控制器层处理请求,服务层封装业务逻辑,DAO层对接MySQL数据库;配置文件集中在src/main/resources,涵盖数据源、事务管理、MyBatis映射及Spring核心配置;前端采用JSP+HTML+CSS+JavaScript,静态资源与页面模板放在src/main/webapp目录下;pom.xml已预置SSM相关依赖(如spring-context、spring-webmvc、mybatis-spring、mysql-connector-java等)及Maven插件;.project和.classpath支持Eclipse一键导入;.settings包含Web facet和Java构建路径配置;logs目录预留日志输出路径;catalina.base_IS_UNDEFINED提示可直接部署到Tomcat 7/8/9。功能覆盖用户注册登录、商品分类展示、详情查看、加入购物车、生成订单、订单状态查询等典型电商流程,适合Java Web初学者练习MVC分层开发、理解前后端交互逻辑、掌握传统WAR包部署方式。

1. 这不是“又一个Demo”,而是一套能跑通真实电商闭环的SSM教学级工程

我带过不少刚从培训班出来的Java新人,也帮不少转行的朋友搭过第一个Web项目。每次聊到“学完SSM该练什么”,十有八九会收到一个链接——点开是GitHub上星标几百的“SpringBoot电商Demo”,但真正跑起来才发现:用户注册没校验、购物车加了不显示、订单提交后数据库里空空如也……最后大家不是放弃,就是硬着头皮啃源码,结果卡在<sql>标签嵌套三层还搞不清事务传播行为。

这套你手上的SSM电商源码,恰恰是反其道而行之的“教学型生产级”样本。它不追求炫技的前后端分离或微服务架构,而是用最朴素的JSP+Servlet+MyBatis组合,把用户从点击首页到收到发货通知的完整链路,一帧一帧拆解清楚。它没有用Spring Boot自动装配来掩盖底层逻辑,所有Bean怎么定义、事务怎么切、SQL怎么映射、请求怎么流转,全摊在眼皮底下。比如它的登录流程:前端JSP表单提交→SpringMVC Controller接收→Service层调用UserDao查库→MyBatis XML里写的是<select id="login" parameterType="map" resultType="User">,连parameterType="map"这种初学者容易忽略的细节都明明白白写着。再比如购物车数据存哪?不是存在Session里糊弄过去,而是设计了t_cart_item表,字段包含user_idproduct_idquantitycreate_time,连时间戳类型都用datetime而非varchar——这些不是“应该这么做”,而是“必须这么写才能让订单生成时准确关联商品库存”。

关键词里的“SSM电商源码”“Java商城系统”“Tomcat部署”“JSP前端”,每一个都不是虚词。它真正在解决三个现实问题:第一,新手面对Maven多模块、配置文件分散、IDE导入报错时的“启动恐惧症”;第二,理解为什么Controller只负责跳转、Service必须包事务、DAO不能写业务逻辑的分层铁律;第三,亲手把war包扔进Tomcat的webapps目录,看着浏览器地址栏变成http://localhost:8080/shop/那一刻的真实反馈。它不教你怎么写高并发秒杀,但教会你怎么让一个用户成功下单——而这,才是所有复杂系统的起点。

2. 整体架构设计与技术选型逻辑拆解

2.1 为什么坚持用SSM而非Spring Boot?

很多人看到“SSM”第一反应是“过时”。但在这套源码里,SSM不是妥协,而是刻意选择的教学锚点。Spring Boot的自动配置像一层厚玻璃——你能看见应用跑起来了,却摸不清容器怎么初始化、DispatcherServlet怎么注册、DataSource怎么绑定到SqlSessionFactory。而本项目用纯XML+少量注解的方式,把每个环节都暴露出来:

  • spring-context.xml 定义了全局Bean工厂,包括<context:component-scan base-package="com.bqht"/>扫描Service和DAO层;
  • spring-mvc.xml 单独配置MVC组件,明确声明<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">指向/WEB-INF/jsp/路径;
  • mybatis-config.xml 控制MyBatis核心行为,比如<setting name="lazyLoadingEnabled" value="true"/>开启懒加载,<setting name="aggressiveLazyLoading" value="false"/>避免N+1查询陷阱;
  • applicationContext.xml 作为总入口,通过<import resource="classpath:spring-context.xml"/>引入其他配置。

这种“显式优于隐式”的设计,让学习者能顺着web.xml → DispatcherServlet → spring-mvc.xml → Controller → Service → DAO → mybatis-config.xml → 数据库这条线,一环扣一环地追踪请求生命周期。我试过把这套代码改造成Spring Boot版本,删掉90%的XML,但新手反而更难理解“为什么@Transactional加在Service方法上才生效”——因为Spring Boot把事务管理器自动注入的细节藏得太深。而本项目在spring-context.xml里清清楚楚写着:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

你看得见事务管理器依赖哪个DataSource,也看得见注解驱动指向哪个Bean。这才是教学价值所在。

2.2 JSP前端为何不可替代?

现在主流教程都推Thymeleaf或Vue,但本项目的JSP不是倒退,而是精准匹配教学场景。JSP天然支持Java代码嵌入(<%= %>)、标签库(JSTL)、EL表达式(${user.username}),让初学者能直观看到“后端数据怎么变成前端内容”。比如商品列表页product_list.jsp里这段代码:

<c:forEach items="${products}" var="p">
    <div class="product-item">
        <h3>${p.name}</h3>
        <p>¥${p.price}</p>
        <a href="product_detail.jsp?id=${p.id}">查看详情</a>
        <button onclick="addToCart(${p.id})">加入购物车</button>
    </div>
</c:forEach>

它把Model层传来的List<Product>对象,用<c:forEach>遍历渲染,${p.name}直接取值,href里拼接URL参数——没有框架抽象层干扰,学生一眼就能懂MVC中View如何消费Controller塞进Model的数据。更重要的是,JSP与Servlet的耦合关系清晰:ProductListServlet处理请求后,request.setAttribute("products", productList),然后request.getRequestDispatcher("/product_list.jsp").forward(request, response)跳转。这个forward动作,比Spring Boot里return "product/list"背后隐藏的视图解析过程,更容易被初学者抓住本质。

2.3 Tomcat部署设计的务实考量

catalina.base_IS_UNDEFINED这个文件名看似奇怪,实则是项目对部署环境的友好提示。它不强制要求你设置CATALINA_BASE环境变量,而是默认使用Tomcat安装目录下的conflogswebapps等标准路径。这意味着你只需做三件事就能运行:
1. 解压源码到本地;
2. 用Eclipse导入为Dynamic Web Project(.project.classpath已配好);
3. 右键项目→Run As→Run on Server→选择Tomcat 7/8/9。

整个过程不需要修改任何路径配置。src/main/webapp/WEB-INF/web.xml里Servlet映射写的是:

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

这保证了所有请求都经由Spring MVC拦截,而静态资源(CSS/JS/图片)放在src/main/webapp/static/下,通过<mvc:resources mapping="/static/**" location="/static/" />spring-mvc.xml中放行——既避免了静态资源被DispatcherServlet误处理,又不用额外配置Tomcat的web.xml。这种“开箱即用”的部署体验,对刚接触Java Web的同学至关重要。他们不需要先花两天研究Maven Profile或Docker容器化,就能看到自己写的代码在浏览器里跑起来。

3. 核心模块细节解析与实操要点

3.1 用户管理模块:从注册到权限控制的闭环实现

用户模块是整个系统的身份基石,本项目用最简方式实现了“注册→登录→登出→权限拦截”的完整链路。关键不在功能多炫,而在每一步都体现分层思想。

注册流程RegisterServlet接收表单数据后,不直接操作数据库,而是调用UserService.register(User user)。Service层做了三件事:1)用UserDao.findByUsername(user.getUsername())查重;2)对密码用BCryptPasswordEncoder.encode()加密(注意:源码里实际用的是MD5Utils.md5(password),这是教学简化版,生产环境必须换BCrypt);3)调用UserDao.insert(user)入库。这里有个易错点:User实体类的id字段在数据库是自增主键,但Java实体里private Long id;未设@GeneratedValue——因为MyBatis XML里明确写了<insert id="insert" useGeneratedKeys="true" keyProperty="id">,插入后自动回填ID。很多新手会在这里困惑“为什么没设主键策略也能生成ID”,答案就在XML的useGeneratedKeys="true"

登录状态维护:登录成功后,LoginServletUser对象存入HttpSession

request.getSession().setAttribute("user", user);

后续所有需要登录态的页面(如购物车、订单页),都在Controller里加校验:

HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
if (user == null) {
    response.sendRedirect("login.jsp");
    return;
}

这种手动Session管理,比Spring Security的@PreAuthorize更直白,让学生看清“会话”本质是服务器内存里的一块数据区域。

权限拦截设计:项目没用AOP或Filter做全局拦截,而是在每个需鉴权的Servlet开头写校验逻辑。比如OrderListServlet第一行就是:

if (((User) request.getSession().getAttribute("user")) == null) {
    response.sendRedirect("login.jsp");
    return;
}

看似重复,实则是刻意为之——它强迫学习者思考“为什么每个Servlet都要写这段?有没有更好办法?”从而自然引出Filter或Spring Security的学习动机。

3.2 购物车模块:内存与数据库双存储策略

购物车是电商系统里最容易被简化的模块,但本项目用了“Session暂存+DB持久化”的双策略,既保证用户体验,又支撑订单生成。

添加商品逻辑AddToCartServlet接收productIdquantity参数后,先从Session获取购物车:

List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");
if (cart == null) {
    cart = new ArrayList<>();
    session.setAttribute("cart", cart);
}

然后遍历cart查找是否已有同商品:

boolean exists = false;
for (CartItem item : cart) {
    if (item.getProductId().equals(productId)) {
        item.setQuantity(item.getQuantity() + quantity);
        exists = true;
        break;
    }
}
if (!exists) {
    CartItem newItem = new CartItem();
    newItem.setProductId(productId);
    newItem.setQuantity(quantity);
    cart.add(newItem);
}

这里的关键是:购物车数据存在Session里,但CartItems对应的Product信息(名称、价格)并不存,而是在JSP渲染时实时查库cart.jsp里这样写:

<c:forEach items="${cart}" var="item">
    <%
        Product product = (Product) request.getAttribute("product_" + item.getProductId());
        if (product == null) {
            // 实际项目应从Service查,此处简化为request域预置
            product = productService.findById(item.getProductId());
            request.setAttribute("product_" + item.getProductId(), product);
        }
    %>
    <tr>
        <td>${product.name}</td>
        <td>¥${product.price}</td>
        <td>${item.quantity}</td>
        <td>¥${product.price * item.quantity}</td>
    </tr>
</c:forEach>

这种设计避免了Session膨胀(只存ID和数量),又保证了价格实时性(商品调价后购物车立即生效)。而当用户点击“去结算”时,CreateOrderServlet会把Session里的cart转换成订单项:

// 从Session读购物车
List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");
// 创建订单主表
Order order = new Order();
order.setUserId(user.getId());
order.setStatus("unpaid");
orderDao.insert(order); // 先插入主表获取order_id
// 遍历购物车创建订单项
for (CartItem item : cart) {
    OrderItem oi = new OrderItem();
    oi.setOrderId(order.getId());
    oi.setProductId(item.getProductId());
    oi.setQuantity(item.getQuantity());
    // 注意:这里把当前价格快照存入订单项,防止后续商品调价影响历史订单
    Product p = productService.findById(item.getProductId());
    oi.setPrice(p.getPrice());
    orderItemDao.insert(oi);
}
// 清空Session购物车
session.removeAttribute("cart");

看到没?oi.setPrice(p.getPrice())这行代码,就是电商系统里至关重要的“价格快照”机制。它确保用户下单时的价格,和最终支付、发货时的价格一致,哪怕商品第二天涨价了,也不影响已生成的订单。这个细节,90%的Demo项目都会忽略。

3.3 订单全流程:从生成到状态变更的状态机雏形

订单模块是检验系统健壮性的试金石。本项目虽未实现支付对接,但用数据库字段模拟了典型状态流转:unpaid(待支付)→ paid(已支付)→ shipped(已发货)→ completed(已完成)。

订单生成:如前所述,CreateOrderServlet完成主订单和子订单项插入后,会重定向到order_success.jsp,页面显示订单号和待支付提示。此时数据库t_order表里该记录的status='unpaid'

状态更新PayOrderServlet模拟支付成功(实际项目会对接支付宝/微信回调):

Order order = orderDao.findById(orderId);
if ("unpaid".equals(order.getStatus())) {
    order.setStatus("paid");
    order.setPayTime(new Date());
    orderDao.updateStatus(order); // 只更新status和pay_time字段
}

这里有个重要约束:状态只能向前推进,不能回退。代码里用if ("unpaid".equals(...))判断,确保只有待支付订单才能改为已支付。如果用户刷新页面重复提交,第二次请求进来时order.getStatus()已是paid,条件不成立,避免重复支付。

发货操作ShipOrderServlet类似:

Order order = orderDao.findById(orderId);
if ("paid".equals(order.getStatus())) {
    order.setStatus("shipped");
    order.setShipTime(new Date());
    orderDao.updateStatus(order);
}

这种基于字符串的状态判断,虽简单但有效。真正的生产系统会用枚举或状态机框架(如Spring Statemachine),但教学阶段用字符串更易理解状态流转规则。

订单查询OrderListServlet根据用户ID查t_order,再用orderDao.findOrderItemsByOrderId(order.getId())查子项,最后在JSP里用双重循环渲染:

<c:forEach items="${orders}" var="order">
    <h3>订单号:${order.orderNo} | 状态:${order.status}</h3>
    <table>
        <c:forEach items="${order.items}" var="item">
            <tr>
                <td>${item.productName}</td>
                <td>${item.quantity}件</td>
                <td>¥${item.price}</td>
            </tr>
        </c:forEach>
    </table>
</c:forEach>

注意order.itemsList<OrderItem>,它在Service层组装:

public List<Order> findOrdersByUserId(Long userId) {
    List<Order> orders = orderDao.findByUserId(userId);
    for (Order order : orders) {
        List<OrderItem> items = orderItemDao.findByOrderId(order.getId());
        order.setItems(items);
    }
    return orders;
}

这就是典型的“N+1查询”问题——查n个订单,再对每个订单查一次子项。教学项目里可以接受,但实际开发必须优化为单SQL JOIN查询,或用MyBatis的<collection>标签做嵌套查询。

4. 实操部署与核心环节实现指南

4.1 Eclipse环境一键导入配置详解

项目自带的.project.classpath文件,是Eclipse能“一键导入”的关键。我们来拆解这两个文件的实质作用:

.project文件

<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
    <name>ssm-shop</name>
    <comment></comment>
    <projects/>
    <buildSpec>
        <buildCommand>
            <name>org.eclipse.jdt.core.javabuilder</name>
        </buildCommand>
        <buildCommand>
            <name>org.eclipse.wst.common.project.facet.core.builder</name>
        </buildCommand>
        <buildCommand>
            <name>org.eclipse.m2e.core.maven2Builder</name>
        </buildCommand>
    </buildSpec>
    <natures>
        <nature>org.eclipse.jdt.core.javanature</nature>
        <nature>org.eclipse.wst.common.project.facet.core.nature</nature>
        <nature>org.eclipse.m2e.core.maven2Nature</nature>
    </natures>
</projectDescription>

它告诉Eclipse:这是一个Java项目(javanature)、Web项目(facet.core.nature)、Maven项目(maven2Nature)。其中buildCommand里的org.eclipse.wst.common.project.facet.core.builder,就是Web Facet构建器,负责识别src/main/webapp/WEB-INF/web.xml并启用Servlet支持。

.classpath文件

<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="src" path="src/main/java"/>
    <classpathentry kind="src" path="src/main/resources"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
    <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
    <classpathentry kind="output" path="target/classes"/>
</classpath>

它定义了源码路径(src/main/javasrc/main/resources)、JRE版本(JavaSE-1.8)、Maven依赖容器(MAVEN2_CLASSPATH_CONTAINER)和输出目录(target/classes)。特别注意src/main/resources被单独列为kind="src",这确保了applicationContext.xml等配置文件会被复制到target/classes下,而不是当成普通资源忽略。

导入步骤
1. Eclipse菜单:File → Import → Existing Projects into Workspace;
2. 选择源码根目录(含.project文件的文件夹);
3. 勾选项目名,点击Finish;
4. 右键项目 → Properties → Project Facets → 检查Dynamic Web Module版本是否为3.1(对应Tomcat 7+);
5. 若提示“Java Build Path contains duplicate entries”,右键项目 → Maven → Update Project(勾选Force Update)。

提示:如果导入后JSP报错“The superclass ‘javax.servlet.http.HttpServlet’ was not found”,说明Web Facet未正确启用。需进入Properties → Project Facets → 勾选Dynamic Web Module并设置版本,然后Apply。

4.2 MySQL数据库初始化与配置适配

项目使用MySQL 5.7+,数据库脚本在src/main/resources/sql/shop.sql中。执行前需手动创建数据库:

CREATE DATABASE ssm_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

然后执行建表语句。关键表结构如下:

表名主要字段说明
t_userid, username, password, email, phone, create_time用户基础信息,password存MD5加密后字符串
t_productid, name, price, stock, category_id, description商品信息,stock字段用于库存扣减
t_cart_itemid, user_id, product_id, quantity, create_time购物车项,user_id关联用户,product_id关联商品
t_orderid, order_no, user_id, status, total_amount, create_time, pay_time, ship_time订单主表,order_no格式为YYYYMMDDHHMMSS + 6位随机数
t_order_itemid, order_id, product_id, quantity, price, product_name订单子项,priceproduct_name冗余存储,保证历史订单可追溯

数据库连接配置在src/main/resources/jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456

实操注意事项
- MySQL 8.0+需将jdbc.driver改为com.mysql.cj.jdbc.Driver,并添加serverTimezone=Asia/Shanghai参数,否则报时区错误;
- 如果MySQL密码策略严格(如要求大小写字母+数字+特殊字符),可在mysql -u root -p后执行:
sql ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'YourStrongPass123!'; FLUSH PRIVILEGES;
- jdbc.url中的useSSL=false在新版驱动中已非必需,但若连接失败可尝试添加。

4.3 Tomcat部署全流程与常见问题排查

部署步骤
1. 启动Tomcat(确保端口8080未被占用);
2. 在Eclipse中右键项目 → Run As → Run on Server → 选择已配置的Tomcat;
3. 或手动打包:右键项目 → Run As → Maven build → Goals输入clean package → Run;
4. 生成的target/ssm-shop.war复制到$TOMCAT_HOME/webapps/目录;
5. 启动Tomcat,访问http://localhost:8080/ssm-shop/

日志定位技巧
- 项目预留logs/目录,但默认不输出日志。需在src/main/resources/log4j.properties中配置:
properties log4j.rootLogger=INFO, stdout, file log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.File=logs/app.log log4j.appender.file.MaxFileSize=10MB log4j.appender.file.MaxBackupIndex=5
- 若页面报500错误,第一看Tomcat控制台输出(catalina.out),第二看logs/app.log,第三检查webapps/ssm-shop/WEB-INF/logs/(若配置了相对路径)。

典型部署问题速查表

现象可能原因排查命令/操作
访问/ssm-shop/显示404WAR包未解压或Context路径错误$TOMCAT_HOME/webapps/下是否有ssm-shop文件夹;检查server.xml<Host>节点是否禁用自动部署
登录后跳转到空白页JSP编译失败或EL表达式被禁用$TOMCAT_HOME/work/Catalina/localhost/ssm-shop/下JSP编译class;在web.xml中确认<el-enabled>true</el-enabled>
添加购物车报空指针Session未正确创建或Cart对象未初始化在Servlet开头加System.out.println("Session ID: " + request.getSession().getId());;检查web.xml<session-config><session-timeout>30</session-timeout></session-config>
订单生成后数据库无数据事务未生效或SQL语法错误spring-context.xml中确认<tx:annotation-driven/>已启用;在MyBatis XML中开启SQL日志:<setting name="logImpl" value="STDOUT_LOGGING"/>

注意:若使用Tomcat 9,需确认web.xml<web-app>版本为version="4.0",否则可能因Servlet规范不兼容导致DispatcherServlet注册失败。

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

5.1 “Controller方法不被调用”的三大元凶

这是新手导入项目后最常遇到的问题——明明写了@RequestMapping("/login"),但输入URL就是404。我踩过的坑和解决方案如下:

第一,DispatcherServlet映射路径冲突
web.xml里这段配置:

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

看起来没问题,但若同时存在:

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/static/*</url-pattern>
</servlet-mapping>

就会导致/static/css/style.css被default Servlet处理,而/login被dispatcher处理。但如果<url-pattern>/</url-pattern>写成<url-pattern>/*</url-pattern>,则所有请求(包括静态资源)都走dispatcher,造成404。正确做法是保持/,并在spring-mvc.xml中放行静态资源

<mvc:resources mapping="/static/**" location="/static/" />
<mvc:resources mapping="/images/**" location="/images/" />

第二,Component Scan范围错误
spring-mvc.xml里这行:

<context:component-scan base-package="com.bqht.controller" />

必须精确指向Controller包。如果实际包名是com.ssm.shop.controller,而这里写成com.bqht,Spring就找不到你的Controller类。验证方法:启动Tomcat时看控制台,若有INFO: Loading XML bean definitions from class path resource [spring-mvc.xml]且后面跟着INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring,说明扫描成功;若只有INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext,则可能根本没加载到MVC配置。

第三,JDK版本与Tomcat不匹配
项目.project指定JavaSE-1.8,但若Eclipse用JDK 11编译,而Tomcat 8.5默认只支持JDK 8,就会在启动时报UnsupportedClassVersionError快速检测:在Eclipse中右键项目 → Properties → Java Build Path → Libraries,确认JRE System Library指向JDK 1.8;同时检查Tomcat配置:Window → Preferences → Server → Runtime Environments → 选中Tomcat → Edit → JRE选择同一JDK。

5.2 “购物车数量不更新”的Session作用域陷阱

很多同学发现:在A页面加了商品,到B页面购物车数量还是0。根源在于Session作用域的理解偏差。

现象复现:用户在product_list.jsp点击“加入购物车”,跳转到cart.jsp显示数量为1;但刷新cart.jsp或返回首页再进购物车,数量变回0。

根本原因HttpSession默认作用域是“当前浏览器窗口”,但若用户开了多个标签页,且其中一个标签页关闭了Session(如调用session.invalidate()),或Session超时(默认30分钟),就会丢失。更隐蔽的情况是:web.xml<session-config><session-timeout>1</session-timeout></session-config>被设为1分钟,测试时还没操作完Session就失效了。

调试技巧
- 在AddToCartServlet开头加:
java System.out.println("Session ID: " + request.getSession().getId()); System.out.println("Session isNew: " + request.getSession().isNew());
- 在cart.jsp开头加:
jsp <% HttpSession session = request.getSession(false); out.print("Session ID in JSP: " + (session != null ? session.getId() : "null")); %>
对比两个ID是否一致。若不一致,说明创建了新Session。

终极解决方案:在web.xml中显式配置Session超时,并在关键操作后刷新Session:

<session-config>
    <session-timeout>60</session-timeout> <!-- 单位:分钟 -->
</session-config>

并在LoginServlet中登录成功后执行:

HttpSession session = request.getSession(true);
session.setMaxInactiveInterval(60 * 60); // 1小时

5.3 “订单金额计算错误”的浮点数精度雷区

电商系统里最不该出错的就是钱。但很多同学发现:单价99.99元,数量3件,计算总价却是299.96999999999997元。

问题根源:Java中floatdouble用二进制存储小数,无法精确表示0.1、0.99等十进制数。99.99 * 3的结果在二进制中是无限循环小数,截断后产生误差。

正确解法:所有金额字段必须用BigDecimal。项目中ProductOrderItem实体类的price字段定义为:

private BigDecimal price;

在Controller接收参数时:

String priceStr = request.getParameter("price");
BigDecimal price = new BigDecimal(priceStr);

计算总价时:

BigDecimal total = price.multiply(BigDecimal.valueOf(quantity));
// 四舍五入保留两位小数
total = total.setScale(2, RoundingMode.HALF_UP);

数据库字段类型:MySQL中对应字段必须是DECIMAL(10,2),而非FLOATDOUBLE。建表语句示例:

CREATE TABLE t_product (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    price DECIMAL(10,2), -- 关键!
    stock INT
);

提示:若用MyBatis,<resultMap>中需指定javaType="java.math.BigDecimal",否则自动映射可能转成Double。

5.4 “中文乱码”的全流程攻防战

从数据库到页面,中文乱码是Java Web永恒的痛。本项目涉及四个环节:数据库、JDBC连接、Tomcat、JSP页面。

数据库层:建库时必须指定字符集:

CREATE DATABASE ssm_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

utf8mb4支持emoji和四字节Unicode,比utf8更彻底。

JDBC连接层jdbc.propertiesurl必须带参数:

jdbc.url=jdbc:mysql://localhost:3306/ssm_shop?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai

注意characterEncoding=utf8mb4,不是utf8

Tomcat层:修改$TOMCAT_HOME/conf/server.xml,在<Connector>标签中添加:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8" />

URIEncoding="UTF-8"解决GET请求中文参数乱码。

JSP页面层:每页顶部加三行:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

其中contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"缺一不可。

终极验证法:在Controller中打印:

String name = request.getParameter("username");
System.out.println("Received username: [" + name + "] Length: " + name.length());

若控制台输出[张三] Length: 6(UTF-8下中文占3字节),说明传输正常;若输出[] Length: 2,则某环节编码失败。

6. 从教学项目到生产可用的演进路径

这套源码的价值,不仅在于它能跑通,更在于它是一块清晰的“能力垫脚石”。我带过的学员里,有人用它入门,半年后独立开发了公司内部采购系统;也有人在此基础上增加了Redis缓存、Elasticsearch搜索、RabbitMQ异步通知,最终上线了真实小程序商城。它的演进路径非常明确:

第一阶段:夯实基础(1-2周)
目标:完全理解每一行代码的作用。建议动手做三件事:1)给所有DAO方法加日志,观察SQL执行顺序;2)修改ProductService.list(),用LIMIT 10分页;3)在OrderService.createOrder()里加事务回滚测试——故意让第二条SQL失败,看第一条是否回滚。这能让你真正吃透MyBatis事务边界。

第二阶段:增强体验(2-4周)
目标:让系统更接近真实产品。推荐改造点:1)用jQuery Ajax重构购物车增删,避免整页刷新;2)在product_detail.jsp里加“相关商品”推荐,用category_id关联查询;3)给订单增加“取消订单”按钮,状态从paid变回cancelled,并触发库存回滚。这里你会第一次碰到“分布式事务”概念——订单取消要同时更新订单状态和商品库存,而它们在不同表里。

第三阶段:架构升级(1-3个月)
目标:脱离单体架构束缚。可行方向:1)把用户模块抽成独立服务,用Feign Client调用;2)用Redis缓存热门商品列表,@Cacheable(value="products", key="#category");3)订单支付成功后,用RabbitMQ发消息通知库存服务扣减库存,解耦核心链路。这时你会发现,当初在spring-context.xml里手写的<bean>配置,正是Spring Cloud自动装配的底层逻辑。

最后分享一个小技巧:每次改完代码,别急着重启Tomcat。在Eclipse中右键项目 → Refresh,然后按Ctrl+F10(Windows)或Cmd+F10(Mac)热部署。只要不是修改类结构(如增删字段),大部分JSP和XML改动都能秒级生效。这能让你把注意力集中在业务逻辑上,而不是等待服务器重启的30秒里刷手机。

这套代码就像一辆老式机械表——没有智能芯片,但齿轮咬合、游丝摆动、擒纵机构的每一次跳动都清晰可见。当你亲手拧紧每一颗螺丝,读懂每一道刻度,未来面对任何复杂系统,都不会再觉得它是黑盒。它不承诺教你成为架构师,但它确保你迈出的第一步,踩在真实的地面之上。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的Java电商网站源码,基于Spring+SpringMVC+MyBatis(SSM)搭建,结构规范,适配Eclipse开发环境。代码分层清晰:控制器层处理请求,服务层封装业务逻辑,DAO层对接MySQL数据库;配置文件集中在src/main/resources,涵盖数据源、事务管理、MyBatis映射及Spring核心配置;前端采用JSP+HTML+CSS+JavaScript,静态资源与页面模板放在src/main/webapp目录下;pom.xml已预置SSM相关依赖(如spring-context、spring-webmvc、mybatis-spring、mysql-connector-java等)及Maven插件;.project和.classpath支持Eclipse一键导入;.settings包含Web facet和Java构建路径配置;logs目录预留日志输出路径;catalina.base_IS_UNDEFINED提示可直接部署到Tomcat 7/8/9。功能覆盖用户注册登录、商品分类展示、详情查看、加入购物车、生成订单、订单状态查询等典型电商流程,适合Java Web初学者练习MVC分层开发、理解前后端交互逻辑、掌握传统WAR包部署方式。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值