做这套基于Web的电子产品销售系统,算是我这几年带项目过程中反复打磨出来的一个典型作品。很多同学第一次接触完整Web项目,往往卡在“代码能跑但不知道整个系统怎么串起来”这个阶段,而毕设或者课设又要求你拿出一个“结构完整、功能闭环、能演示、能答辩”的东西。这套系统正好就是冲着这个需求去的:前端用户能注册登录、浏览商品、加购物车、下单付款,后台管理员能管商品、管订单、管用户,整套流程走下来,才算是一个真正意义上“可用”的Web系统,而不是一堆零散的页面和接口。
这篇文章我尽量把从需求分析、技术选型、数据库设计到核心代码实现、常见坑位排查的完整过程写清楚,不管是拿去做毕业设计参考,还是想自己动手写一个类似项目练手,按着这条线走,都能省掉不少碰壁的时间。
1. 项目整体设计与需求拆解
1.1 电子产品销售系统的核心业务闭环
先别急着写代码,把这个系统当成一个真实的线上商城来梳理需求。电子产品销售,本质上是“人-货-单”三个核心要素的流转:用户在前台浏览商品、把心仪的商品加入购物车、提交订单并完成支付;后台收到订单后对订单进行发货处理,同时管理商品的上架、下架和库存;整个过程还会涉及用户信息、商品分类、订单状态等数据的持久化存储。
从功能模块拆解,前台用户端至少要覆盖以下几个场景:
- 用户注册与登录,以及登录状态下的个人信息维护
- 商品列表展示,支持分类筛选、关键词搜索、价格区间筛选和分页浏览
- 商品详情页,展示图片、价格、库存、规格描述等信息
- 购物车管理,支持加入、删除、修改数量、勾选结算
- 订单提交,填写收货地址、生成订单、模拟支付并生成订单详情
- 个人中心查看订单列表、订单状态、取消订单等操作
后台管理端的核心则是:
- 管理员登录与权限控制
- 商品管理,包含商品的增删改查、上下架、库存修改
- 订单管理,查看用户订单、修改订单状态(发货、完成、取消)
- 用户管理,查看注册用户列表、启用或禁用账号
- 可选的数据统计页面,比如商品销量排行、订单交易额曲线、分类占比
这套模块划分在很多成熟的电商系统里都能看到影子,只不过毕设场景下我们不需要做到微服务、分布式、高并发那些企业级复杂度,而是把一个单体Web应用做扎实,把CRUD、业务逻辑、事务控制、权限校验这些基本功练到位。
1.2 为什么选B/S结构和MVC模式
这类销售系统采用B/S(Browser/Server)架构可以说是不二选择。用户不需要安装任何客户端,有浏览器就能访问系统,部署也只需要一台服务器跑Web应用和数据库,符合实际使用场景。从技术实现的角度,B/S架构天然把“展示层”和“业务层”分离,前端页面负责交互和可视化,后端服务负责业务处理和数据处理,开发、调试、部署的边界都很清晰。
MVC模式则是贯穿整个Web后端开发的核心思想。以Spring Boot实现为例:Model层对应实体类和数据库表结构,View层对应前端页面(Thymeleaf模板或Vue页面),Controller层接收HTTP请求并调用Service层处理业务逻辑,Service层再通过Mapper/DAO层完成数据库操作。这样分层之后,每一层的职责单一、相互解耦,比如要改数据库查询逻辑,只动Mapper层,不影响页面展示;要调整前端页面,也不动后端接口。
很多同学在做这个项目的时候,容易犯一个毛病:把所有逻辑全堆在Controller里面,一个方法走天下。这种写法对付几百行代码的小Demo还行,一旦业务多了,比如订单、购物车、商品、用户这些模块全都堆在一起,改一个地方牵连一片,代码根本没法维护。所以从一开始就按“Controller -> Service -> Mapper -> Database”的层次来组织代码,哪怕前期看起来多写了一些类和接口,后期开发和答辩演示都会舒服得多。
1.3 功能模块划分与角色权限模型
在这套系统里,角色权限模型是必须提前想清楚的环节。只需要两种角色:普通用户和管理员。用户登录后只能操作自己的购物车、自己的订单,修改的是自己的个人信息;管理员则拥有商品、订单、用户的全局管理权限。
权限控制的落地方式很简单:登录成功后把用户对象和角色标识存入Session,再写一个拦截器(Interceptor)统一拦截需要权限的URL前缀。前端用户相关接口以/user/开头,后台管理接口以/admin/开头,拦截器直接对请求路径做匹配:
- 未登录请求/admin/**,直接跳转到管理员登录页
- 未登录请求/user/、/cart/、/order/**,跳转到用户登录页
- 普通用户访问/admin/**,拦截并提示无权限
这种方式虽然简单,但已经完全满足这个量级的需求。用责任链式的Interceptor,比在每个Controller方法里手写判断要优雅得多,也更贴近企业开发里Spring Security做权限过滤的思路。
2. 开发环境与核心技术选型
2.1 技术栈选型:Java为主流方案的底层逻辑
网上这类“电子产品销售系统”的源码,常用的技术栈其实就那么几类:Java(Spring Boot + MyBatis + MySQL)、PHP(ThinkPHP或原生)、Python(Django/Flask)、ASP.NET等。如果是自己从零开始做,我通常建议优先选Java技术栈,原因很直接:
第一,Java生态成熟,Spring Boot已经大幅降低了Web开发的入门门槛,一个注解就能启动内嵌Tomcat,Maven管理依赖也很方便。第二,岗位需求大,企业级Web开发中Java的占比仍然非常高,做完这个项目对实习和就业都有帮助。第三,资料多、排错容易,遇到问题几乎都能在搜索引擎找到对应场景的解决方案。
这套系统在实现上选择了Spring Boot + MyBatis-Plus + MySQL + Thymeleaf的组合。MyBatis-Plus在原生MyBatis基础上提供了BaseMapper和IService,常见的单表增删改查直接继承就能用,连XML都不用写,能省出一大截代码量,特别适合毕设和课设这类需要快速出效果、但业务复杂度并不高的场景。Thymeleaf则是服务端渲染模板,通过ModelAndView把后端查询到的数据绑定到HTML页面上,比前后端完全分离的方案在部署和演示时更省心。
2.2 Spring Boot项目基础环境的搭建步骤
从头把环境搭起来,按照下面这个顺序走基本不会卡壳。
第一步是安装JDK。这套系统用的Spring Boot 2.7.x版本对应JDK 8即可,如果你新装的电脑直接上JDK 11甚至17也行,但注意Spring Boot的版本也要对应调整到2.7.16以上或3.x。环境变量JAVA_HOME和PATH配好之后,命令行里输入java -version能正常输出版本号就算JVM这块过关了。
第二步是装MySQL。推荐直接用MySQL 8.0,安装时注意字符集选择utf8mb4,这个字符集对中文支持完整,也支持存储表情符号。安装完成后,用Navicat或者命令行创建一个数据库,建议库名就叫electronic_mall。创建时最好显式指定字符集:
CREATE DATABASE electronic_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第三步是创建Spring Boot项目。用IDEA的Spring Initializr,Group填com.example、Artifact填electronic-mall,依赖勾选Spring Web、Thymeleaf、MySQL Driver、MyBatis-Plus Framework,如果不用MyBatis-Plus也可以换成Spring Data JPA,看个人习惯。等待Maven把依赖下载完,项目骨架就出来了。
第四步是配置IDE。IDEA里建议安装Lombok插件,实体类里用@Data注解就能自动生成getter/setter/toString,少写大量重复代码。File -> Settings -> Plugins里搜Lombok直接安装,注意要让项目的注解处理器处于开启状态。
2.3 核心依赖与配置文件详解
pom.xml是Maven项目的灵魂,核心依赖集中在Spring Boot的起步依赖和MyBatis-Plus的扩展包上。需要在pom里额外加入MyBatis-Plus的依赖(不同版本对应的starter略有差异,建议直接用官网最新的3.5.x):
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>配置文件application.yml里,有几个细节值得注意。数据库连接串一定要带上useUnicode=true&characterEncoding=utf-8,否则插入中文会出现乱码;MySQL 8.0以上要加serverTimezone=Asia/Shanghai,否则驱动会报时区错误。MyBatis-Plus的配置可以开启驼峰映射(默认开启)、逻辑删除和分页插件,分页插件需要注册一个MybatisPlusInterceptor的Bean。
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/electronic_mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里把cache: false关掉是为了开发阶段改完页面刷新就能看到效果,不用重启应用。log-impl配成StdOutImpl能直接在控制台打印SQL,方便定位问题。这些配置看起来琐碎,每一条踩过坑的人都知道值不值。
3. 数据库设计与核心表结构详解
3.1 实体关系梳理与E-R模型分析
数据库设计是这类系统的地基,表结构不好,后面写代码全是补丁。核心实体包括用户(User)、商品分类(Category)、商品(Product)、购物车(Cart)、订单(Order)、订单明细(OrderItem),其中订单和订单明细是典型的一对多关系,一个订单包含多个商品明细,这种设计是为了让同一订单里的每个商品都有一个独立的记录,方便统计销量、追踪单个商品的售后情况。
从外键关系的角度来看:
- 商品属于一个分类,Category和Product是一对多
- 购物车记录属于某个用户,同时关联某个商品,User和Cart是一对多
- 订单属于某个用户,User和Order是一对多
- 订单明细属于某个订单,同时关联商品,Order和OrderItem是一对多
有了这个实体关系,写SQL和创建实体类的时候思路会非常清晰。
3.2 五张核心表的字段设计与建表语句
数据库建表时需要注意几个通用规范:主键用自增的INT或BIGINT;金额字段一定不要用DOUBLE或FLOAT,会出现精度丢失问题,要用DECIMAL(10,2);时间字段用DATETIME类型默认值设为CURRENT_TIMESTAMP;所有表都加上create_time和update_time字段用于记录数据时间线。
用户表:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录用户名', password VARCHAR(100) NOT NULL COMMENT '登录密码(BCrypt加密)', nickname VARCHAR(50) COMMENT '昵称', phone VARCHAR(20) COMMENT '手机号', email VARCHAR(100) COMMENT '邮箱', avatar VARCHAR(255) COMMENT '头像URL', role TINYINT DEFAULT 0 COMMENT '角色: 0-用户 1-管理员', status TINYINT DEFAULT 1 COMMENT '状态: 1-正常 0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );商品分类表:
CREATE TABLE product_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名称', sort INT DEFAULT 0 COMMENT '排序号,越小越靠前', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );商品表:
CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT '所属分类ID', name VARCHAR(100) NOT NULL COMMENT '商品名称', description TEXT COMMENT '商品详细描述', price DECIMAL(10,2) NOT NULL COMMENT '商品价格', original_price DECIMAL(10,2) COMMENT '划线原价', stock INT NOT NULL DEFAULT 0 COMMENT '库存数量', sales INT NOT NULL DEFAULT 0 COMMENT '销量', cover VARCHAR(255) COMMENT '商品封面图', images TEXT COMMENT '轮播图,多个以逗号分隔', status TINYINT DEFAULT 1 COMMENT '状态: 1-上架 0-下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );购物车表:
CREATE TABLE cart_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', product_id BIGINT NOT NULL COMMENT '商品ID', quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量', checked TINYINT DEFAULT 1 COMMENT '是否选中: 1-选中 0-未选中', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );订单表和订单明细表:
CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单编号', user_id BIGINT NOT NULL COMMENT '下单用户ID', total_price DECIMAL(10,2) NOT NULL COMMENT '订单总金额', status TINYINT DEFAULT 0 COMMENT '订单状态: 0-待付款 1-待发货 2-待收货 3-已完成 4-已取消', receiver_name VARCHAR(50) NOT NULL COMMENT '收货人', receiver_phone VARCHAR(20) NOT NULL COMMENT '收货电话', receiver_address VARCHAR(255) NOT NULL COMMENT '收货地址', pay_time DATETIME COMMENT '支付时间', deliver_time DATETIME COMMENT '发货时间', finish_time DATETIME COMMENT '完成时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT '订单ID', product_id BIGINT NOT NULL COMMENT '商品ID', product_name VARCHAR(100) COMMENT '商品快照名称', product_price DECIMAL(10,2) COMMENT '商品快照单价', quantity INT NOT NULL COMMENT '购买数量', total_price DECIMAL(10,2) COMMENT '小计金额' );这里订单明细表存储了product_name和product_price的“快照”,是一个电商系统里非常关键的经验:订单生成后,即使管理员修改了商品名称、价格,历史订单里展示的商品信息也不能跟着变,否则对账和售后都说不清楚。把商品关键字段冗余到明细表里,看起来“反范式”,实际场景中却是刚需。
3.3 索引设计与性能考量
对于一个课堂级或毕设级的项目,数据量一般不大,索引不需要过度设计,但几个高频查询场景还是应该覆盖到位。比如购物车表按user_id查,订单表按user_id查,订单明细表按order_id查,这些都是高频操作,不加索引时会全表扫描。推荐在建表时给这些字段加上普通索引:
ALTER TABLE cart_item ADD INDEX idx_user_id (user_id); ALTER TABLE orders ADD INDEX idx_user_id (user_id); ALTER TABLE order_item ADD INDEX idx_order_id (order_id); ALTER TABLE product ADD INDEX idx_category_id (category_id);商品搜索场景如果要支持关键词模糊查询(LIKE '%keyword%'),用MySQL的普通索引是没法优化的,这里也是小项目的一个合理取舍:在数据量达到百万级别之前,直接全表扫描的代价可以接受,不需要引入Elasticsearch这类搜索引擎,但如果有心,可以在商品表加一个全文索引体验一下MySQL 8的全文检索,扩展起来也有话可说。
4. 核心功能模块实现与关键代码拆解
4.1 用户注册登录模块:安全机制与会话管理
注册登录是整个系统的门面,凡是涉及密码的环节,我的原则是“密码绝不能明文入库”。用Spring Security自带的BCryptPasswordEncoder,每次加密时随机生成盐,所以同一个密码每次加密结果都不同,即使数据库泄露,也无法直接反推出明文密码。注册过程就是把用户名唯一性校验和密码加密合在一起处理。
登录验证逻辑上,前端提交用户名和密码之后,后端按用户名查库,比对BCrypt密文。验证通过就把用户对象存进Session,并在这个用户对象的脱敏版本里设置角色标识,供后续的权限拦截器判断。
登录接口的核心代码可以这样处理:
public LoginResult login(String username, String rawPassword, HttpSession session) { SysUser user = userMapper.selectOne( new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, username) ); if (user == null) { return LoginResult.error("用户名不存在"); } if (user.getStatus() == 0) { return LoginResult.error("账号已被禁用,请联系管理员"); } if (!passwordEncoder.matches(rawPassword, user.getPassword())) { return LoginResult.error("密码错误"); } // 登录成功,保存会话 SysUserVO vo = new SysUserVO(); BeanUtils.copyProperties(user, vo); vo.setPassword(null); session.setAttribute(Constants.USER_SESSION_KEY, vo); return LoginResult.success(vo); }这中间有一个细节:User里如果存了密码字段,查询出来后即使要放进Session,也一定要把密码置空或单独建VO对象。很多同学图省事直接把实体类放进Session,答辩时被追问一句“密码明文存在哪里?”就容易露怯。
4.2 商品展示与搜索筛选:多条件查询的优雅实现
商品列表页是用户打开系统的第一印象,功能上要支持分类导航、关键词搜索、价格区间,还要有分页导航。用MyBatis-Plus的LambdaQueryWrapper可以非常优雅地拼接动态查询条件,不需要手写一堆if拼接SQL:
public IPage<ProductVO> queryProducts(int page, int size, ProductQueryDTO query) { Page<Product> pageParam = new Page<>(page, size); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 状态必须为上架的商品 wrapper.eq(Product::getStatus, 1); // 分类筛选 if (query.getCategoryId() != null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } // 关键词搜索 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Product::getName, query.getKeyword()); } // 价格区间筛选 if (query.getMinPrice() != null) { wrapper.ge(Product::getPrice, query.getMinPrice()); } if (query.getMaxPrice() != null) { wrapper.le(Product::getPrice, query.getMaxPrice()); } // 排序:按照销量倒序(热门优先) wrapper.orderByDesc(Product::getSales); return productMapper.selectPage(pageParam, wrapper); }这段代码看着简单,实际跑起来有几个点容易出错。第一是分页插件必须注册,否则selectPage查出来的total始终是0。第二是like查询对关键字里的%和_这两个通配符不会自动转义,如果用户搜索的内容里恰好包含了这两个字符,会把所有数据都匹配出来。这一步我习惯自己写一个escapeLike方法,把%和_先替换成\%、\_,再加类似ESCAPE '\\'的SQL片段,不过项目量级不大时也先不管,心里有数就行。
4.3 购物车模块:用户维度的数据读写
购物车模块的业务逻辑不复杂,核心是“一个用户拥有的购物车记录”。添加购车时先查数据库里是否已经存在该用户对这件商品的购物车记录,如果已经存在就直接累加数量,否则新增一条记录。这个逻辑防止了一个商品在购物车里出现多条重复记录的问题。
public void addToCart(Long userId, Long productId, Integer quantity) { CartItem existing = cartMapper.selectOne( new LambdaQueryWrapper<CartItem>() .eq(CartItem::getUserId, userId) .eq(CartItem::getProductId, productId) ); if (existing != null) { existing.setQuantity(existing.getQuantity() + quantity); cartMapper.updateById(existing); } else { CartItem item = new CartItem(); item.setUserId(userId); item.setProductId(productId); item.setQuantity(quantity); item.setChecked(1); cartMapper.insert(item); } }还有一个小细节是,加入购物车时应该校验商品状态和库存。如果商品已下架,或者用户试图添加的数量已经超过库存,直接弹出提示,而不是等到下单结算时才报错。商品的库存是动态变化的,购物车阶段只是一个“意向清单”,所以这里不需要锁库存,结算下单时再做严格的库存校验。
另外,购物车的实时总价计算,我建议不用数据库算,而是在后端查出购物车列表后,循环计算每个商品的单价乘数量,再汇总。单价需要实时查商品表获取最新价格,不能依赖购物车表里的价格字段。这跟订单里的价格快照逻辑是两回事:购物车是动态的,用最新价格;订单一旦生成就是历史单据,用快照价格。
4.4 订单模块:事务与并发控制
下单是整套系统里最核心、也最容易出问题的地方。一个订单的完整流程是:从前端提交购物车中勾选的商品ID列表和收货地址,后端根据商品ID查出最新价格,计算订单总价,生成订单主记录,生成订单明细记录,扣减商品库存,清空购物车中对应的记录,最后返回订单编号。
这里最关键的三个点是:
第一是事务。下单涉及多个表的写操作,比如插入订单表、插入明细表、更新商品库存、删除购物车记录,任何一个环节失败,前面的写入都算白写,必须回滚。Spring里只要在Service方法上加@Transactional(rollbackFor = Exception.class)即可,注意要加在实现类的方法上而不是接口方法上。
第二是库存的并发控制。两个人同时下单购买同一个商品,如果都先查库存再扣减,有可能会发生超卖。最简单的办法是在扣减库存的SQL上加条件,只有当前库存大于等于购买数量时才更新:
boolean success = productMapper.reduceStock(productId, quantity) > 0;对应的reduceStockSQL是一条带条件的UPDATE:
UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{productId} AND stock >= #{quantity}用这种方式,在数据库层面保证原子性,完全够用。这也是我特别想提醒的一点:不要先select查库存,在Java里判断够不够,再执行update,这个判断和更新之间是有时间窗口的,并发一高就会超卖。把判断直接写进UPDATE的WHERE条件里,由数据库来保证原子性,才是正确姿势。
第三是订单编号的生成。不能直接用数据库自增ID作为用户看到的订单号,太容易猜测,也别用UUID.randomUUID()这种32位随机串,太丑且无序。我习惯生成一个形如20240101123000001的可读编号,规则是时间戳 + 随机数或用户ID,保证在同一个数据库里不重复:
String orderNo = "OD" + System.currentTimeMillis() + String.format("%03d", new Random().nextInt(1000));4.5 后台管理员模块:商品管理与订单流转
后台管理端的核心是商品CRUD和订单状态流转。
商品管理部分,需要支持新增、编辑、下架、删除。这里有一个细节:商品表用了status字段区分上架/下架,逻辑删除字段deleted由MyBatis-Plus统一处理,所以删除操作调用deleteById实际执行的是UPDATE把deleted置为1,而不是物理DELETE。这样设计是防止管理员误删商品后连订单明细里的商品信息都受影响,也方便后期数据恢复。
订单管理部分,核心是状态机流转。订单状态我用整数表达:0待付款、1待发货、2待收货、3已完成、4已取消。管理员从待发货状态改成待收货状态时,本质是调用了deliverOrder方法,更新状态的同时要更新deliver_time。为了防止别人通过直接调接口的方式“越权改单”,需要校验状态变更的前置条件,比如待付款订单不能直接变成已完成,必须走完“支付 -> 发货 -> 收货”这条链路。这块可以用一个简单的状态流转校验Map来管理,目前项目里直接用if判断也可行,重要的是状态不能跳变。
5. 前端页面设计与交互实现
5.1 页面结构与公共布局的搭建
前端页面我采用Thymeleaf服务端渲染,页面文件放在resources/templates目录下。整体布局上,前台页面分为首页、商品列表页、商品详情页、购物车页、订单确认页、订单列表页、个人中心,后台管理是单独的admin.html框架页加多个局部页面。
Thymeleaf的公共布局可以用th:fragment抽取公共片段,比如顶部导航栏topbar :: header、底部版权信息footer :: info。这样每个页面只需要通过th:replace引入公共片段,页面的header和footer部分就不会重复开发,后期要改联系方式或者新增导航链接,也只改一个文件。
首页的整体结构按电商惯例:顶部导航 -> 搜索栏 -> 商品分类导航 -> 轮播图 -> 热门商品推荐 -> 新品上架。这些数据都从后端注入,通过th:each遍历渲染。商品卡片上要显示图片、标题、价格、销量,按钮是“加入购物车”和“查看详情”。
5.2 前后端交互:Ajax与页面跳转的分工
虽然是服务端渲染,但不代表所有操作都是整页刷新。购物车添加、删除、数量修改、结算金额计算这些高频微操作,用了jQuery的Ajax,返回JSON数据,前端局部更新;而页面跳转类操作如点击商品进入详情页、提交订单成功跳转订单页,用传统的window.location.href。
添加购物车这个交互我实测下来最舒服的方案是弹一个小提示条,而不是跳转到购物车页,也不弹原生alert打断用户。具体做法是Ajax请求成功后,在页面右上角出现一个浮层提示“加入成功”,同时导航栏的购物车数量角标高亮一下:
$.ajax({ url: '/cart/add', method: 'POST', data: { productId: id, quantity: 1 }, success: function(res) { if (res.code === 200) { showToast('已加入购物车'); refreshCartBadge(); } else { showToast(res.msg); } } });购物车里数量加减和结算金额的联动,用事件委托监听数量框的change事件,每次变化后重新计算当前行的金额和总的合计金额。注意一点:购物车页面的单价由后端返回,尽量减少前端自己计算后传给后端的环节,金额计算以服务端下单接口的最终结果为准。
5.3 管理后台页面设计
后台管理页面走的是经典后台框架风格:左侧菜单栏、顶部固定导航、右侧内容区。菜单包括仪表盘、商品管理、分类管理、订单管理、用户管理。这些页面统一用一套表格组件来展示列表数据,每一行右侧提供操作按钮,比如编辑、删除、上下架,顶部提供搜索框和“新增商品”按钮。
这里要提一个开发效率的窍门:后台页面尽量不用前端MVVM框架(如Vue),直接用Thymeleaf从后端注入数据渲染表格。原因很简单,后台页面是给管理员用的,交互相对简单,不需要复杂的响应式状态管理;服务端渲染让页面刷新后数据就是最新的,不用额外处理数据同步。如果后续想升级体验,再把后台管理抽成Vue + Element Admin风格的前后端分离项目也不迟。
6. 安全防护与系统边界处理
6.1 口令安全与SQL注入的防护
密码加密使用BCryptPasswordEncoder已经提到了。SQL注入方面,MyBatis-Plus的QueryWrapper在大多数场景下都是预编译的PreparedStatement,天然防注入。真正需要注意的坑是手写SQL时拼接条件,比如在XML里写${orderBy}这种直接拼接的用法,一定要限制传入值的范围,不能让用户随便传字段名。另外,MyBatis的#{param}是安全的,${param}是不安全的,能不用${}就不用。
还有一个容易被忽略的点是XSS攻击。用户通过商品评论、个人签名等位置输入的内容,如果直接前后端不加处理地渲染,可能被嵌入恶意脚本。Thymeleaf默认会对文本输出做HTML转义,所以服务端渲染的场景相对安全。但如果是Ajax拿JSON数据后用innerHTML写入页面,就要小心了,最好用textContent赋值,或引入前端的XSS过滤工具。
6.2 接口防刷与敏感操作校验
这套系统做了几个必要但不过度的接口安全措施。一是后台接口带一个简单的操作日志切面,管理员的增删改操作会异步记录到操作日志表,方便回溯误操作。二是下单接口校验用户Session中携带的地址信息和提交信息的一致性,避免用户通过篡改请求参数修改别人的订单。三是所有金额相关参数,前端提交过来的只当作参考,实际以下单接口根据商品ID重新查库计算出的金额为准。
防接口刷量方面,我给提交订单的接口加了一个简易的Token校验:下单页加载时后端生成一个一次性Token存到Session,提交订单时前端携带这个Token,后端消费后立即失效。这样虽然挡不住重放攻击的全部变种,但能挡住最基础的“F5刷新重复下单”场景。再配合下单时校验库存,整体安全级别已经超过毕设项目普遍水平。
7. 部署运行与常见问题排查实录
7.1 本地开发和打包部署的完整步骤
本地开发和打包部署是两套不同的运行方式。开发时直接在IDEA里运行主类,内置Tomcat监听8080端口,数据库连接指向本机的MySQL。要给别人演示或者提交到服务器上运行,就需要打成可执行Jar包:
mvn clean package -DskipTests执行完成后target目录下会生成一个electronic-mall-0.0.1-SNAPSHOT.jar,在服务器上运行:
java -jar electronic-mall-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod生产环境的数据库地址、账号密码建议通过application-prod.yml单独配置,不要和开发环境混在一起。如果端口8080被占用,可以用--server.port=8081来覆盖。
部署环节常见的坑是:本地连不上远程MySQL、服务器防火墙没放行端口、Java版本和Spring Boot不兼容导致的启动报错。检查顺序基本是:先看MySQL能不能从服务器本机连上,再看端口能不能通,最后看项目日志的具体异常栈。
7.2 常见问题快速排查清单
我把这个项目开发和调试过程中最容易踩的坑整理成了一张速查表,按问题现象、可能原因、解决办法对应列出:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 启动后访问页面报404 | Controller没扫描到,Controller包不在主类同级或子包下 | 检查@ComponentScan扫描范围,把主类放在根包 |
| 数据库中文乱码 | 数据库字符集不是utf8mb4或连接串没有编码参数 | 检查建库字符集和JDBC URL的characterEncoding |
| 查询列表页总数恒为0 | MyBatis-Plus分页插件未注册 | 注册MybatisPlusInterceptor并添加PaginationInnerInterceptor |
| 下单后库存没有减少 | 服务方法没有加@Transactional,或扣库存SQL条件不生效 | 检查事务注解和扣库存SQL的WHERE条件 |
| 登录成功后刷新页面失效 | Session默认过期时间过短或浏览器禁用了Cookie | 配置server.servlet.session.timeout或在代码中设置Session超时 |
| 部分Ajax请求返回403 | Spring Security或拦截器拦截了该请求 | 检查拦截路径配置,对Controller方法放行或补充权限 |
| 部署后图片不显示 | 上传路径和访问路径没对应 | 配置静态资源映射映射到实际磁盘路径 |
| 前端调接口出现跨域 | 端口不同或前后端分离部署 | 配置CorsFilter或设置后端允许跨域 |
7.3 答辩演示时容易翻车的三个场景
这个项目大概率最终要面向答辩或者评审演示,我特意把自己经历过的几个“演示名场面”写一下,给大家提个醒。
第一个场景是现场演示登录但数据库没启动。建议在答辩正式开始前,把数据库服务设为开机自启,并在项目里配置ConnectionTimeout的重试逻辑,不要一启动报错就让全场干等着。
第二个场景是演示时输入了一条超长商品名称,导致页面布局错乱。这个纯属测试数据不规范,可以在添加商品时给名称长度加一个前端和后端双重校验,比如要求最多50个字符,遇到超出就在Controller返回错误提示。
第三个场景是演示下单时网络卡顿,用户连续点了两次提交按钮,生成了两笔重复订单。这个就是前面提到的Token防重机制的用处了,一次会话一次下单令牌,能很大概率避免这种尴尬场面。如果没做Token,至少在前端加一个“提交后按钮置灰”的处理,减少重复提交的概率。
8. 文档撰写与项目提升建议
8.1 毕业设计文档的结构安排
这类“文档+源码”的项目,文档部分通常是论文或设计说明书,结构上一般包含:摘要、绪论(背景与意义、国内外现状)、需求分析(可行性分析、功能性需求、非功能性需求)、系统设计(总体架构、模块设计、数据库设计)、系统实现(核心功能界截图、关键代码分析)、系统测试(测试用例、测试结果)、总结与展望。
写文档时有几个实用技巧:第一,截图要统一风格,浏览器建议用Chrome,分辨率统一收缩到合适的宽度再截。第二,每个核心模块实现章节配合一到两个核心代码片段,并写清楚这段代码解决什么问题,不要大段粘贴源码。第三,测试章节要写真实的测试过程,比如用JUnit写几个Service层的单元测试,比只贴黑盒测试结果更有说服力。第四,页面截图里要把导航栏和关键操作按钮露全,方便评审老师一眼看出这是完整系统。
8.2 进阶功能扩展和性能优化方向
如果做完这些基础功能还有余力,有几个性价比极高的扩展方向:
第一个方向是引入Spring Security做更完整的认证授权。现在用Session + 拦截器能跑通,但Spring Security在密码加密、登录成功/失败处理器、方法级别权限控制上有一套更完善的体系,引入后代码结构会偏向企业级。
第二个方向是增加统一异常处理和参数校验。用@RestControllerAdvice把异常统一包装成JSON响应,用@Validated注解对入参进行校验,能大幅提升代码健壮性,也是面试时能拿出来讲的技术点。
第三个方向是数据统计可视化。在后台仪表盘做一个“近7日订单量柱状图”和“商品分类销售占比饼图”,用ECharts在前端展示,后端接口统计数据。这个功能既是亮点,又不需要引入重型框架,很适合在毕设中展示。
另外,如果想把数据库性能这块做得好看一点,可以把商品表加上ALTER TABLE product ADD FULLTEXT INDEX ft_product_name (name),全文索引后用MATCH(name) AGAINST('手机')做搜索,和LIKE '%手机%'做性能对比分析,这也能作为一个小的技术闪光点放进文档。
我从实际带项目的经验来看,这套系统的价值并不仅限于“交差”。把它吃透,从需求分析、库表设计、接口实现到部署发布走完一遍,Web开发的整个主链路就算真正打通了。后续无论换什么业务场景(图书销售、二手交易、预约系统),核心思路和大部分代码都是可以复用的。做的时候别怕改代码,多踩几个坑,答辩的时候反而更有故事可讲。