
简介这是一套面向Java Web初学者与课程设计开发者的购书商城系统完整源码基于SpringBoot、Mybatis、Thymeleaf与MySQL构建可用于毕业设计、课程作业或电商项目练手。压缩包共109个文件约4.81MB以35个Java源文件为核心配合10个HTML页面、9个JavaScript脚本、8个CSS样式及6个XML配置另有SQL建表脚本、yml与properties配置、Maven包装器与说明文档前后端与数据库脚本齐备。系统覆盖用户注册登录、图书浏览与搜索、分类筛选、购物车、下单购买、订单管理及后台商品、用户、数据统计等模块并预留了权限控制、搜索引擎与第三方支付等二次开发空间。已有51人学习下载适合希望快速理解SpringBoot整合Mybatis与Thymeleaf开发流程、需要可运行电商参考项目的读者。1. 从一份购书商城系统源码说起SpringBootMybatisThymeleafMySQL 到底能跑出什么很多人拿到「基于SpringBootMybatisThymeleafMySQL开发的购书商城系统」这类项目时第一反应是打开压缩包看目录然后被一堆pom.xml、application.yml、mapper文件夹和templates目录劝退。这个技术栈组合在国内 Java Web 教学和中小型电商项目里非常典型SpringBoot 负责把 Tomcat、数据源、事务、MVC 全部自动装配好Mybatis 负责把 SQL 从 Java 代码里剥离出来Thymeleaf 负责在服务端渲染页面MySQL 负责存书、订单、用户和购物车。它解决的核心问题是用一套上手成本低、资料密度高的方案快速搭出一个能浏览、能搜索、能下单、能后台管理的购书商城。这套组合适合谁适合正在做课程设计、毕业设计、或者想从零理解「一个电商系统到底有哪些表、哪些接口、哪些页面」的 Java 开发者。它不适合直接拿去做高并发生产环境因为 Thymeleaf 的服务端渲染在高并发下不如前后端分离灵活Mybatis 的手写 SQL 在复杂查询下也需要更多优化。但作为学习项目和技术验证它的完整度足够让你把「用户登录 → 图书列表 → 图书详情 → 加入购物车 → 提交订单 → 后台管理」这条链路跑通。下面我会按「先跑起来 → 再拆结构 → 再调参数 → 再避坑」的顺序把这份源码里最值得复现的部分讲清楚。2. 把项目跑起来环境准备与最小启动命令2.1 先确认 MySQL 能连上再谈 SpringBoot 启动很多新手一上来就mvn spring-boot:run结果控制台报Communications link failure或者Access denied for user rootlocalhost。血泪经验是先把 MySQL 装好、库建好、账号密码确认能登录再去碰 SpringBoot。MySQL 安装方式看你的系统Windows 下用安装包Linux 下用rpm或apt都行但要注意error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock这个经典错误通常是因为 MySQL 服务没启动或者 socket 文件路径不对。建库语句我一般直接写死字符集避免中文乱码-- 创建购书商城数据库字符集用 utf8mb4 支持 emoji 和生僻字 CREATE DATABASE bookshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 创建专用账号避免直接用 root 跑应用 CREATE USER bookuser% IDENTIFIED BY Book123456; GRANT ALL PRIVILEGES ON bookshop.* TO bookuser%; FLUSH PRIVILEGES;逻辑说明utf8mb4是必须的因为图书标题里可能出现特殊符号utf8三字节存不下。专用账号比 root 安全也方便你在application.yml里区分环境。参数说明Book123456这种密码强度在本地开发够用但如果你要部署到服务器至少换成 16 位随机串。2.2 application.yml 里四个必调参数SpringBoot 的配置文件是src/main/resources/application.yml这份源码里通常会有datasource、mybatis、thymeleaf三段。我一般会重点改这四个地方spring: datasource: url: jdbc:mysql://localhost:3306/bookshop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: bookuser password: Book123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false # 开发阶段关缓存改 HTML 不用重启 prefix: classpath:/templates/ suffix: .html encoding: UTF-8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.bookshop.entity configuration: map-underscore-to-camel-case: true # 数据库 snake_case 自动映射 Java camelCase log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印 SQL逻辑说明serverTimezoneAsia/Shanghai不加会报时区错误useSSLfalse在本地开发避免 SSL 连接错误生产环境要开thymeleaf.cachefalse是开发期热更新的关键配合 IDEA 的Build project automatically能实现改完 HTML 刷新浏览器就生效map-underscore-to-camel-case让book_name自动映射到bookName省掉大量resultMap配置。参数说明log-impl用StdOutImpl会把 SQL 打到控制台方便调试但上线前要关掉否则日志量爆炸。2.3 启动类与 Mapper 扫描启动类上必须加MapperScan否则 Mybatis 的接口不会被代理成 BeanSpringBootApplication MapperScan(com.example.bookshop.mapper) // 扫描 Mapper 接口所在包 public class BookshopApplication { public static void main(String[] args) { SpringApplication.run(BookshopApplication.class, args); } }逻辑说明MapperScan告诉 Mybatis 去哪个包下找接口并为每个接口生成动态代理。如果你不加这个注解就得在每个 Mapper 接口上单独写Mapper项目大了容易漏。参数说明包名要和你的实际目录一致别照抄com.example。3. 拆开购书商城的骨架表结构、实体与 Mybatis 映射3.1 五张核心表撑起一个最小商城购书商城不管功能多花哨核心表就这几张user用户、book图书、category分类、cart购物车、orders订单。我一般会再加一张order_item存订单明细因为一个订单里有多本书。建表时注意外键和索引CREATE TABLE book ( id INT NOT NULL AUTO_INCREMENT, book_name VARCHAR(200) NOT NULL, author VARCHAR(100) DEFAULT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category_id INT DEFAULT NULL, cover_img VARCHAR(255) DEFAULT NULL, description TEXT, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_name (book_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明price用DECIMAL(10,2)而不是FLOAT避免金额计算精度丢失stock默认值设为 0对应热词里「mysql设置默认值为0」的场景idx_name索引是为了支持图书名称模糊搜索。参数说明cover_img存图片路径如果你后面要接 MinIO 或本地文件上传这个字段就是入口。3.2 Mybatis XML 映射与 typeHandler 的取舍Mybatis 的 XML 文件放在resources/mapper/下命名对应 Mapper 接口。一个典型的图书查询select idselectByKeyword resultTypecom.example.bookshop.entity.Book SELECT id, book_name, author, price, stock, cover_img FROM book where if testkeyword ! null and keyword ! book_name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY id DESC LIMIT #{offset}, #{limit} /select逻辑说明where标签会自动处理第一个AND避免WHERE AND语法错误#{}是预编译占位符能防 SQL 注入${}是字符串拼接除非排序字段动态化否则不要用。参数说明offset和limit由 Service 层计算offset (pageNum - 1) * pageSize。如果你遇到日期类型映射问题比如 Oracle 的DATE到 Java 的LocalDateTime就需要自定义TypeHandler但 MySQL 的DATETIME一般不用。3.3 Thymeleaf 模板里最容易写错的三个表达式Thymeleaf 的语法不复杂但新手常在三处翻车th:each遍历、th:href传参、th:if判断。图书列表页典型写法div classbook-list div classbook-item th:eachbook : ${books} img th:src{/images/placeholder.png} alt封面 h3 th:text${book.bookName}书名/h3 p th:text ${book.price}价格/p a th:href{/book/detail(id${book.id})}查看详情/a span th:if${book.stock 0}有货/span span th:unless${book.stock 0}缺货/span /div /div逻辑说明th:each的变量名book在循环内可直接用${book.xxx}{/book/detail(id${book.id})}会生成/book/detail?id1比手拼 URL 安全th:if和th:unless是互斥判断。参数说明${books}是 Controller 里model.addAttribute(books, bookList)传过来的。注意 Thymeleaf 3 之后th:text里的字符串拼接要用不能直接写${book.price}元。4. 参数调优与常见报错排查从能跑到跑稳4.1 数据库连接池怎么选、怎么配SpringBoot 默认用 HikariCP性能已经很好但默认连接数只有 10。购书商城在本地开发够用但如果多人同时访问或者你开了 Mybatis 二级缓存连接池配置就要调spring: datasource: hikari: maximum-pool-size: 20 # 最大连接数按 CPU 核数 * 2 磁盘数估算 minimum-idle: 5 # 最小空闲连接避免频繁创建销毁 connection-timeout: 30000 # 获取连接超时 30 秒 idle-timeout: 600000 # 空闲连接 10 分钟后释放 max-lifetime: 1800000 # 连接最长存活 30 分钟比 MySQL wait_timeout 小逻辑说明max-lifetime必须小于 MySQL 的wait_timeout否则会拿到已被服务端关闭的连接报Communications link failure。参数说明maximum-pool-size不是越大越好20 在 4 核机器上足够设 100 反而会因线程切换拖慢响应。4.2 Mybatis 一级缓存和二级缓存的坑Mybatis 一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 内相同查询只走一次数据库。二级缓存是 Mapper 级别的需要手动开cache evictionLRU flushInterval60000 size512 readOnlytrue/逻辑说明evictionLRU最近最少使用淘汰flushInterval60 秒清一次size最多缓存 512 个对象。参数说明readOnlytrue性能好但要求对象不可变如果返回的对象会被修改要设false。坑在于二级缓存跨 SqlSession 共享如果两个 Mapper 操作同一张表一个更新后另一个缓存不会自动清必须用cache-ref或者手动flushCache。4.3 常见报错与排查清单现象原因解决Invalid bound statement (not found)Mapper XML 路径不对或方法名不匹配检查mapper-locations和 XML 里的idError creating bean with name sqlSessionFactory数据源配置错误或驱动缺失确认driver-class-name和 MySQL 驱动版本Thymeleaf 页面 500模板找不到prefix或suffix配错确认 HTML 在templates/下且后缀是.html中文乱码数据库、连接、页面编码不一致统一utf8mb4serverTimezone设对java.sql.SQLException: Access denied账号密码错或权限不足用GRANT重新授权注意%和localhost区别5. 避坑与常见问题那些让我加班到凌晨的细节5.1 坑一Thymeleaf 热更新不生效改完 HTML 没反应现象改了book/list.html刷新浏览器还是旧页面。原因spring.thymeleaf.cachetrue是默认值模板被缓存了。解决开发环境设cache: false并且在 IDEA 里开启Build project automatically同时按CtrlShiftA搜索Registry勾选compiler.automake.allow.when.app.running。这样改完 HTML 不用重启应用。5.2 坑二Mybatis 的#{}和${}混用导致 SQL 注入现象搜索框输入 OR 11能查出所有书。原因XML 里用了${keyword}拼接。解决一律用#{}只有动态表名、动态排序字段才用${}并且要在 Java 层做白名单校验。血泪经验ORDER BY ${sortField}这种写法如果sortField来自前端必须用枚举限制。5.3 坑三MySQL 时区错误导致订单时间差 8 小时现象下单后数据库里的create_time比实际时间少 8 小时。原因连接串没加serverTimezoneAsia/Shanghai或者 MySQL 全局时区是 UTC。解决连接串加时区参数同时检查 MySQL 的time_zone变量。如果用的是LocalDateTime还要确认 Jackson 的序列化时区。5.4 坑四SpringBoot 版本太高导致 Mybatis 兼容问题现象启动报NoClassDefFoundError: org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration。原因SpringBoot 3.x 要求 Mybatis Starter 也用 3.x但很多老教程用的是 2.x。解决要么降 SpringBoot 到 2.7.x要么把mybatis-spring-boot-starter升到 3.0.x。注意 SpringBoot 3 要求 Java 17别用 Java 8 硬跑。5.5 坑五购物车数量为 0 时没删记录现象用户把购物车数量减到 0记录还在下次登录又出现。原因更新逻辑只做了UPDATE cart SET quantity ?没判断 0。解决Service 层加判断quantity 0时执行DELETE或者用UPDATE ... SET quantity quantity - 1 WHERE quantity 0配合前端限制。6. 进阶技巧用 Mybatis 拦截器打印完整 SQL 与 Thymeleaf 片段复用6.1 自定义 Mybatis 拦截器把 SQL 和参数一起打出来log-impl: StdOutImpl只能打印带?的 SQL参数是分开的排查问题时对不上。我一般写一个Interceptor把最终 SQL 拼出来Intercepts({Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class SqlLogInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms (MappedStatement) invocation.getArgs()[0]; Object parameter invocation.getArgs()[1]; BoundSql boundSql ms.getBoundSql(parameter); String sql boundSql.getSql().replaceAll(\\s, ); // 压缩空白 System.out.println( SQL: sql); System.out.println( PARAM: boundSql.getParameterObject()); return invocation.proceed(); } }逻辑说明Intercepts指定拦截Executor.query方法getBoundSql拿到带参数的 SQL 对象。参数说明replaceAll(\\s, )把换行和多余空格压掉方便复制到 Navicat 里执行。注册方式是在配置类里加Bean返回SqlLogInterceptor实例。注意生产环境要关掉或者用日志框架按级别输出。6.2 Thymeleaf 的 fragment 复用别复制粘贴导航栏购书商城每个页面都有导航栏和页脚复制粘贴会导致改一处要改十处。Thymeleaf 的th:fragment和th:replace能解决!-- templates/common/header.html -- header th:fragmentheader nav a th:href{/}首页/a a th:href{/cart}购物车/a a th:href{/user/orders}我的订单/a /nav /header !-- templates/book/list.html -- div th:replace~{common/header :: header}/div逻辑说明th:fragmentheader定义片段名th:replace~{common/header :: header}在别的页面引用。参数说明~{...}是 Thymeleaf 3 的片段表达式语法旧版用th:replacecommon/header :: header也行。这样导航栏只维护一份改完所有页面生效。6.3 验证清单上线前我会跑一遍的五个检查第一把application.yml里的thymeleaf.cache改成truelog-impl删掉useSSL改成true。第二用mvn clean package打 jar 包java -jar跑一遍确认不是只有 IDEA 能启动。第三把数据库账号从 root 换成专用账号权限只给bookshop.*。第四用 Postman 或浏览器把「注册 → 登录 → 搜索 → 加购 → 下单 → 查看订单」走一遍确认没有 500。第五检查所有RequestMapping有没有漏掉权限校验尤其是后台管理接口。我自己的习惯是每次改完 Mapper XML先在 Navicat 里把 SQL 跑一遍确认结果集对了再写 Java。这样能省掉大量「以为是代码问题其实是 SQL 写错」的排查时间。购书商城这类项目表结构和 SQL 是骨架SpringBoot 和 Thymeleaf 只是皮肉骨架正了皮肉怎么长都不会太歪。希望帮到你。本文还有配套的精品资源点击获取