简介:基于Web的网上家具用品销售系统设计与实现,是一份Java方向的本科毕业设计论文,面向计算机专业学生、毕业设计者及对JSP/Java/MySQL开发感兴趣的技术爱好者,用于解决家具商城从需求分析、技术选型到功能落地的完整设计问题。整个资源共1个文件,为单个doc文档,压缩包大小1.66MB,内容涵盖系统背景、技术选型以及个人中心、轮播管理、家具资讯、客户审核、家具分类、家具审核、卖家审核、售后维权、统计中心等核心模块设计细节。相比纯代码,这份论文更强调软件工程方法与项目文档规范,读者能掌握将业务需求转化为系统功能架构的思路,学习JSP前后端交互与MySQL数据管理方式,并迁移到其他电商或管理系统开发中。目前该资源已有51人学习浏览,适合需要快速搭建毕业设计论文框架、了解网上销售系统全貌的在校大学生。
1. 基于web的家具销售系统:先定业务边界,再谈技术实现
很多人拿到「基于web的网上家具用品销售系统的设计与实现」这个题目,第一反应是开个Spring Boot项目然后狂写CRUD。真这么做,你的系统要么在答辩时被评委一句话问住,要么在演示时因为某个角落的bug直接翻车。这个标题背后的本质是一套B/S架构的小型电商系统,核心不在于「家具」这两个字,而在于「销售系统」四个字——它需要覆盖商品展示、检索、购物车、订单生成、库存扣减这条完整的交易链路,同时还要兼顾后台管理用的商品维护和订单处理。适合做这个题目的,是正在准备毕业设计、课程设计,或者想用一套可演示的案例去求职的开发者。本文会按我在实际小项目里验证过的路线,从工程选型、数据库设计、核心模块实现到部署答辩,把每一步的参数和边界讲清楚,让你照着能跑通,而不是停留在「能启动就行」。
2. 选型与工程骨架:单体会话方案,比前后端分离更适合这个场景
2.1 为什么Spring Boot + Thymeleaf是稳妥选择
网上家具销售系统的规模决定了技术栈不能太重。常见做法是用Spring Boot做服务端,模板引擎用Thymeleaf,数据库用MySQL,前端样式用Bootstrap或者原生CSS配合少量JavaScript。这种单体架构的好处是:你只需要维护一个工程,页面由服务端渲染,session天然可用,不需要处理跨域,也不需要在部署时同时起Node服务和Java服务。对毕设或者中小型演示项目来说,前后端分离引入的Vue和API调用会让人花大量时间在联调上,而收益在这个业务规模下并不明显。
如果你在「idea2024版本创建web项目」里看到过Spring Initializr的界面,那这条路线对你来说尤其顺手。Spring Initializr生成的项目结构自带启动类和测试骨架,你只要在此基础上补业务代码就行。依赖选择上,我一般会勾选Spring Web、Thymeleaf、Spring Data JPA或者MyBatis、MySQL Driver、Validation,再加一个Lombok减少冗余代码。不要一开始就引入Spring Cloud那套东西,销售系统用不到分布式,加上去只会让你的启动时间变长、排错范围变大。
2.2 用IntelliJ IDEA创建工程的三个参数要填对
我用IDEA创建这类工程时,有几个地方容易踩坑。Group和Artifact建议用com.furniture和sales-system这种见名知意的组合,Java版本选8还是11取决于你本机的JDK,选错会导致编译报错;Spring Boot版本不要追求最新,选2.7.x这类的稳定版本即可,因为3.x带来的Jakarta命名空间变化会让你在网上搜到的旧方案对不上。
工程创建完成后,application.yml是第一个要动的地方。数据源配置里的时区、编码和连接参数直接决定你后面会不会被中文乱码和时差折磨:
server: port: 8080 servlet: context-path: /furniture spring: datasource: url: jdbc:mysql://localhost:3306/furniture_sales?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mode: HTML encoding: UTF-8 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis: mapper-locations: classpath:/mapper/*.xml configuration: map-underscore-to-camel-case: true这段配置里,characterEncoding=utf8和serverTimezone=Asia/Shanghai是我后面排错时最后悔没提前写上的两项。前者不写,页面上提交的中文用户名存进数据库就变成问号;后者不写,订单时间会比北京时间少八个小时,答辩时演示「查看近一周订单」会让评委立刻看出问题。allowMultiQueries=true在MyBatis批量更新库存时会用到,注意这是MySQL连接串上的参数,不要写在MyBatis配置里。context-path统一加前缀,可以避免部署到Tomcat根路径时和你本机其他项目冲突。
提示:Thymeleaf的
cache: false只在开发阶段开,部署到生产环境前记得改成true,否则每次改完页面模板都必须重启服务才能看到效果,演示现场会很尴尬。
2.3 分包结构与前端资源的位置
工程骨架建好后,我习惯按controller、service、mapper(或者dao)、entity、dto、config、interceptor分包。家具系统业务虽然不复杂,但商品、用户、购物车、订单、管理员这五块逻辑相互依赖,如果不分层,最后改一个需求会牵动十几个类。前端资源放进src/main/resources/static,模板页面放进src/main/resources/templates,Thymeleaf默认从这里解析。注意静态资源里不要放JSP文件的旧习惯,Thymeleaf模板和JSP不能混用,混用会导致解析器互相抢资源,报错信息又晦涩难懂。
3. 数据库设计:家具商品表到订单表的状态流转
3.1 五张核心表的字段与关系
网上家具销售系统的数据库设计,我建议按「用户—商品—购物车—订单—订单明细」五张表起步,外加一张管理员表。家具商品的特殊点在规格属性多——沙发有材质、尺寸、颜色,床有软硬、尺寸、材质。如果在商品表里硬生生加上一堆列,后续扩展会非常痛苦。常见做法是主表存通用字段,规格差异用spec字段存JSON字符串。MySQL 5.7版本以上支持JSON类型,这比拆一张规格表省去很多join操作,对演示和简单后台管理都更方便。
建表脚本是这类项目里最重要的交付物,评委会直接看数据表设计。我把商品表和订单表的关键结构写出来:
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(120) NOT NULL COMMENT '商品名称', `category_id` bigint(20) NOT NULL, `price` decimal(10,2) NOT NULL COMMENT '现价', `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价,用于展示折扣', `stock` int(11) NOT NULL DEFAULT 0 COMMENT '库存数量', `main_image` varchar(255) DEFAULT NULL COMMENT '主图路径', `spec` json DEFAULT NULL COMMENT '规格:材质、尺寸、颜色等', `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1上架 0下架', `sales_count` int(11) NOT NULL DEFAULT 0 COMMENT '销量,排序用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号,展示给用户', `user_id` bigint(20) NOT NULL, `total_amount` decimal(10,2) NOT NULL, `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '0待付款 1待发货 2待收货 3已完成 4已取消', `receiver_name` varchar(50) NOT NULL, `receiver_phone` varchar(20) NOT NULL, `receiver_address` varchar(255) NOT NULL, `pay_time` datetime DEFAULT NULL, `deliver_time` datetime DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order_no用业务订单号而不是自增id作为对外展示的单号,我会在第五章的排查部分专门说为什么要这么做。sales_count这个字段很多人忽略,但对家具这种低频消费品,用户排序时最关注的就是销量,没有这个字段就只能用count(订单明细)去实时统计,数据量稍大页面就变慢。spec用JSON后,商品列表页不需要解析,详情页直接转成Map渲染成规格表即可。
3.2 购物车与订单明细:为什么购物车不存冗余价的实现方案
购物车表只需要user_id、product_id、quantity、checked四个字段加一个id。绝对不要在购物车里冗余price字段,因为购物车展示的是实时价,加入购物车时存了价格,结算时后台还要重新查一次商品表校验,留着一个过期价格只会带来不一致的bug。
订单明细表要冗余下单时的快照信息,包括商品名称、单价、图片、规格描述。这属于有意的反规范化设计——用户下单后商品可能改价甚至下架,但订单详情必须忠实还原用户付款时看到的商品信息,不能跟着实时数据一起变动。订单明细表的order_id关联订单主表,在MyBatis里用一个List<OrderItem>装载,这样在页面端可以用一次查询展示订单和商品列表,不会产生N+1查询问题。
3.3 库存扣减与「下架但未删数据」的约定
商品表里我加了status字段,下架用0表示,而不是物理删除。这个约定在电商后台管理里很重要,因为下架商品仍然关联历史订单,物理删掉会让订单明细失去参照完整性。后台管理员重置分类、整理页面时,下架商品还能帮你保留价格和规格信息。分类表可以再加一个sort_order字段控制展示顺序,家具的客厅、卧室、书房这些分类排序直接影响首页的商品陈列效果,静态排序比按名称排序靠谱得多。
4. 核心功能实现:商品检索、购物车与下单扣库存
4.1 首页商品列表与多条件检索的实现
家具销售系统的检索条件通常是分类+价格区间+关键词,再加一个排序选项。实现时不要指望一条复杂的SQL解决所有情况,我更习惯在Service层组装查询条件,用MyBatis的动态SQL处理。价格区间用price_min和price_max两个参数接收,前端不传就设成null,SQL端用<if>判断拼接。关键字搜索用name LIKE CONCAT('%', #{keyword}, '%'),注意CONCAT在MySQL的索引利用上有限制,但这个数据量级下完全够用。
商品列表接口返回后,前端用Thymeleaf的th:each循环渲染卡片。家具商品的图片一般比较大,加载慢是常见问题。我习惯在列表页请求缩略图路径,详情页用原图,这样列表页响应快,不至于因为一次加载二十多张大图把本机浏览器卡住。缩略图可以靠Java端简单缩放上传的图片生成,用Thumbnailator这个库几十行代码就能跑通。
如果商品图片存在OSS或云存储,缩略图和原图分离是基本功;如果是本地存储,我会在文件上传时就按尺寸各存一份,不要偷懒只存原图。4.2 购物车模块:登录拦截与数量校验
购物车的操作分「加入购物车」「调整数量」「选中结算」三段,每一段都有一个隐藏的防御逻辑。数量校验是很多人不写的漏洞——前端页面把输入框的max属性设成库存数,但攻击者可以用伪造请求把数量改成999。后端必须在加入购物车和修改数量时重新读取数据库里的库存值,如果请求数量大于库存,直接拒绝。演示时你可以现场打开浏览器开发者工具改数据,证明这个校验是真实存在的。
在我实现的项目里,购物车接口统一要求登录,否则跳转到登录页。这个策略用Spring拦截器实现,在WebMvcConfigurer里注册Interceptor,放行登录接口、商品列表接口和静态资源路径,其余购物车和订单接口全部拦截。注意Session里保存的是用户主键而不是用户对象,后续如果修改了用户状态,旧session不会立刻同步,但这在毕设场景可以接受,不必为它引入Redis刷新机制。
4.3 下单与扣库存:用事务和行锁解决超卖
下单是整个系统里最核心、也是评委最爱追问的事务场景。我采用的做法是:创建订单、创建订单明细、扣减库存、清空购物车这四步放到同一个事务方法里,在扣减库存这一条SQL上使用行级锁。
@Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateDTO dto, Long userId) { String orderNo = generateOrderNo(); BigDecimal totalAmount = BigDecimal.ZERO; List<OrderItem> itemList = new ArrayList<>(); for (CartItemVO cart : dto.getCartItems()) { Product product = productMapper.selectByIdForUpdate(cart.getProductId()); if (product == null || product.getStatus() != 1) { throw new BusinessException("商品不存在或已下架"); } if (product.getStock() < cart.getQuantity()) { throw new BusinessException("商品「" + product.getName() + "」库存不足,当前库存" + product.getStock()); } int updated = productMapper.decreaseStock(product.getId(), cart.getQuantity()); if (updated == 0) { throw new BusinessException("商品「" + product.getName() + "」库存扣减失败"); } totalAmount = totalAmount.add(product.getPrice().multiply(new BigDecimal(cart.getQuantity()))); // 组装订单明细对象并加入到itemList } OrderInfo order = new OrderInfo(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(0); order.setReceiverName(dto.getReceiverName()); order.setReceiverPhone(dto.getReceiverPhone()); order.setReceiverAddress(dto.getReceiverAddress()); orderMapper.insert(order); itemList.forEach(item -> { item.setOrderId(order.getId()); orderItemMapper.insert(item); }); cartMapper.deleteByUserAndProductIds(userId, dto.getProductIds()); return order.getId(); }这段逻辑里,selectByIdForUpdate是关键点。FOR UPDATE锁定被选中的商品行,同一件商品在这个事务提交前不会被其他线程再次读到旧库存值。decreaseStock的SQL是UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity},这条语句自带条件判断,即使没有锁,多线程同时执行时也只会有一个请求成功,配合行锁做到双保险。注意事务必须包含decreaseStock后的异常处理,一旦后面生成订单明细失败,rollbackFor = Exception.class让库存回滚,不产生幽灵订单。
提示:不要在Controller里做库存判断,那是无效的——并发场景下两个请求同时通过判断,库存仍然会被扣成负数。
4.4 模拟支付与订单状态流转
答辩演示时不可能真的接入支付宝,我通常加一个「模拟支付」接口:订单状态从0待付款换成1待发货,同时写入pay_time。后台管理员登录后有一张「待发货订单」列表,点击发货按钮把状态改为2待收货,并写入deliver_time。再做一个「确认收货」按钮,用户端点击后状态变为3已完成。这四个状态串起来就是完整的订单生命周期,也覆盖了表设计里状态字段的全部取值。
订单状态的变更我在Service里做了状态机校验:只有当前状态等于预期值才能流转到下一步,比如发货操作只允许从1转到2,直接从0跳到2就报错。状态机用最简分支判断即可:
if (!Objects.equals(order.getStatus(), 0)) { throw new BusinessException("当前订单状态不可支付"); }5. 避坑指南:家具销售系统中的五处高频翻车现场
5.1 图片上传后页面不显示,路径总是404
现象:后台管理上传商品主图成功,但前台商品列表页图片裂开,浏览器控制台提示404。
原因:我在早期版本里把图片存到了项目源码目录下的src/main/resources/static/upload,全量打包重启后文件被清掉,而且IDEA的资源编译目标目录可能不包含这个路径。第二个坑是拼接图片URL时写死localhost:8080,部署到云服务器后路径全废。
解决:把上传目录配置在application.yml里,用绝对路径指向项目外的目录,比如D:/furniture-upload/(Windows)或者/data/furniture-upload/(Linux)。然后配置一个WebMvc资源映射把/upload/**指向这个本地目录。这样打包重启不影响图片,部署到服务器只需要把整个目录拷过去。记住,main_image字段直接存/upload/xxx.jpg,页面用th:src="@{${product.mainImage}}"渲染,引擎会自动拼接上下文路径。
5.2 订单号过长、重复或直接用自增id暴露销量
现象:订单详情感perception不对,或者两份订单的展示编号一样;又或者答辩时评委看到订单号是1、2、3,追问是不是可以直接猜出系统卖了多少单。
原因:直接用数据库自增id当订单号是最省事但最不专业的做法。自增id在同一张表内唯一没问题,但对外暴露业务量,且让爬虫可以遍历所有订单;不加前缀的话,多个系统服务拼接时容易冲突。
解决:用yyyyMMddHHmmss + 三位随机数 + 用户id后四位拼业务订单号,落库前查重一次,数据库侧再建唯一索引兜底。这样的订单号可读性好,能看出下单时间,也不会暴露整体销量。生成代码放在Service层,不要写在Mapper里。
5.3 中文乱码:页面、数据库、控制台三处各乱各的
现象:页面提交的中文商品名存进MySQL变成???,控制台打印的SQL日志中文乱码,页面显示正常但导出Excel时乱码。
原因:三层编码不一致。数据库连接串没有characterEncoding=utf8,MySQL表的字符集不是utf8mb4,Thymeleaf模板文件保存编码不是UTF-8。这三个问题单独发生和同时发生的处理方式不同,排查时先把表的charset查一遍。
解决:建表统一用utf8mb4,连接串加characterEncoding=utf8,模板文件用IDEA右下角确认编码为UTF-8,server.servlet.encoding.force=true强制请求和响应编码。想验证是否修复,后台新增一个中文名称的商品,再到mysql>客户端执行SELECT name FROM product WHERE id = 最新一条,看到中文而不是问号才算通过。注意MySQL 8.0驱动默认utf8mb4,但老版本驱动仍需显式指定字符集。
5.4 并发下单导致库存变负数
现象:用JMeter或者十几个人同时抢一件商品时,库存变成负数,订单还能创建成功。
原因:Controller层先查库存、判断大于等于数量,再执行update扣减。两个请求同时查到的库存剩余都是1,都能通过判断,然后各自扣掉1,库存变成-1。判断和扣减不是原子操作。
解决:把「判断库存是否充足」和「扣减库存」合并成一条SQL,即UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity},返回值是影响行数。如果影响行数为0,说明库存不够,直接抛异常回滚事务。再多并发也不会出现负库存,因为数据库的行锁让这条SQL在同一商品上串行执行。这条方案比悲观锁配合编程式判断更简洁,也更容易在答辩时讲清楚。
5.5 部署后8080端口被占用或访问不了
现象:本地运行正常,部署到Linux服务器后浏览器访问不了;或者启动提示Port 8080 was already in use。
原因:服务器安全组没有放行8080端口,或者另一个Java进程占用了端口。
解决:先用netstat -tlnp | grep 8080检查端口占用;如果是自己残留的进程,kill掉后重启应用;如果安全组问题,去云控制台放行端口。同时为了让部署更稳,建议启动脚本里用java -jar furniture-sales.jar --server.port=8080 &指定端口,并用nohup挂后台。如果测试环境有多套服务,把context-path设为/furniture能少很多冲突。
提示:Spring Boot默认内嵌Tomcat,打包时用
mvn clean package打成的jar,在Linux上直接java -jar运行即可,不需要再额外装一个Tomcat。把端口和数据库地址作为启动参数传入,换环境时不用重新改代码。
6. 从能跑到能交付:提升演示说服力的四个关键设计
6.1 数据填充:造假数据也要造得专业
家具销售系统最终要给人演示,空数据库展示出来非常难看。我建议写一个DataInitializer脚本,自动生成十几个分类、每个分类下若干个商品,商品价格要符合市场价——沙发5000到10000元,床垫1500到4000元,书桌800到2500元。图片可以用占位图服务或者本地放几张免费素材图,别用版权不明的商业图。库存建议留几个低库存商品,比如库存3件和5件,现场演示购物车提交时能展示库存扣减的效果。再用SQL脚本批量插入几十条历史订单,让后台管理页面的订单列表和数据统计图表不空荡。
6.2 页面精简:别在样式上花太多时间,但三个页面必须精致
首页、商品详情页、购物车结算页是演示时评委或面试官看得最久的三个页面。首页要体现分类导航、特价专区、销量排序;详情页要有图片展示、规格参数表、加入购物车按钮、库存状态;结算页要有收货人表单、商品明细、合计金额。这三个页面建议用Bootstrap栅格布局,保持简单干净,不要去做花哨的动画或大图轮播,动画卡顿会拉低整个演示的完成度。
列表页的排序功能不要做成只有名字排序,把「销量优先」「价格从低到高」「价格从高到低」三个都做上,这在演示时是快速展示系统完整性的一个细节。
6.3 答辩准备:预判会被追问的三个问题
第一个问题是「为什么不用Redis做购物车」,答案要点是:Redis适合分布式场景下的会话共享,本系统单体架构下Session存储购物车已经足够,引入Redis是过度设计,但如果你使用了Redis,那就准备好被追问数据一致性问题。第二个问题是「库存扣减怎么做并发控制」,上面讲的update ... where stock >= #{quantity}加事务就是这个问题的标准答案,演示时可以用两个浏览器窗口同时下单验证。第三个问题是「订单状态为什么用数字而不是字符串」,数字的好处是做状态机校验方便,占用空间小,索引效率更高,但需要维护一个状态枚举类来保证代码可读性。
6.4 打包部署:用Maven一次打出可运行jar
最后讲落地的工程习惯。mvn clean package打包后,确认jar包在target目录里生成。Spring Boot的默认打包插件会把依赖一起打进去,生成的是胖jar,直接可以java -jar运行。部署时把数据库连接参数和上传目录配置放到application-prod.yml里,启动命令加--spring.profiles.active=prod,避免每次改环境都动配置文件。落库前的mvn test建议至少保留一个测试类,验证「库存充足时下单成功」和「库存不足时下单异常回滚」这两个核心用例,面试官翻到测试代码时会明显更认可你这个项目不是只写了CRUD。
我个人的习惯是项目做完后把启动说明和数据库初始化脚本放在一个README.md里,再配上几张页面截图。这种整理工作看起来不产生代码,但每一次答辩、每一次把项目展示给别人的时候,它省下的解释成本都远超过写它的时间。希望这篇笔记能帮你在做家具销售系统时少踩几个坑,把精力留在真正能给系统加分的设计上——比如并发库存、状态机、订单号和数据的完整性上。
本文还有配套的精品资源,点击获取