ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SSM架构网上书城系统实战:从数据库设计到防超卖与部署排错

SSM架构网上书城系统实战:从数据库设计到防超卖与部署排错 简介这份基于SSM架构的网上书城系统源码包适合Java Web学习者、SSM框架初学者及需要课程设计或毕业设计参考的高校学生。项目从注册登录、图书展示与搜索、购物车管理到订单状态流转与在线支付接口完整覆盖网上商城常见业务链路同时提供JSP页面、前端样式脚本、后台分层代码及数据库初始化脚本便于边读边练。压缩包共1253个文件大小14.6MB以120个Java源码和100个JSP页面为主体另有364个JS交互脚本、146份CSS样式文件、图片素材、XML配置与SQL脚本结构清晰适合源码研读与二次开发。目前已有43人浏览学习。借助该项目可以直观理解Spring、SpringMVC、MyBatis三者如何协同工作掌握用户鉴权、购物车、订单状态机等关键实现也能导入开发工具直接部署作为个人作品集或毕设基础。1. 拿到「SSM架构网上书城系统.zip」之后先想清楚这三件事解压这套 SSM 架构的网上书城系统源码通常先看到 controller/service/dao 三层包结构、resources 下的 Spring XML 或 properties以及一个 sql 目录和一页 README。先别急着启动 Tomcat把三件事确认掉JDK 与 Spring/MyBatis 的版本是否匹配、数据库脚本要往哪个库执行、连库账号写在哪个文件。这三处没对齐多数会卡在 ClassNotFoundException 或连接超时上。我会按接手这类书城源码最常规的路子写先立住领域模型与表结构再讲 Spring SpringMVC MyBatis 的整合配置然后拆购物车、下单、减库存这些核心链路最后落到排错顺序、性能自检和上线前的接口验收。新手能照着命令跑通5 年以上的人直接看 4.3 的防超卖、5.3 的 Druid 监控和 5.4 的静态源码扫描这几处边界。2. SSM 书城系统的领域建模与数据表设计从 ER 图到建表 SQL2.1 网上书城有哪些角色与状态先把权限和订单态定死一套书城系统的实体并不复杂用户、图书、分类、购物车、订单、订单明细。边界主要在权限与状态上。前台用户能注册、登录、浏览、搜索、下单、查看订单后台管理员能维护分类与图书、上下架、改库存、处理订单状态。权限在 SSM 里最朴素的做法是登录拦截器加用户角色字段拦截器管“能不能进”角色字段管“能干什么”。订单状态建议从一开始就定成数值枚举0 待付款、1 已付款、2 已发货、3 已签收、4 已取消。不要用中文字符串直接落库枚举转 int 存 order_status前端展示时再映射中文后续统计和索引都省心。状态机的流转规则要写进 Service 层只有已付款的订单才能发货只有未付款的订单才能取消这些校验不能只放在前端按钮上否则绕过页面直接调接口就能把订单状态改乱。2.2 六张核心表的落库设计user、book、category、cart_item、orders、order_item我见过的大多数网上书城表结构都在这六张表上做加减。下面是去掉冗余后的版本InnoDB 引擎、utf8mb4 字符集、订单明细里的单价单独存一份防止图书改价影响历史订单。-- 用户表区分前台用户与后台管理员 CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT BCrypt 或加盐哈希后的密文, phone VARCHAR(20) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT 1前台用户, 2管理员, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 图书表冗余分类名避免查询时每行都 join 分类表 CREATE TABLE book ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, category_id INT UNSIGNED NOT NULL, title VARCHAR(128) NOT NULL, author VARCHAR(64) DEFAULT NULL, price DECIMAL(10,2) NOT NULL COMMENT 定价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, sales_count INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架, 0下架, cover_url VARCHAR(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_cat_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 购物车同一用户同一本书只保留一行 CREATE TABLE cart_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, book_id INT UNSIGNED NOT NULL, quantity INT NOT NULL DEFAULT 1, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车条目;这段 SQL 里有三个细节值得留意。uk_user_book 唯一键保证同一用户重复加购同一本书时走“存在则加数量”的分支而不是插入两行。book 表建了 category_id 加 status 的联合索引后台按分类筛选上架图书时能走索引下推避免扫整个图书表。user 表用户名做唯一键注册接口靠它兜住并发下的重名问题。接下来是订单主表和明细表它们解决的是“下单后图书改价怎么办”的问题。-- 订单主表订单号独立生成业务上只做唯一约束 CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已签收 4已取消, receiver VARCHAR(64) NOT NULL, address VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_created (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; -- 订单明细快照三要素后续不查 book 表也能还原交易现场 CREATE TABLE order_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_id INT UNSIGNED NOT NULL, book_id INT UNSIGNED NOT NULL, book_title VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;订单明细里的 book_title 和 price 是典型的快照字段。下单后哪怕图书改价、改名历史订单不受影响后台统计也不需要再 join book 表代价是数据冗余但对订单域来说这份冗余值得。金额一律 DECIMAL(10,2)页面传上来的数字先转 BigDecimal不要在 Service 里用 double 累加否则 0.1 加 0.2 的精度漂移足以让订单对不上账。2.3 金额、库存与关联键三处容易让书城返工的设计数据字段推荐类型理由金额类DECIMAL(10,2)精确小数避免 float/double 精度漂移状态类TINYINT占 1 字节枚举 int 化后便于索引与统计编码类VARCHAR(20~32)定长业务编码不要用 TEXT描述/地址VARCHAR(255)超出需求再升级 TEXT减少行溢出时间戳DATETIME范围到 9999 年不受 2038 问题影响库存/数量INT用 INT UNSIGNED禁止浮点实体关系一句话讲清分类 1 对多图书用户 1 对多购物车条目用户 1 对多订单订单 1 对多订单明细。外键在 SSM 书城这类高并发写场景里尽量不建物理外键依靠 Service 层事务保证一致性但订单号、用户名这类业务唯一键必须建它们是接口幂等和防重的最后一道闸。3. 从零搭起 SSM 骨架Maven 模块、web.xml 与 Spring 配置拆分3.1 按 controller/service/dao 拆模块读 SSM java 源码不再迷路网上书城这类业务目录结构比微服务简单得多。常见做法有两种单 Maven 项目包内分层或者 parent 加 ssm-book-core、ssm-book-web 两个子模块。我建议读源码阶段用单模块自己动手重构时再拆多模块。拆模块的意义在编译边界core 里只有 service、dao、domainweb 里只有 controller、拦截器、视图互相只依赖接口而不是靠包名约定自觉。pom.xml 的依赖集中在七个坐标上版本组合有个经验法则JDK 8 配 Spring 5.3.x、MyBatis 3.5.x、mybatis-spring 2.0.x、Druid 1.2.x这是 SSM 里比较常见的组合。JDK 升到 17 后Spring 要整体升到 6.x配置方式换成 Spring Boot 更省事硬套老 XML 反而麻烦。properties java.version1.8/java.version spring.version5.3.39/spring.version mybatis.version3.5.16/mybatis.version mybatis-spring.version2.0.7/mybatis-spring.version druid.version1.2.20/druid.version /properties!-- 核心依赖webmvc 自带 spring-web、spring-context -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependencyspring-webmvc 会带出 spring-web、spring-context不用重复声明spring-jdbc 提供 DataSourceTransactionManagermybatis-spring 负责把 MyBatis 的 SqlSessionFactory 交给 Spring 容器管理。这里最容易犯的错是只引 mybatis 不引 mybatis-spring结果 Spring 容器里扫不到 Mapper。数据库驱动按 MySQL 版本选5.7 用 5.1.x 驱动类8.0 用 8.x 的 mysql-connector-j驱动类名完全不同。3.2 XML 加注解的整合配置读源码先从这三个文件开始SSM 让新手头痛的原因是同一件事有 XML 和注解两套写法。我拆书城源码时先找三个文件applicationContext.xml、spring-mvc.xml、web.xml。职责只要分开项目基本不会乱。applicationContext.xml 只放数据源、事务管理器、Service 和 DAO 扫描!-- 连接池Druid 参数全部可调本地用小值先跑通 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ property namemaxWait value60000/ property namevalidationQuery valueSELECT 1/ property nametestOnBorrow valuetrue/ /bean bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean context:component-scan base-packagecom.bookstore.core.service/Druid 的 initialSize 不要小于 3maxActive 看应用线程数Tomcat 默认 200 线程时给 20 到 50 足够不是越大越好。maxWait 设 60000 表示拿不到连接时最多等 60 秒超过就抛获取连接超时本地调试可以改小到 10000早点暴露连接泄漏。validationQuery 配 SELECT 1是为了借出连接时验证这条连接有没有被 MySQL 的 wait_timeout 断开。spring-mvc.xml 只扫描 controller同时配置注解驱动、JSP 视图解析、静态资源和登录拦截器context:component-scan base-packagecom.bookstore.web.controller/ mvc:annotation-driven/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean mvc:resources mapping/static/** location/static// mvc:interceptors bean classcom.bookstore.web.interceptor.LoginInterceptor/ /mvc:interceptorsprefix 和 suffix 是 JSP 物理路径绑定Controller 里 return book/list 会被解析成 /WEB-INF/views/book/list.jsp。mvc:resources 的 mapping /static/** 表示 URL 以 /static/ 开头时直接去 webapp 根目录找文件不再进 DispatcherServlet。拦截器如果不放行静态资源页面 CSS 和 JS 会被登录拦截器一并拦掉表现为“页面 HTML 有、样式全丢”。web.xml 里再挂上字符过滤器和 DispatcherServletfilter filter-nameencoding/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingload-on-startup 等于 1 让容器启动时就初始化 DispatcherServlet而不是等第一个请求进来才初始化。url-pattern 用正斜杠而不是 *.do意味着所有请求都会进 SpringMVC静态资源只能靠前面的 mvc:resources 放行配置漏一步就会出现“登录页没有样式”的经典故障。CharacterEncodingFilter 必须放在过滤器链最前面forceEncoding 设为 true 才能同时保证请求和响应都是 UTF-8否则页面中文正常、接口返回的中文乱码。3.3 JDBC URL 与驱动类SSM 连不上 MySQL 八成卡在时区和驱动场景MySQL 5.7MySQL 8.0driverClassNamecom.mysql.jdbc.Drivercom.mysql.cj.jdbc.DriverURL 尾部参数useUnicodetruecharacterEncodingutf8追加 serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue依赖坐标mysql-connector-java 5.1.xmysql-connector-j 8.xMySQL 8 默认 caching_sha2_password 认证某些连接方式下需要先取服务器公钥不配 allowPublicKeyRetrievaltrue 会直接报 Public Key Retrieval is not allowed。serverTimezone 不配会报 CST 时区异常Asia/Shanghai 是常见写法。这两行参数写对SSM 连接数据库这一步就过了一大半。4. 把网上书城核心链路写通图书检索、购物车、下单回滚4.1 图书列表用动态 SQL 支撑多条件筛选书城列表页常见条件有分类、书名关键词、价格区间、上架状态。Service 层接收条件对象 BookQueryMapper 里用 where 与 if 拼条件select idselectPageByQuery resultTypecom.bookstore.core.domain.Book SELECT id, category_id AS categoryId, title, author, price, stock, cover_url FROM book where if testquery.categoryId ! null and query.categoryId ! 0 AND category_id #{query.categoryId} /if if testquery.keyword ! null and query.keyword ! AND (title LIKE CONCAT(%, #{query.keyword}, %) OR author LIKE CONCAT(%, #{query.keyword}, %)) /if if testquery.minPrice ! null AND price gt; #{query.minPrice} /if if testquery.status ! null AND status #{query.status} /if /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /selectwhere 标签会自动去掉第一个 AND避免写“WHERE 11”这种旧写法。LIKE CONCAT(%, #{keyword}, %) 而不是 LIKE %${keyword}%后者是字符串拼接用户输入 % or 11 -- 就能注入#{} 预编译后 keyword 只是参数。price 比较里的 是 XML 转义不能直接写大于等于号否则 XML 解析直接报错。这个 XML 片段不依赖任何自定义拦截器单独跑通后再往上套 PageHelper。4.2 购物车加购先查后插还是先查再加购物车逻辑用很短一段 Java 就能表达关键是处理“同一本书加第二次”public String addToCart(HttpSession session, Long bookId, Integer quantity) { User user (User) session.getAttribute(loginUser); if (user null) { return redirect:/login; } CartItem item cartItemMapper.findByUserAndBook(user.getId(), bookId); if (item null) { CartItem newItem new CartItem(); newItem.setUserId(user.getId()); newItem.setBookId(bookId); newItem.setQuantity(quantity); cartItemMapper.insert(newItem); } else { cartItemMapper.increaseQuantity(user.getId(), bookId, quantity); } return redirect:/cart; }cart_item 表上的 uk_user_book 唯一键保证“找不到就插找到就加数量”。increaseQuantity 对应 SQL 应该是 UPDATE cart_item SET quantity quantity #{delta} WHERE user_id ? AND book_id ?不要先在 Java 里查出当前数量加一后再整体 update两个并发请求同时读到同一个旧值就会互相覆盖。加购接口还要做数量上限校验比如单本不能超过 99这个校验放在 Controller 入参转 Integer 后立刻做不能依赖前端 input 的 max 属性。4.3 下单防超卖乐观扣减加事务回滚两段代码一次说清下单主流程是校验购物车条目非空计算总价扣库存写订单主表写订单明细清空购物车。其中扣库存最容易并发超卖。先在 BookMapper 里写乐观扣减UPDATE book SET stock stock - #{count}, sales_count sales_count #{count} WHERE id #{bookId} AND stock #{count}Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListCartItem items) { BigDecimal total BigDecimal.ZERO; Order order new Order(); order.setOrderNo(generateOrderNo()); for (CartItem item : items) { Book book bookMapper.findById(item.getBookId()); if (book null || book.getStatus() 0) { throw new ServiceException(图书不存在或已下架); } int rows bookMapper.deductStock(book.getId(), item.getQuantity()); if (rows 0) { throw new ServiceException(库存不足); } total total.add(book.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemMapper.insert(buildOrderItem(order, book, item)); } order.setTotalAmount(total); orderMapper.insert(order); cartItemMapper.clearByUser(userId); return order; }stock #{count} 放在 where 里数据库行锁保证同一本书在同一时刻只有一个事务能扣成功影响行数为 0 就抛异常Transactional 会把已经插入的订单明细一起回滚不会出现“订单生成了、库存没减”的脏数据。价格是从 book 表读出来的快照组装进 order_item 后再把 total_amount 落进 orders两次插入要么都成功要么都不成功。这里有一个事务注解的细节Transactional 默认只在 RuntimeException 下回滚受检异常不会回滚所以 rollbackFor Exception.class 是必写的。ServiceException 一般是 RuntimeException那这条规则也兜得住。如果 catch 住异常后只 return null 不重新抛出事务会正常提交库存扣减后就再也回不去了。4.4 分页与 PageHelper 的顺手坑startPage 必须紧跟查询PageHelper 使用上有一条铁律startPage 之后必须紧跟第一条 Mapper 查询中间不能插其他查询。错误示范是先 startPage再执行一条无关的 count 查询分页线程变量会串到那条 SQL 上导致 list 查出来是全量数据。PageHelper.startPage(pageNum, pageSize); ListBook books bookMapper.selectByQuery(query); PageInfoBook pageInfo new PageInfo(books);startPage 紧跟 select 之后MyBatis 拦截器会把这条 SQL 改写成带 LIMIT 的语句PageInfo 里能直接取 total、pageNum、pages、list前端分页条要什么有什么。多表 join 时 PageHelper 自动 count 的是原 SQL 包一层子查询如果 SQL 里有 GROUP BY 或 DISTINCT自动 count 可能偏大这时候手动指定 countSql或者干脆拆成两条独立查询一条 count 一条 list代码可读性更好。5. 网上书城排错顺序与性能自检从一个 404 到一次下单超时从维护视角看书城源码能在本地跑起来之后会遇到四类问题启动报错、页面 404、静态资源掉样式、下单慢或库存扣错。按下面的顺序排查大部分不用看堆栈第二页。5.1 项目导入后先把这三处对齐数据库脚本、连接串、Maven 镜像我拿到源码包后的固定操作顺序是建库并执行 sql 脚本确认六张表和初始化数据都建出来打开 jdbc.properties把 url、username、password 改成本地值Maven 仓库换国内镜像再 package避免依赖下载卡住执行 mvn clean package -DskipTests 打出 war部署到 Tomcat看 catalina.out 前 30 行确认 Druid 连接池初始化成功。有一步报错就修一步SQL 报错多半是脚本没执行完整ClassNotFoundException 多半是 Spring 和 MyBatis 版本不匹配ConnectException 不是代码问题是连接串写错或 MySQL 没起来。排错别一上来翻 Java 代码先确认环境三件套。5.2 404 与丢样式先从 DispatcherServlet 的 URL 映射查起页面 404 只有三种可能请求没进 Tomcat、进来后没匹配到 Controller、Controller 返回了但 JSP 不在预定位置。固定排查顺序# 先看请求是否真的到达应用 tail -f /path/to/tomcat/logs/localhost_access_log.*.txtURL 直接就是 /book/list 而不是 /book/list.do说明走了正斜杠映射访问日志里没有这条请求先看 context path 对不对有请求但 404看 spring-mvc.xml 的 component-scan 有没有扫到 BookControllerJSP 路径 404看 InternalResourceViewResolver 的前后缀与文件在 webapp/WEB-INF/views 下的位置是否一致样式全丢则静态资源没进入 mvc:resources 白名单。看到浏览器里 HTML 正常、CSS 全部 404十有八九是配置了 url-pattern 但漏配 mvc:resources。5.3 下单慢与数据库打满Druid 监控与慢 SQL 定位Druid 自带 StatViewServlet开发环境非常有用线上不要开放。web.xml 里加一段servlet servlet-namedruidStat/servlet-name servlet-classcom.alibaba.druid.support.http.StatViewServlet/servlet-class init-param param-nameloginUsername/param-name param-valuedruid/param-value /init-param init-param param-nameloginPassword/param-name param-valuedruid123/param-value /init-param /servlet servlet-mapping servlet-namedruidStat/servlet-name url-pattern/druid/*/url-pattern /servlet-mapping然后访问应用路径下的 /druid/sql.html按执行次数和慢查询排序基本能看出哪条 SQL 是全表扫。书城项目里常见慢 SQL 有两类订单列表没走 idx_user_created 导致回表图书条件查询漏了 status 条件导致扫全表。监控页还能看到连接池活跃数下单慢的时候盯着这个数字如果活跃数持续等于 maxActive说明事务方法里有连接没释放去查是不是 catch 住异常没有重新抛出。# 快速确认应用里有没有慢 SQL 之外的耗时 curl -s -o /dev/null -w connect%{time_connect} total%{time_total}\n \ http://localhost:8080/bookstore/book/listconnect 时间短、total 时间长瓶颈在服务端渲染或数据库connect 时间都长先怀疑网络和 Tomcat 线程池被打满。5.4 Fortify 静态源码扫描与权限越界检查发布前把 Controller 层参数全部梳理一遍。凡是出现 request.getParameter 或 RequestParam 之后直接拼进 SQL、拼进 HTML 的地方重点看入参校验防止 XSS 和 URL 越权。常见问题是购买数量 quantity 只在前端限制后台没做 1 到 99 的校验订单详情接口只校验“是否登录”没校验“订单是不是当前用户的”把订单 id 换一换就能看别人订单。现象优先检查常见原因启动找不到 DBjdbc.properties连接串、时区、账号密码页面 404DispatcherServlet 映射扫描包路径错、视图前后缀错页面没样式mvc:resources静态资源被拦截器挡住下单慢/druid/ 监控页慢 SQL、连接池打满库存变负数下单 SQL先查后改、没有乐观扣减6. 部署上线前用接口自查清单把 SSM 网上书城再验一遍交付 SSM 源码前我会做一轮不依赖页面点击的接口自查用 curl 把核心接口全部打一遍配合一个检查脚本。这样既方便验收也能保证重构后不会出现“首页能看、下单必挂”的事故。6.1 用 curl 跑一遍书城核心接口接口方法参数预期/book/listGETcategoryId、pageNum、pageSize200返回图书列表/book/1GET路径参数200返回图书详情/cart/addPOSTbookId1quantity1302 到 /cart未登录 302 到 /login/order/createPOST从购物车生成200 且 orders 表多一条记录/admin/order/pageGETpageNum1200仅管理员可访问BASEhttp://localhost:8080/bookstore # 1. 图书列表状态码和最终 URL 一起打出来 curl -s -o /dev/null -w %{http_code} %{url_effective}\n \ $BASE/book/list?categoryId1pageNum1pageSize10 # 2. 登录把 session cookie 存到 cookies.txt curl -s -c cookies.txt -d usernametestpassword123456 $BASE/login # 3. 加购必须带同一个 cookie否则会被拦截器跳登录 curl -s -b cookies.txt -d bookId1quantity1 $BASE/cart/add # 4. 下单响应体里应该能捞到订单号 curl -s -b cookies.txt $BASE/order/create第一行把 HTTP 状态码和最终 URL 同时打印重定向到哪里一眼能看出来。第二行登录拿 session cookie后面的加购和下单都要带同一个 cookie否则会话对不上。真正验收时还要把响应体里的订单金额和库里 orders、order_item 的金额做一次比对只看到 200 不算完。6.2 Tomcat 启动参数与 JVM 建议CATALINA_OPTS-Xms512m -Xmx1024m -XX:UseG1GC -Dfile.encodingUTF-8书城属于中低并发 Web 应用1G 堆足够。file.encoding 设为 UTF-8 是为了防止 Linux 默认 LANG 不是 UTF-8 导致 JSP 页面中文乱码。war 包名决定 context pathbookstore.war 访问路径是 /bookstore想用根路径访问就把 war 改名 ROOT.war同时清理 Tomcat 的 work 目录避免旧 JSP 编译缓存干扰。6.3 顺手验证静态资源响应头curl -sI $BASE/static/css/app.css | grep -iE HTTP/|content-type|cache-control预期 content-type 是 text/css。如果没有 Cache-Control 响应头去 web.xml 或过滤器里给静态资源统一加 Expires这一步不需要改 Spring 业务代码用一个 OncePerRequestFilter 就能完成。把 6.1 的 curl 命令集合成一个 check.sh 放进源码根目录的 scripts/ 下以后每次发版重跑一遍就能在用户访问前发现链路断在哪。本文还有配套的精品资源点击获取
返回列表