Spring Boot手工艺品销售系统:从架构设计到完整落地复盘 手工艺品销售系统怎么做才不烂大街基于Spring Boot的完整落地复盘手工艺品销售系统这个标题在各类毕设题目里出场频率很高——基于Spring Boot的XX销售系统、基于Web的XX管理系统看着平平无奇真正动手做的时候才发现水挺深。尤其是手工艺品这四个字它和普通商品销售最大的不同在于非标品、小批量、强图片展示、库存管理逻辑特殊不能套用标准电商模板硬抄。我这段时间把这个项目从零到一完整落地了一遍从前端页面到后端接口从商品上架到订单履约把每个环节都趟了一遍踩了不少坑也整理出一套可以直接复用的方案。如果你正准备做类似的Web项目或者正在为Spring Boot毕设选题发愁这篇内容应该能帮你省掉大量试错时间。先说结论这套方案选型为Spring Boot Vue MySQL MinIO前端用Vue构建用户端和管理后台后端提供RESTful API对象存储单独拎出来做商品图片管理不走本地磁盘也不直接塞数据库。完整跑通商品浏览、购物车、下单、支付回调模拟、后台管理、数据统计这几个闭环模块前后端加起来大约4500行代码不含前端框架自动生成的内容一个人一到两周可以完成。1. 项目整体拆解手工艺品销售系统的核心需求与模块划分1.1 手工艺品电商和普通商品系统差在哪很多人一看是销售系统就直接套电商模板用户表、商品表、订单表、购物车表完事。但手工艺品这个品类有几个实际特性直接影响数据库设计和功能优先级。第一非标品导致商品信息必须足够丰富。一件手工陶瓷杯它的尺寸、工艺、作者、制作周期、是否接受定制这些字段都是用户做购买决策的关键信息。所以商品表不能只有价格和库存得预留一个可扩展的详情信息结构最好拆成商品主表 商品详情属性表或者用JSON字段存扩展属性降低后续改表结构的成本。第二图片展示权重极高。手工艺品卖的是外观和质感无图无真相在这类场景里是完全的硬道理。一个商品至少需要3-5张图涵盖主图、细节图、场景图。这就要求图片存储方案必须稳定、可扩展不能把图片堆在本地磁盘目录里后期打包部署和迁移都麻烦。第三库存管理逻辑特殊。手工艺品通常一件起订、同一款式库存可能只有几件甚至一件很多还支持定制也就是先下单后制作。这意味着下单时不能简单做减法对于定制类商品订单状态要多一个待制作阶段而不是普通的待发货。这些细节才是答辩时能体现思考深度的点。1.2 功能模块怎么划用户端、管理端、订单工单三条线我的做法是划分成三个大模块对应三套接口体系和两套前端页面模块面向对象核心功能商品浏览与检索普通游客/用户分类浏览、关键词搜索、商品详情、图片预览交易链路注册用户购物车管理、提交订单、模拟支付、订单状态跟踪、评价管理后台管理员商品上架/下架/编辑、分类管理、订单处理发货、数据统计用户端不做注册的只有游客模式可以看商品但不能下单下单前强制登录这是很多销售系统容易忽略的点。管理后台单独抽出来放在另一个前端项目里和生产端分离。别为了省事把管理员功能直接塞进用户端页面前端路由管理和权限控制都会变得很混乱。用户端是一套Vue项目管理后台是另一套Vue项目两者共用后端API路径上用/api/user/**和/api/admin/**做区分。订单处理这块我设计了一个订单状态流转图无图文字描述待支付 - 已支付/待制作 - 制作完成/待发货 - 已发货 - 已完成/待评价其中待制作就是手工艺品定制场景特有的一环。后台管理员可以对订单做开始制作、确认发货两个操作每次状态变更都写入一条订单日志方便用户端展示进度。2. 技术选型这套组合为什么能扛住毕设和演示2.1 Spring Boot版本选型不追新只求稳热门搜索里springboot版本太高出现的频率相当高这不是空穴来风。Spring Boot 3.x要求JDK 17起步而很多学校机房、实验室的JDK还停留在1.8更麻烦的是3.x里javax包全面换成jakarta很多老教程里的代码直接报错MyBatis-Plus也有版本兼容问题。我的建议是如果目标是稳定跑通、顺利答辩选Spring Boot 2.7.18 JDK 1.8。Spring Boot 2.7是2.x系列的最后一个版本维护周期长资料最多市面上的教程、遇到过的坑基本都能搜到答案。同时它支持JDK 8到JDK 21就算你本机装了新版JDK也兼容建议还是老实装JDK 8或11。如果你确实想体现技术新颖性比如简历上写熟悉Spring Boot 3.x JDK 17那选3.2.x也可以但要做好以下心理准备所有依赖版本要手动对齐MyBatis-Plus需要3.5.5部分第三方工具包的兼容性要靠自己试。我个人不推荐在毕设阶段给自己加这个风险。2.2 MySQL Redis MinIO怎么分工数据存储这块我最终用了三个组件各司其职MySQL存核心业务数据商品、订单、用户、分类、评价一共六张业务表加一个订单日志表。Redis存验证码、登录Token或者用JWT二选一、购物车临时数据。实际上购物车也可以直接存MySQL但用Redis做能体现懂缓存的意识技术面试时这是加分话题。MinIO对象存储服务专门放商品图片。这是最值得推荐的组件——它把图片存储从应用服务器中彻底剥离开本地开发、服务器部署都是同一套逻辑。MinIO的部署很简单下载一个服务端二进制文件minio server启动默认端口9000控制台端口9001。Spring Boot项目里引入minio-java-sdk大概10行代码就能封装一个上传工具类。有个坑是MinIO SDK的版本8.x和Spring Boot 2.x的依赖兼容性需要用8.5.7以上版本低于这个版本会报NoClassDefFoundError。2.3 前端为什么不上Thymeleaf而是Vue如果只做后端用Thymeleaf模板渲染页面最省事服务端渲染一套代码搞定。但考虑到基于Web这个关键词和实际演示效果我选了前后端分离Vue 2.7 Element UI Axios一屏是用户端的商城页面另一屏是管理后台的表格操作界面。Vue的好处不只是页面好看更重要的是它把前端交互和后端接口完全解耦让整个项目呈现工程化的气质在答辩演示和简历描述的时候都有东西可讲。要说缺点也有配置跨域、处理Token传递、打包部署都要多花时间这些坑我在第4节里详细说。关于Vue版本我用的是Vue 2.7 Element UI因为Vue 3 Element Plus的学习成本略高而且网上Vue 2的现成组件和案例更多适合赶工期的场景。这里不评价谁好谁坏务实优先。3. 核心模块实操从商品上架到订单履约的关键代码与配置3.1 商品模块MinIO集成与图片回显商品图片是整个项目里第一个实操难点。我当时花了大半天踩了上传成功但图片不显示的坑才彻底搞明白。先把MinIO的工具类写出来Component public class MinioUtil { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Value(${minio.bucket}) private String bucket; private MinioClient client; PostConstruct public void init() { client MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } public String upload(MultipartFile file, String objectName) throws Exception { boolean exists client.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { client.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); } client.putObject( PutObjectArgs.builder() .bucket(bucket) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); // 返回可访问的URL不生成临时签名直接拼接endpoint和路径 return endpoint / bucket / objectName; } }注意这里返回的URL是直接拼接的。如果你用浏览器访问这个URL发现图片打不开大概率是MinIO的bucket权限问题是private必须要生成带签名的临时URL或者在MinIO控制台把bucket的访问策略改为public。毕设场景建议直接设为public最省事但要在答辩时说明生产环境应该用预签名URL两种方案知道怎么做就行。商品表的核心字段设计CREATE TABLE product ( id int(11) NOT NULL AUTO_INCREMENT, category_id int(11) DEFAULT NULL COMMENT 分类ID, name varchar(100) DEFAULT NULL COMMENT 商品名称, subtitle varchar(200) DEFAULT NULL COMMENT 副标题/一句话卖点, main_image varchar(255) DEFAULT NULL COMMENT 主图URL, sub_images text COMMENT 子图URLJSON数组, detail text COMMENT 商品详情, price decimal(10,2) DEFAULT NULL COMMENT 售价, stock int(11) DEFAULT NULL COMMENT 库存, sales int(11) DEFAULT 0 COMMENT 销量, is_custom tinyint(1) DEFAULT 0 COMMENT 是否支持定制0-否 1-是, craft_time varchar(50) DEFAULT NULL COMMENT 制作周期如3-5天, status tinyint(1) DEFAULT 1 COMMENT 1-上架 0-下架, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;sub_images用JSON数组存多个图片URL前端解析后再展示比单独建一张图片表简单得多也够用。3.2 购物车与库存校验乐观锁防止超卖手工艺品库存少但超卖问题在并发场景下依然存在。两个用户同时下单买同一件商品如果代码里先查库存再扣减就会出问题——都用最基础的select stock from product where id ?查到库存1然后都执行update product set stock stock - 1最终库存变成-1而订单却有两个。常规做法是加锁但直接在SQL层面用乐观锁更轻量也更适合展示。核心思路是update语句带着条件执行UPDATE product SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{productId} AND stock #{quantity}执行后如果影响行数为0说明库存不足或者已经被其他订单扣掉了直接抛出手慢无库存不足的提示。这个方案不需要引入Redisson分布式锁在单机部署的毕设场景下性能足够同时能体现并发安全的设计意识答辩时值得展开讲。购物车模块我放在Redis里key是cart:userId用Hash结构field是商品IDvalue是数量。加购时直接hincrby cart:userId productId 1查询时批量查商品信息拼装列表。这块没有太复杂的技术点主要提醒一点从Redis取出的购物车数据在生成订单前必须重新查一遍实时价格和库存不能让前端传过来的价格直接入库否则用户可以改价格下单。3.3 订单流程状态机设计与模拟支付订单模块是整个项目代码量最大、逻辑最绕的部分。我的建议是先把订单状态机用枚举定义清楚public enum OrderStatusEnum { UN_PAY(0, 待付款), PAID(1, 已付款/待制作), PRODUCING(2, 制作中), WAIT_SHIP(3, 待发货), SHIPPED(4, 已发货), FINISHED(5, 已完成), CANCELED(6, 已取消); private int code; private String desc; // getter... }用户提交订单走的是校验购物车 - 校验库存乐观锁扣减 - 生成订单状态待付款 - 清除购物车数据 - 跳转支付页支付环节在毕设里一般两种做法接支付宝沙箱或者直接做一个模拟支付按钮。我考虑到演示的方便选了模拟支付用户点击立即支付后端直接调一个mockPay接口把订单状态改成已付款同时写一条支付流水记录。如果你想让项目看起来更完整可以接支付宝沙箱APi文档很详细但要注意沙箱环境的配置和证书问题整体会多花一天时间。订单号生成规则不要用数据库自增ID会暴露销量信息而且不好看。我用SimpleDateFormat拼时间戳加三位随机数String orderNo HC new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()) String.format(%03d, new Random().nextInt(1000));前缀HC是Handicraft的缩写看着有辨识度答辩时可以顺嘴提一句。3.4 管理后台商品上下架与订单处理管理后台的页面逻辑比较套路商品管理表格带分页和搜索、分类管理树形列表、订单管理里的状态筛选和操作按钮。这里我做了一个自认为比较加分的功能数据统计面板首页展示今日订单数、销售额、热销商品Top5用ECharts画柱状图。统计接口需要写一条多表联查的SQL热度排序按销量排序SELECT p.name, SUM(oi.quantity) AS total_sales, SUM(oi.amount) AS total_amount FROM order_item oi LEFT JOIN product p ON oi.product_id p.id WHERE oi.create_time DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY oi.product_id ORDER BY total_sales DESC LIMIT 5这条SQL不复杂但涉及JOIN和GROUP BY是答辩时IT技术水平展示的常见话题。4. 开发实录五个最让人崩溃的坑及排查方式4.1 Spring Boot版本和JDK不兼容程序直接起不来这个坑排在第一位因为太常见了。现象是控制台启动到一半报错日志里出现java.lang.UnsupportedClassVersionError或者各种奇怪的ClassNotFoundException实际上就是Java版本不对。排查三步走java -version看当前JDK版本看Maven的pom.xml里spring-boot-starter-parent版本号确认IDE里的Project StructureIDEA中指定的JDK是同一个版本。我当时的组合是Spring Boot 2.7.18 JDK 8或11如果MAven报了Fatal error compiling多半是Maven默认的编译器JVM版本和项目不匹配在pom.xml里显式指定编译参数properties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties4.2 MinIO上传成功但页面图片裂开这是我第3节提到的坑的实际案例。上传返回的URL是http://localhost:9000/handicraft/xxx.jpg但浏览器打开是403。原因就是bucket访问策略是private。解决方案有两个二选一即可方案一MinIO控制台 - Bucket - Access Policy - 设置为Public路径为只读方案二写一个内网之外的FileController通过MinIO SDK的getPresignedObjectUrl生成预签名URL返回前端。我后期为了展示更专业改成了预签名URL方案但你必须知道预签名URL有有效期默认7天商品图片如果长期展示需要每次都动态生成或定期刷新。所以实际生产上图片存储一般走CDN这在答辩时可以提一嘴如果想提升加分项的话。4.3 登录接口调通了但带了Token还是返回401JWT或Token从登录接口获取后前端所有的请求都要在请求头带上Authorization: Bearer xxxxx。如果你发现前端控制台请求头带了Token后端还是401大概率是拦截器白名单没配好把所有接口都拦截了。我的拦截器配置经验registry.addInterceptor(jwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/product/**, /api/category/**, /api/home/** );注意顺序/api/product/**这种公开访问的接口必须在排除列表里。有些同学把拦截器直接放行/**又等于没拦截需要掌握好这个度。4.4 联调时前端报跨域错误前后端分离项目的经典问题。浏览器控制台报CORS错误网络请求直接FAIL。解决办法是后端全局配置CORS或者用Nginx做反向代理。开发阶段后端的CORS配置最直接Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有一个小坑allowedOrigins(*)不能和allowCredentials(true)同时用必须改成allowedOriginPatterns(*)否则前端发起带Cookie的请求时依然报错。4.5 数据库表设计不佳导致后期疯狂改代码最后这个是软坑但很致命。我一开始把商品属性颜色、尺寸、材质直接做成了列后来发现每加一个属性都要改表、改实体、改前端表单极其痛苦。后来参照电商标准做法改成扩展属性JSON字段属性值直接存JSON字符串前端动态渲染表单组件。这件事的教训是建表之前先多想一步哪些字段是稳定的、哪些是未来一定会变的。对于毕设项目来说JSON扩展字段比关系表更省事但要在设计文档里写明为什么不用多表。5. 打包部署与答辩现场的经验清单5.1 前后端分离怎么打包一个jar 一个dist前后端分离项目部署时最大的问题是前端怎么和后端一起跑。实际操作后端项目在根目录执行mvn clean package -DskipTests生成target/xxx.jar。Spring Boot默认内嵌了Tomcat所以这一个jar就能提供后端服务的所有能力。前端Vue项目执行npm run build生成dist目录里面是静态文件html、js、css。把dist目录里的文件复制到Spring Boot的src/main/resources/static/下重新打包。这样启动Spring Boot后访问localhost:8080直接就是前端页面localhost:8080/api/**走后端接口完美规避了跨域问题。这个方案的优点是不需要额外装Nginx服务器上只跑一个Java进程就能完整运行对毕设演示来说非常省事。部署时的命令也就是nohup java -jar handicraft-system-1.0.jar --spring.profiles.activeprod app.log 21 如果服务器上有Nginx也可以把dist部署到/usr/share/nginx/html再把/api反向代理到localhost:8080这两种方案选一条即可都简单。5.2 答辩前必做的三件事第一初始化数据要漂亮。商品图片必须找真实的手工艺品照片不要在数据库里塞一堆暂无图片的占位数据。图片资源可以在一些免版权图库网站上找或者自己拍几张实物图这一项很影响现场演示的第一印象。第二准备一个演示脚本。很多同学答辩时手忙脚乱一紧张就不知道点哪里。写一个按顺序操作的脚本比如从首页点开商品详情 - 加购 - 登录 - 结算 - 支付 - 后台看到订单 - 发货每一步对应系统里的什么功能五分钟内讲清楚整个业务闭环。第三准备一份系统架构图。不需要太复杂画清楚浏览器 - Vue - Nginx - Spring Boot - MySQL/Redis/MinIO这条链路就行。答辩时一张图抵得上讲十段话。画图工具任意PPT、ProcessOn都行。5.3 高频必问问题及回答思路为什么选Spring Boot回答思路Spring Boot解决的是Spring配置繁琐的问题内嵌Tomcat、自动装配、开箱即用专注业务逻辑这里的答案要结合自己的实际操作讲而不是背书。手工艺品系统和普通电商系统有什么区别回答思路非标品、图片展示权重、库存少、支持定制订单状态。这个问题答好了就是亮点。库存超卖怎么解决回答思路从SQL层乐观锁讲起再延伸说生产环境可以Redis分布式锁或消息队列削峰体现对方案演进的理解。项目里的安全性怎么考虑的回答思路密码MD5加盐或BCrypt、Token校验、SQL预编译防注入MyBatis自带、文件上传类型校验。6. 后续还能怎么扩展不只是交差最后聊点个人看法。如果你时间充裕这个项目还有几个方向可以延展工程量都不大但能显著提升项目档次第一接入邮箱/短信验证码替代纯图形验证码第二把评价模块做完整支持买家秀图片上传第三做一个小程序端或者移动端适配证明你有跨端开发能力第四增加简单的推荐功能根据用户浏览记录推荐同类商品——不用上机器学习直接同分类按销量排就够。这些方向挑一个做出来写在简历上项目经历那一栏比写满一堆熟悉XX技术管用得多。从我个人的实际体验看做这类系统最大的收获不是我会了CRUD而是完整理解了一个软件项目从需求拆解、数据建模、接口设计到部署上线的全过程。中间踩的那些坑当时觉得很耽误事现在回头看每一个都是能写进简历的真实经验。如果你现在也在这个过程里卡住了别急把每个报错日志一行行看完答案大概率就在里面。