1. 网上超市系统的需求本质:先从业务角度拆清楚
做了这么多年Java后端,我发现很多人一拿到“网上超市系统”这类题目,第一反应就是赶紧建项目、写Controller、连数据库,结果做到一半才发现购物车逻辑乱了、库存扣减对不上、订单状态捋不顺,最后整个项目推倒重来。其实这类系统的核心难点从来不是CRUD,而是业务状态的流转设计和数据一致性的把控。
网上超市系统本质上是传统电商的简化版,核心用户是两类人:C端消费者和B端运营管理员。消费者关心的是能不能快速找到商品、加购物车、下单支付、查看订单;管理员关心的是商品上下架、库存管理、订单处理、用户管理。如果做毕业设计或面试项目,这四块业务就足够撑起整个系统的骨架了。
但这里有一个很关键的设计决定:不要一上来就做分销、秒杀、优惠券那一套复杂营销玩法。我见过太多人为了“显得项目大”硬塞一堆模块,结果自己都讲不清楚,答辩时一问三不知。网上超市系统的核心价值在于把“商品—购物车—订单—库存”这条主链路打磨顺,再扩展会员、分类、公告这些辅助功能,就完全够用且有说服力了。
立项之初,我给自己定的功能边界是这样的:
- 前台:用户注册登录、商品分类浏览与搜索、商品详情、购物车、订单结算、个人中心(订单查看/信息维护)
- 后台:管理员登录、商品管理(增删改查/上下架)、分类管理、订单管理(发货/状态更新)、用户管理、基础统计
这套边界既覆盖了电商领域的经典业务闭环,又不至于因为范围过大而失控。实测下来,从零开发到跑通全流程,四个功能模块全部落地大概是三周左右的业余时间,节奏非常舒服。
技术选型上,我用的是Spring Boot 2.x + MyBatis Plus + MySQL 8.0 + Redis(可选),前端是Thymeleaf+Bootstrap,没有做前后端分离。原因后面详说,先记住结论:这是一个用最少成本把业务闭环讲清楚的最佳组合。
2. 为什么这样搭:Spring Boot 电商项目的技术选型心得
2.1 Spring Boot 版本怎么选:2.x是当下最稳妥的答案
我看到现在还偶尔有人直接用Spring Boot 3.x做这类项目,其实不是不行,但对大多数同学来说,Spring Boot 2.7.x是更推荐的选择。原因在于:3.x是基于Jakarta EE的,很多老教程、老依赖的包名从javax改成jakarta,网上查资料时会发现大量报错解法对不上,白白浪费调试时间。
另外就是MyBatis Plus的兼容性问题。MP虽然已经支持3.x,但如果你参考的文档、视频课是2.x时代的,你会发现导入依赖后一堆API报红,分页插件配置方式也变过。为了把精力花在业务逻辑而不是环境适配上面,我个人建议用2.7.18这个版本,它是个非常成熟的稳定版,资料量也最大。
核心依赖清单大致如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <!-- Web 层 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Thymeleaf 模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- MyBatis Plus 持久层框架 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 密码加密工具 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Lombok 简化实体类 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>2.2 为什么用Thymeleaf而不是前后端分离
这是个很容易被忽视的问题。现在前端框架满天飞,Vue、React哪个不比服务端渲染“时髦”?但如果你的目标是做课程设计、毕业设计或者面试演示项目,Thymeleaf + Bootstrap的服务端渲染方案,实际上是最优解。
原因有三点。第一,这类项目PPT答辩时,面试官/老师最在意的是你能否讲清楚业务逻辑,前后端分离意味着你要维护两套工程,联调成本、跨域问题、Token鉴权,这些都会把主线搅浑。第二,Thymeleaf天然和Spring Boot集成,后端传Model数据直接在HTML里渲染,代码量少一大截,演示的时候刷新一个页面就能看到效果变化。第三,部署简单,一个jar包丢服务器上就能跑,省去Nginx配置、前端构建那些环节。
我当时也犹豫过要不要用Vue重写一遍前端,后来实测下来,用Thymeleaf五天的页面工作量,换成Vue至少要两周,而这个项目本身核心是后端业务,所以果断放弃。
2.3 MyBatis Plus带来的效率提升非常明显
早期项目我用的是原生MyBatis,每个实体类都要写一堆XML映射文件,找bug的时候还得来回切换Java和XML,效率非常低。后来切换到MyBatis Plus,单表CRUD几乎不用手写SQL,内置的Wrapper条件构造器让动态查询变得极其简洁。
举一个典型例子,后台商品列表的筛选查询:
public Page<Product> queryProductPage(int page, int size, String keyword, Integer categoryId) { Page<Product> pageParam = new Page<>(page, size); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 关键字模糊搜索(商品名称) wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword); // 分类筛选(精确匹配) wrapper.eq(categoryId != null && categoryId > 0, Product::getCategoryId, categoryId); // 按上架时间倒序排列 wrapper.orderByDesc(Product::getCreateTime); return productMapper.selectPage(pageParam, wrapper); }看到没,整个查询逻辑就这几行,没有XML、没有SQL拼接、没有烦人的if标签。关键点在于LambdaQueryWrapper里的那个布尔条件参数:StringUtils.hasText(keyword)为false时,这个条件会被自动忽略。这意味着你只需要写一套查询逻辑,就同时兼顾了无条件查询和带条件查询,这在MyBatis里至少要写5个动态SQL标签。
2.4 密码安全是这类项目最容易忽视的点
很多学生的项目里密码都是明文存数据库的,这要是拿到面试官面前,基本等于直接暴露了自己没有基本的安全意识。我用的是Spring Security自带的BCryptPasswordEncoder做加密处理。
注册时加密:
PasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(user.getPassword()); user.setPassword(encodedPassword);登录时校验:
if (encoder.matches(rawPassword, user.getPassword())) { // 密码匹配成功,跳转登录 }BCrypt的特别之处在于它是带盐的哈希算法,同一个明文密码每次生成的哈希值都不同,但校验时依然能匹配。这意味着即使数据库被拖库,攻击者也无法通过彩虹表反查密码,安全性有基本保障。
3. 数据库表设计:电商系统最重要的一张图
3.1 六张核心表搞定整个业务闭环
网上超市的表结构设计,我最终定的是六张核心业务表加一张日志表。很多人喜欢设计十几张表,但仔细一看很多表之间根本没有业务关联,纯粹是凑数量。表不在多,能够形成闭环才是关键。
我最终的建表清单如下:
t_user:用户表,存账号、密码(BCrypt加密后的)、昵称、手机号、创建时间t_category:商品分类表,父级ID用于支持二级分类t_product:商品表,存分类ID、名称、主图、价格(以分为单位存储)、库存、上下架状态、销量t_cart:购物车表,用户ID + 商品ID + 数量 + 勾选状态t_order:订单表,订单号、用户ID、总金额、收货信息、订单状态t_order_item:订单明细表,订单ID + 商品ID + 单价 + 数量 + 小计t_address:收货地址表(这个可以并入用户表,但独立出来更清晰)
这里有一个非常容易被新手忽略的设计:金额字段一律用整数(以分为单位)存储,禁止用double或float。为什么?因为浮点数在计算机底层是二进制近似表示的,0.1 + 0.2在Java里可能等于0.30000000000000004。把199.9元的商品计算9折,浮点运算就会出现精度问题。我用分存储,所有计算都是整数运算,展示时再除以100转成元,一劳永逸。
3.2 订单表的设计是重中之重
订单表是整个系统的核心,也是面试官最爱深挖的地方。我的t_order表核心字段如下:
CREATE TABLE `t_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `user_id` BIGINT NOT NULL COMMENT '用户ID', `total_amount` BIGINT NOT NULL COMMENT '订单总金额(单位:分)', `receiver_name` VARCHAR(32) NOT NULL COMMENT '收货人姓名', `receiver_phone` VARCHAR(20) NOT NULL COMMENT '收货人电话', `receiver_address` VARCHAR(255) NOT NULL COMMENT '收货地址', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款 1待发货 2待收货 3已完成 4已取消', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME DEFAULT NULL, `deliver_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';订单状态字段用TINYINT数字表示,代码里用枚举类去语义化,这是很标准的做法。状态流转严格按照电商逻辑走:待付款→待发货→待收货→已完成,同时允许待付款→已取消。这里的核心是,状态的变化一定要由后端方法控制,绝不能让前端直接传一个状态值来改。
3.3 购物车和库存的关系:这是电商事务的核心战场
购物车表本身很简单,就是记录了用户往购物车加了多少商品。真正的难点在下单那一刻,涉及三个表的并发操作:扣减商品库存、创建订单、清空购物车。这三个操作要么全成功,要么全失败,这就需要事务控制。
我在写订单提交逻辑时,用的是Spring的@Transactional注解:
@Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, List<CartItem> items, Address address) { // 1. 生成订单主记录,状态为待付款 Order order = buildOrder(userId, items, address); orderMapper.insert(order); // 2. 扣减库存:SELECT ... FOR UPDATE 锁行 for (CartItem item : items) { Product product = productMapper.selectByIdForUpdate(item.getProductId()); if (product.getStock() < item.getQuantity()) { throw new BusinessException("商品【" + product.getName() + "】库存不足"); } product.setStock(product.getStock() - item.getQuantity()); productMapper.updateById(product); } // 3. 插入订单明细 for (CartItem item : items) { OrderItem orderItem = buildOrderItem(item); orderItemMapper.insert(orderItem); } // 4. 清空购物车 cartMapper.deleteByUserId(userId); return order; }这里我需要特意强调selectByIdForUpdate这个方法,它是通过SELECT ... FOR UPDATE语句对商品行加了悲观锁。为什么要加锁?想象一个场景:商品库存只剩1件,A和B两个用户同时下单。如果不加锁,两个事务都读到库存为1,都认为库存充足,然后各自扣减为0,实际上卖出了2件,这就是超卖。
加了FOR UPDATE之后,A事务先锁住这行商品进行扣减,B事务必须等A提交或回滚后才能读这行数据,此时此刻B读到的库存已经是0了,直接提示库存不足。库存一致性由此得到保证。
3.4 订单号生成:一个容易翻车的小细节
订单号的生成方式看起来是个小问题,但实际上非常容易踩坑。有人用System.currentTimeMillis()拼接随机数,并发情况下容易出现重复。我用的方案是时间戳 + 用户ID后四位 + 随机数的组合:
public static String generateOrderNo(Long userId) { String timePart = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); String userPart = String.format("%04d", userId % 10000); int randomPart = ThreadLocalRandom.current().nextInt(1000, 9999); return timePart + userPart + randomPart; }这个方案在没有分布式锁的场景下已经足够使用,但如果要做到严格不重复,更稳妥的办法是在数据库层面加唯一索引兜底,真要是撞上了就重新生成一次。数据库的唯一约束才是最后一道防线,生成算法只是降低冲突概率的手段。
4. 核心功能实现细节:从代码层面逐一拆解
4.1 用户登录与拦截器:不写拦截器等于白做
用户登录这块,我用的是最简单的Session方案,没有引入JWT,原因还是那个——项目体量决定技术复杂度。Session方案对单体应用完全够用,代码也容易理解。
登录成功后,把用户对象丢进Session:
session.setAttribute("loginUser", user);然后写一个拦截器,拦截需要登录才能访问的路径:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object loginUser = session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("/user/login"); return false; } return true; } }注册到Web配置里的时候,要注意放行哪些路径。如果不放行静态资源,CSS、JS全被拦下来,页面会变得惨不忍睹:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/user/login", "/user/register", "/product/**", // 商品浏览不需要登录 "/category/**", "/css/**", "/js/**", "/images/**" ); } }拦截器的作用相当于给系统装了一道门禁:游客可以逛超市(看商品),但结账和查订单必须登录。这道逻辑如果漏掉,任何用户都能通过直接修改URL访问后台功能,那就出大事了。
4.2 购物车的三种实现方案对比
购物车是网上超市系统里实现方式最多样化的模块,我在调研阶段对比过三种常见方案:
- 方案一:纯Session存储。购物车数据放Session里,代码量最少,但用户一关浏览器购物车就消失,体验很差。
- 方案二:Cookie存储。购物车数据放在浏览器Cookie里,不占服务端存储,但Cookie大小限制4KB,存不了多少东西,而且用户可以随意篡改。
- 方案三:数据库存储。购物车表记录用户与商品的关系,数据持久化,换设备也能看到,缺点是每次增删改查都要和数据库交互。
我最终选了方案三,因为这是一个完整项目,数据库存储购物车会让业务闭环更完整,而且面试时能顺带聊到“如何用联合唯一索引防止重复添加购物车记录”。购物车表设计了UNIQUE KEY uk_user_product (user_id, product_id),这样同一个用户添加同一个商品,如果记录已存在,就直接更新数量而不是插入新记录:
public void addToCart(Long userId, Long productId, Integer quantity) { CartItem existing = cartMapper.selectByUserIdAndProductId(userId, productId); if (existing != null) { existing.setQuantity(existing.getQuantity() + quantity); cartMapper.updateById(existing); } else { CartItem cartItem = new CartItem(); cartItem.setUserId(userId); cartItem.setProductId(productId); cartItem.setQuantity(quantity); cartItem.setChecked(true); // 默认勾选 cartMapper.insert(cartItem); } }4.3 商品搜索:用MyBatis Plus的like实现模糊查询
网上超市的商品搜索,需求很简单,就是根据关键词匹配商品名称和描述。用LambdaQueryWrapper写非常清晰:
public List<Product> searchProducts(String keyword) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.like(Product::getName, keyword) .or() .like(Product::getDescription, keyword) .eq(Product::getStatus, 1); // 只查询上架状态 return productMapper.selectList(wrapper); }但这里有一个坑:like和or组合时,如果不加括号,SQL的优先级可能不符合预期。MyBatis Plus的or()方法默认是全局的,通常会配合and(wrapper -> wrapper.like(...).or().like(...))来把逻辑分组。这样生成的SQL语句才是WHERE (name LIKE ? OR description LIKE ?) AND status = 1。
public List<Product> searchProducts(String keyword) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.and(w -> w.like(Product::getName, keyword) .or() .like(Product::getDescription, keyword)) .eq(Product::getStatus, 1); return productMapper.selectList(wrapper); }4.4 秒杀式并发场景的简化处理:乐观锁版本号
之前说扣库存用悲观锁SELECT ... FOR UPDATE,逻辑简单且安全。但如果你想让项目多一点亮点,可以考虑把库存扣减换成乐观锁方案。思路是在商品表加一个version字段,更新时带上版本号条件:
UPDATE t_product SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{id} AND version = #{oldVersion} AND stock >= #{quantity}如果更新影响行数为0,说明版本已变化或库存不足,则重试或抛出异常。两者对比一下:
- 悲观锁:实现简单,但在高并发下锁等待会导致吞吐量下降。
- 乐观锁:无锁竞争,并发效率高,但需要处理更新失败后的重试逻辑。
对于网上超市这种级别的系统,用悲观锁就可以了,因为并发量并不高。但面试如果被问到“如何防止超卖”,你能同时说出悲观锁和乐观锁两种方案,会加不少分。
5. 后台管理模块:运营人员的工作台怎么设计
5.1 商品管理:图片上传与表单校验
后台商品管理是运营人员最常用的功能,核心是表单提交和图片上传。图片上传用Spring Boot的MultipartFile接收文件,保存到服务器本地目录,再把访问路径存到数据库。
@PostMapping("/admin/product/save") public String saveProduct(Product product, @RequestParam("file") MultipartFile file) throws IOException { if (!file.isEmpty()) { // 生成不重复的文件名:时间戳 + 原始文件名后缀 String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = System.currentTimeMillis() + suffix; // 保存到指定上传目录 String uploadDir = "D:/upload/"; File dest = new File(uploadDir + fileName); file.transferTo(dest); product.setImage("/upload/" + fileName); } productMapper.insert(product); return "redirect:/admin/product/list"; }需要额外配置一个静态资源映射,把/upload/**路径映射到本地磁盘目录:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:D:/upload/"); }这个配置漏掉的话,商品图片全部404,页面展示全挂,这是我自己踩过的坑,印象极其深刻。
5.2 订单管理:发货操作驱动的状态流转
后台订单管理最核心的操作是发货。管理员点击发货,订单状态从“待发货”变为“待收货”,同时记录发货时间:
@PostMapping("/admin/order/deliver") @ResponseBody public Result deliverOrder(@RequestParam Long orderId) { Order order = orderMapper.selectById(orderId); if (order == null) { return Result.error("订单不存在"); } if (order.getStatus() != OrderStatus.WAIT_DELIVER.getCode()) { return Result.error("当前状态不可发货"); } order.setStatus(OrderStatus.WAIT_RECEIVE.getCode()); order.setDeliverTime(new Date()); orderMapper.updateById(order); return Result.success(); }这段代码最大的亮点在于状态校验。发货前先检查当前状态是否为“待发货”,如果不是,直接拒绝操作。这个逻辑称为“状态机模式”,可以保证订单状态永远按照预定义的路径流转,不会出现从“已取消”直接跳到“待收货”这种非法操作。
5.3 简单的数据统计:分析订单和用户的整体情况
后台首页我放了一个简易的面板,展示四个核心指标:总用户数、上架商品数、总订单数、当日订单数。这些统计用SQL聚合函数就能搞定:
public DashboardVO getDashboardData() { DashboardVO vo = new DashboardVO(); vo.setTotalUsers(userMapper.selectCount(null)); vo.setTotalProducts(productMapper.selectCount( new LambdaQueryWrapper<Product>().eq(Product::getStatus, 1) )); vo.setTotalOrders(orderMapper.selectCount(null)); // 当日订单数 LocalDateTime startOfDay = LocalDateTime.now().with(LocalTime.MIN); vo.setTodayOrders(orderMapper.selectCount( new LambdaQueryWrapper<Order>().ge(Order::getCreateTime, startOfDay) )); return vo; }这里要记住,统计查询如果数据量变大,一定要在create_time字段上建索引,否则全表扫描会让查询越来越慢。我一般在建表SQL里就直接加上索引。
6. 遇到过的坑和解决方案:这份避坑指南价值不低
6.1 坑一:Thymeleaf页面加载不出CSS
这个问题初学时几乎人人都碰过。现象是页面打开只有纯文本,样式全部丢失。按我的排查经验,按以下顺序排查,90%的问题能解决:
- 浏览器控制台查看报错,确认CSS文件的HTTP状态码是404还是403。
- 如果404,检查Controller里是否有
@RequestMapping("/css")之类的路径冲突。 - 如果403,检查拦截器是否把静态资源拦截了。
- 检查Thymeleaf页面里引用CSS的
th:href="@{/css/style.css}"写法,注意@{}是必须的。 - 配置了Spring Security的项目,还要确认SecurityConfig里放行了静态资源。
6.2 坑二:金额精度不对,1.1+2.2成了3.3000000000000003
这是我当初用double类型存金额时踩过的经典坑。199.9元的订单,打个95折,算出来的金额居然是189.90500000000001。虽然位数很小,但展示给用户就是很奇怪。
解决方案就是我前面说的:金额统一以“分”为单位的整数存储。数据库层用BIGINT,Java层用Long,展示层用工具方法除以100转成字符串:
public static String formatPrice(Long priceInCents) { if (priceInCents == null) return "0.00"; return String.format("%.2f", priceInCents / 100.0); }这样整个系统里永远不会出现浮点运算,精度问题从根源上被消除。
6.3 坑三:MyBatis Plus分页不生效
我早期用MP分页时,查出来的数据总是全量,翻页根本没效果。排查后发现原因很简单:MP的分页需要通过MybatisPlusInterceptor配置一个分页插件,不配置的话selectPage只是返回全部数据。
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这是一个配置类的事,但漏掉的人非常多。分页不生效先看这个,别去纠结SQL。
6.4 坑四:事务不生效
@Transactional注解看起来很简单,但有很多细节会导致它悄悄失效。最常见的三种情况:
- 注解加在了
private方法上,Spring的事务代理是基于接口或类的代理实现的,private方法不会走代理。 - 同类内部方法调用,比如
A类方法调用A类的另一个带@Transactional的方法,此时事务不生效。因为代理对象被绕过了。 - 异常被吞掉了,事务方法内
try-catch捕了异常没抛出,Spring感知不到,事务自然不回滚。
解决方法是:被事务调用的方法放在不同类里,或者把异常抛出到事务方法外;@Transactional加在public方法上,配置rollbackFor = Exception.class,确保任何异常都触发回滚。
7. 文档与演示:源码之外的加分项
7.1 这份项目的文档应该怎么组织
很多人拿到源码不知道文档怎么总结。我的经验是:文档的价值在于让人快速理解项目,而不是把人绕晕。我一般按以下结构组织:
- 项目介绍:一句话说清楚系统干什么
- 技术栈说明:框架版本、核心依赖
- 数据库设计说明:每张表的用途,核心字段的解释,表间关系
- 模块功能介绍:前台模块和后台模块的划分,每个模块的页面路径
- 部署运行指南:从导入数据库到启动项目的每一步,包括数据库密码在哪里改
- 核心逻辑说明:订单状态流转、库存扣减逻辑、购物车实现方式
- 测试账号:管理员账号、测试用户账号
文档不求长,但求准确。每一步都必须实测,虚假的运行步骤会直接消耗读者信任。
7.2 演示演示什么最加分
项目演示时,不要上来就展示CRUD。我的习惯是围绕三条链路演示:
- 用户主链路:注册→登录→浏览商品→加购物车→下单→查看订单。
- 管理员主链路:登录后台→新增商品→处理订单→发货。
- 亮点链路:演示库存不足时的提示效果,演示未登录访问购物车时的跳转拦截。
这三条链路走完,整个系统的业务闭环已经清楚了。我再额外手动改一下数据库里的订单状态,演示管理员的发货操作对前台订单列表的实时影响,这会让“状态流转”这个概念瞬间变得具体可感。
7.3 部署上线:一台服务器
项目做完之后,我觉得只在自己电脑上跑没啥意思,就买了一台一年的轻量云服务器,把系统部署了上去,顺便练习了Linux环境下的Java项目部署流程。
部署的核心步骤很简单:
# 1. 安装JDK和MySQL yum install -y java-1.8.0-openjdk mysql-server # 2. 上传项目jar包 scp target/supermarket.jar root@服务器IP:/opt/ # 3. 初始化数据库,将sql文件导入 mysql -uroot -p < supermarket.sql # 4. 启动项目 nohup java -jar supermarket.jar > /opt/logs/supermarket.log 2>&1 & # 5. 配置安全组规则,开放8080端口半个小时的活儿,但收获极大。你能亲眼看到自己的系统在真实的公网环境下被访问,那种感觉跟本地localhost完全是两码事。部署过程中你还会遇到数据库字符集配置、云服务器安全组规则、进程后台运行管理等实际问题,这些都是面试时非常加分的谈资。
8. 写在最后:关于这类项目的一些真实体会
做完这套网上超市,我自己最大的感慨是:做项目的价值不在于用了多新的技术,而在于把一件事情的完整流程跑通,并且能清晰地说出每一步为什么这么做。
从一开始的模糊想法,到需求梳理、数据库设计、编码实现、测试、部署,每个环节都会遇到问题,而解决问题的过程,恰恰是能力提升的过程。比如我直到做这个项目,才真正理解了悲观锁和乐观锁的适用场景,理解了为什么订单状态要用数字存储而不是字符串,理解了@Transactional为什么经常“不生效”。
如果让我给你一个建议:拿到一个项目题目,先花两天时间画清楚数据库的表关系图,再动代码。数据库设计好了,后面的编码就像填空题一样简单。表关系没想清楚就急于编码,后期必然会经历大量的改表和返工。
这套系统的完整代码、SQL建表脚本和开发文档,我都整理成了一个压缩包,包含源码目录、数据库脚本、部署说明和演示截图四部分。拿到之后按文档里的步骤操作就能跑起来。如果你在部署或运行过程中卡壳了,欢迎随时来交流,我可以帮你排查环境或者引导你定位问题。代码这种东西,多跑几次就熟了。