
汉服这两年的热度不用我多说每逢传统节日街上穿汉服的人肉眼可见地多起来。很多学 Java 的同学想找一个有文化内涵、功能又完整的电商项目来当毕业设计或者面试项目汉服商城管理系统就是非常典型的一个。它表面看只是一个商城实际上把 Java Web 开发里最常用的能力全串起来了用户注册登录、商品浏览、购物车、下单结算、后台商品管理、订单状态流转、数据库设计、权限拦截等等。这篇文章我会把整个系统的设计思路、技术选型、数据库建表、核心功能实现、前端交互逻辑以及我实际开发中踩过的坑完整写出来。项目原型就是一套基于 Spring Boot MyBatis Plus MySQL Thymeleaf 的经典单体商城适合正在做毕设、准备写简历项目、或者想系统性过一遍 Java Web 全流程的朋友。就算你目前只学过 Java SE也能按着这套设计一步步读懂、跑起来、改出属于自己的版本。1. 项目定位与技术选型先想清楚再动手1.1 需求分析商城到底要做哪些事做任何项目之前第一步不是写代码而是把需求列清楚。汉服商城管理系统从名字就能拆出两个关键词商城、管理。所以它天然包含两个端口面向普通用户的前台商城以及面向管理员的后台管理端。前台核心功能我总结了六块用户注册与登录区分普通用户和管理员角色首页商品分类展示、商品列表分页、按名称搜索商品详情页展示图片、价格、库存、尺码、商品介绍购物车支持添加商品、修改数量、删除、计算总金额订单确认填写收货地址、生成订单、模拟支付个人中心查看自己的订单列表和订单状态后台管理端逻辑更偏 CRUD管理员登录统一走角色校验商品分类管理新增、修改、删除分类商品管理上传图片、维护库存、上下架订单管理查看订单、修改发货状态用户管理查看用户列表、禁用账号这个需求拆完后你会发现它就是一个标准的电商最小闭环。别小看这个闭环用户从看到商品到下单支付的全链路走通是需要认真设计状态和流程的。我见过太多项目只做了增删改查下单功能形同虚设面试官一问订单状态怎么流转就支支吾吾。这个项目里我会重点把订单流程讲细。1.2 技术栈选择为什么是 Spring Boot而不是 SSM 手写配置技术选型是每个 Java 项目都绕不开的问题。现在的实际开发环境里Spring Boot 已经是绝对主流一个明显的好处就是不用再写繁琐的 XML 配置内嵌 Tomcat 可以直接打成 jar 包运行。我的建议是除非学校强制要求 SSH/SSM 结构否则直接上 Spring Boot 3.x JDK 17简历上也更跟得上技术节奏。持久层我用的是 MyBatis Plus原因很直接单表 CRUD 不需要写 SQL自带分页插件代码量能少三分之一。对于商城这种以单表操作为主的系统MyBatis Plus 比原生 MyBatis 顺手太多。数据库用 MySQL 5.7存储引擎 InnoDB字符集 utf8mb4因为商品描述里可能包含生僻字和特殊符号。前端没有做成前后端分离而是选择 Thymeleaf 服务端渲染。这里要解释一下为什么这个系统适合展示 Java Web 完整调用链用服务端渲染的话Controller 直接返回视图Session 管理登录态非常直观不需要额外处理跨域。如果你更想写前后端分离项目可以把后端改成纯接口模式并做好跨域配置但主线还是以单体渲染为主更稳。1.3 功能模块拆解一张图理清项目结构工程结构我用标准的 Controller - Service - Mapper 三层分包额外加了 config、common、entity、vo 这几个包。具体分包如下com.hanfu.shop ├── common // 统一返回结果、异常处理、常量 ├── config // 拦截器、Web 配置 ├── controller // 控制层前台商城 后台管理 ├── entity // 数据库实体类 ├── mapper // MyBatis Plus Mapper 接口 ├── service // 业务逻辑 ├── vo // 视图对象比如购物车VO、订单VO └── HanfuShopApplication.java分层的目的只有一个让各层职责清晰。Controller 只做参数接收和结果返回Service 处理核心业务逻辑Mapper 只负责数据库交互。很多新手喜欢把业务全部堆在 Controller 里一个方法写 200 行后面维护起来非常痛苦。2. 数据库设计整个系统最核心的地基2.1 六张核心表的字段设计与用途数据库设计好坏直接决定后续开发是顺畅还是返工。我设计了六张核心表用户表、商品分类表、商品表、购物车表、订单表、订单明细表。下面把关键字段列出来大家建表时可以对照参考。用户表t_user除了基本登录字段外我加了 role 字段区分管理员和普通用户status 字段控制账号是否禁用。CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(MD5加密), nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, address varchar(255) DEFAULT NULL COMMENT 默认收货地址, role tinyint DEFAULT 0 COMMENT 角色 0-普通用户 1-管理员, status tinyint DEFAULT 0 COMMENT 状态 0-正常 1-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;商品表t_product是整个商城的信息核心。汉服商品和普通数码产品不太一样它通常有尺码、形制、颜色这些属性我采用最简单的方式把关键属性直接做成字段不引入复杂的 SKU 表保证项目上手难度适中但又能体现商品多样性。CREATE TABLE t_product ( id bigint NOT NULL AUTO_INCREMENT, category_id bigint NOT NULL COMMENT 分类ID, name varchar(100) NOT NULL COMMENT 商品名称, subtitle varchar(200) DEFAULT NULL COMMENT 副标题/卖点, main_image varchar(255) DEFAULT NULL COMMENT 主图地址, detail text COMMENT 商品详情, price decimal(10,2) NOT NULL COMMENT 价格, stock int NOT NULL DEFAULT 0 COMMENT 库存, status tinyint DEFAULT 1 COMMENT 状态 1-上架 0-下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;订单表t_order设计时要注意两个点金额字段必须用 decimal 而不是 float/double否则会出现精度问题订单状态建议用数字存储而不是直接存字符串方便扩展。我的状态设计是 0-待支付1-已支付待发货2-已发货3-已完成4-已取消。CREATE TABLE t_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, pay_amount decimal(10,2) NOT NULL COMMENT 实付金额, receiver_name varchar(50) NOT NULL COMMENT 收货人, receiver_phone varchar(20) NOT NULL COMMENT 收货电话, receiver_address varchar(255) NOT NULL COMMENT 收货地址, status tinyint DEFAULT 0 COMMENT 订单状态, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;购物车表t_cart和订单明细表t_order_item相对简单购物车用 user_id product_id 做唯一约束避免同一商品重复加入。订单明细表记录下单那一刻的商品快照包括商品名称和价格这样即使后续商品被删除或改价历史订单依然能显示当时的信息。2.2 表关系与关键设计决策这几张表的关系可以用一句话概括用户拥有多张订单一张订单包含多个订单明细一个分类下有多个商品一个商品可以被多个用户加入购物车。这里有两个设计决策值得展开说一下。第一个是金额存储所有涉及钱的地方统一用decimal(10, 2)Java 实体对应 BigDecimal。我见过有人用 double 存价格结果订单总额出现 199.999999 这种值非常尴尬。第二个是逻辑外键除了用户和订单的关联是必须的我基本不在数据库层面加物理外键约束而是通过 service 层保证数据一致性。理由也很实际电商系统并发量大物理外键会影响插入和更新性能而且商城项目本身数据量不大逻辑关联完全够用。2.3 初始化数据管理员账号和演示商品数据库设计好之后要准备一批初始化数据不然项目跑起来首页空荡荡的。我通常会插入一个管理员账号和一个普通用户账号密码统一用 MD5 加密比如明文都是 123456预计算好密文直接写进 SQL。同时插入汉服分类比如齐胸襦裙、明制汉服、宋制汉服、汉服配饰、发簪、团扇再给每个分类下配几件商品价格从几十到几百不等库存不要全部设成很大这样测试购物车和库存扣减时效果更真实。INSERT INTO t_user (username, password, nickname, role) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 管理员, 1); -- e10adc3949ba59abbe56e057f20f883e 是 123456 的 MD5 值3. 后端核心功能实现从登录到下单全链路3.1 统一返回结果与全局异常处理很多新手写后端接口时一会儿返回 Map一会儿返回 JSONObject没有统一规范。这个项目我从一开始就定义了统一的返回结果类R所有接口都返回这个结构前端拿到后根据 code 字段判断请求是否成功。Data public class RT { private Integer code; // 200 成功500 失败 private String msg; private T data; public static T RT ok(T data) { RT r new R(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } public static T RT error(String msg) { RT r new R(); r.setCode(500); r.setMsg(msg); return r; } }配合全局异常处理器业务代码里不需要到处 try-catch只需要在需要的地方抛出CustomException统一由RestControllerAdvice捕获并转换成 R 对象返回。这样 Controller 里的代码会非常干净逻辑一目了然。我甚至会把参数校验失败、商品库存不足这些业务异常全部放进全局异常体系里。3.2 用户登录注册MD5 加密与拦截器权限控制用户模块最核心的是登录态管理。我这套实现里用 Session 保存登录用户配合拦截器做权限控制。登录流程是这样用户提交用户名密码Service 层查出用户并校验密码校验通过后把用户对象放进 Session同时根据 role 字段判断跳转到商城首页还是后台首页。注册接口的注意点是一定要做重复用户名校验其次密码不能明文存储。这个项目用 MD5 做一个简单的不可逆加密虽然生产环境我更加推荐 BCrypt但考虑到演示项目需要讲解原理MD5 更方便理解。如果你想显得更专业引入spring-security-crypto的 BCryptPasswordEncoder 也只需要几行代码。拦截器这块是很多项目的薄弱环节一定不能漏。我写了一个LoginInterceptor在里面校验 Session 里是否存在登录用户如果没有就重定向到登录页。再写一个AdminInterceptor继承前者额外校验用户角色是否为管理员只拦截/admin/**路径。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(/user/login); return false; } return true; } }3.3 商品列表与分页搜索的完整实现商品展示是前台商城的主干。我使用 MyBatis Plus 的分页插件通过 Page 对象实现分页查询。这里有个细节商品列表页需要同时展示分类信息但商品表里只存了 categoryId所以返回前端时不能直接返回实体类要组装一个 ProductVO额外带 categoryName 字段。这一步很多教程会忽略直接拿实体类渲染页面这是不合理的因为实体类里的内部字段不应直接暴露给前端。Service 层代码如下public PageProductVO pageProducts(int pageNum, int pageSize, String keyword, Long categoryId) { PageProduct page new Page(pageNum, pageSize); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1); // 只查上架商品 if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword); } if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.orderByDesc(Product::getCreateTime); PageProduct result productMapper.selectPage(page, wrapper); // 将 Product 转成 ProductVO填充分类名称 ... return productVOPage; }3.4 购物车功能用户维度的数据管理购物车模块核心是用户维度四个字。所有操作都必须带上当前登录用户的 ID不能出现 A 用户看到了 B 用户购物车数据的情况。添加购物车时先查该用户是否已添加过同一商品如果已经存在就把数量累加否则新增一条记录。修改数量时要注意前端传过来的值可能不合法比如负数或者超长数字Service 层要做基本校验。删除购物车项就简单多了但务必带上 userId 和 cartId 双重条件防越权。购物车列表我实现了一个 CartVO里面不仅包含购物车记录本身还通过商品表查询出商品名称、主图、单价、小计。小计在 VO 里用price.multiply(new BigDecimal(count))计算。这个字段不需要存数据库属于典型的冗余展示字段。3.5 订单流程下单、扣库存、模拟支付订单是整个系统的重头戏我把下单逻辑拆成四步每步都是面试常考的点。第一步接收前端传来的商品 ID 列表和数量、收货人信息。第二步遍历商品检查库存是否充足不充足直接抛异常。第三步计算总金额生成订单号和订单明细记录。第四步扣减库存创建订单返回订单详情。这里最关键的是下单必须和扣库存放在同一个事务里。如果先扣库存再创建订单两个操作中间出了异常会导致库存扣了但订单没生成如果先创建订单再扣库存订单生成了但库存扣减失败会出现超卖。正确做法是加Transactional注解让整个过程保持原子性。写代码时还要注意一个逻辑点计算订单金额不能直接信任前端传过来的总价要后端重新根据数据库里商品的价格来计算。前端传的价格随时可以篡改正经系统都是只把商品 ID 和数量传给后端金额由后端算。模拟支付功能是这个项目的亮点。我设计成点击去支付按钮后前端调/order/pay/{orderNo}接口后端校验订单属于当前用户且状态为待支付然后更新状态为已支付、设置支付时间、按照支付模板渲染支付成功页面。整个环节没有对接真实支付网关但状态流转是完整的面试时讲清楚这套逻辑足以体现你理解电商核心流程。3.6 后台管理接口商品管理与订单处理后台管理接口相对直接。商品管理就是标准 CRUD但要注意图片上传的处理。本地演示环境下我建立静态资源映射把upload目录映射到 URL 路径/upload/**。图片上传接口使用 MultipartFile 接收文件校验文件大小和后缀名生成的文件名用 UUID 拼接避免重名覆盖。订单管理端最核心的操作是发货。管理员点击发货后订单状态从已支付变成已发货。这个操作同样要做好角色校验只能由管理员执行。另外我还加了一个数据统计的小功能在后台首页展示商品总数、用户总数、待发货订单数这几个数字用 count 查询就能实现但对后台页面的完整度提升非常明显。4. 前端页面与交互从静态页面到动态渲染4.1 商城首页与商品列表的页面布局前端我使用的是 Thymeleaf 模板引擎加 Bootstrap 5 加少量原生 JavaScript。为什么不用 Vue因为服务端渲染场景下Thymeleaf 直接把数据渲染到 HTML 上刷新页面就能看到最新数据流程最简单适合教学和理解后端渲染机制。首页布局主要包含导航栏、分类菜单、推荐商品区域我应用的是一个横向排列的商品卡片区。商品卡片上显示主图、名称、价格和加入购物车按钮。列表页做了分类侧边栏、搜索框和分页。Thymeleaf 里遍历商品的写法如下div classrow th:eachproduct : ${page.records} div classcol-md-3 div classcard img th:src${product.mainImage} classcard-img-top alt商品图 div classcard-body h5 classcard-title th:text${product.name}商品名称/h5 p classcard-text text-danger th:text¥ ${product.price}价格/p a th:href{/product/detail/{id}(id${product.id})} classbtn btn-primary查看详情/a button classbtn btn-outline-primary add-cart th:attrdata-id${product.id}加入购物车/button /div /div /div /div4.2 商品详情页与购物车交互细节详情页布局通常是左边大图右边商品信息区下面是商品详情文字和图片。商品信息区要突出价格、库存状态、尺码选择。我这里没有做复杂的 SKU 联动而是提供尺码下拉框选完后点击加入购物车。购物车页面我用一张表格展示每行一个商品包含图片、名称、单价、数量输入框、小计、删除按钮。这里要重点处理数量变化时的小计实时更新以及底部总价联动。我写了一个 jQuery 方法监听数量输入框的 change 事件通过>boolean success productMapper.reduceStock(productId, quantity);对应的 SQL 是UPDATE t_product SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}利用数据库行锁保证同一时刻只有一个请求能成功扣减。如果你的项目里用了 Redis还可以用 Lua 脚本做更精细的库存预扣减但对于这个商城系统SQL 条件更新已经足够了而且更好理解。5.4 项目扩展方向如何把它变成简历上的亮点基础版本做完之后我强烈建议你至少做一到两个扩展点这会让项目在简历和面试中更有话题性。最容易扩展的方向是下面几个。第一引入 Redis 做首页轮播图或者热门商品的缓存减少数据库压力。第二给登录模块接入 JWT改成真正的前后端分离接口模式。第三文件上传从本地目录切换到 OSS 对象存储这能体现你对生产环境部署的了解。第四订单支付对接真实的支付宝沙箱环境支付回调解验签的流程写出来绝对是面试加分项。我个人操作时的体会是这个项目最大的价值不是代码量有多少而是它覆盖了从需求分析、数据库设计、后端接口、前端页面到部署上线的完整链路。你把每个环节的设计原因讲清楚比单纯说我用了 Spring Boot 和 MyBatis Plus有说服力得多。最后再分享一个小技巧做这类管理系统时编码之前先把所有接口路径列出来比如/product/list、/cart/add、/order/submit然后统一在 Controller 中规划好。这样开发过程中就不会出现接口路径混乱、职责不清的问题。我自己做这个项目时最深的体会就是提前把数据库表和接口清单定好后面写代码的速度会快得非常明显。希望这篇文章能帮你少走弯路做完一个真正能说清、能演示、能反复打磨的 Java 项目。