新闻详情 资讯动态

全面了解最新资讯与建站知识,洞察行业趋势。

行业资讯

手工艺品销售系统实战:Spring Boot+Vue全栈开发详解

发布时间:2026/10/6 4:58:52
手工艺品销售系统实战:Spring Boot+Vue全栈开发详解 手工艺品销售系统这个项目我前前后后折腾了两周多。说实话一开始拿到这个题目我第一反应是这不就是个简单版电商系统嘛无非是商品展示加购物车加下单那点事。但真正动手做的时候才发现里面的细节远比想象中多——手工商品的特殊性、前后端联调的断点、订单状态流转的边界情况每一项都能让你卡上一整天。这篇东西我会把整个项目的设计与实现完整拆开从技术选型到数据库表设计从前端页面到后端接口再到实际踩到的坑和对应的解决方案尽量都讲清楚。项目采用Spring Boot Vue MySQL这套非常主流的前后端分离方案功能覆盖了用户注册登录、商品分类浏览、关键词搜索、购物车管理、下单与订单查询以及管理员后台的商品管理、订单处理和用户管理。整套源码配合文档来看结构清晰扩展性好非常适合正在做Java课程设计或毕业设计的同学参考也适合刚接触全栈开发的朋友照着练手——你不仅能学到一套完整的项目怎么搭还能看到前后端怎么一步步联调起来而不是停留在会写两个接口和页面的水平。1. 项目整体设计与思路拆解1.1 手工艺品销售的核心痛点与系统定位先聊聊为什么选这个题目。手工艺品和普通标准品有个巨大的区别它非标准化、地域性强、往往带着很浓的个人创作色彩。传统的线下手工艺品销售手工艺人只能在景区或本地集市摆摊买家想找一件有特色的手工艺品也只能靠碰运气。这就导致了一个信息断层——好的作品找不到合适的买家想买的人也找不到心仪的商品。所以这个系统要解决的核心问题就是给手工艺人和买家之间搭一座桥。它的定位并不是一个功能复杂的大电商平台而是一个小而美的垂直电商系统重点在于让手工艺品能被合理地分类展示、快速检索、顺畅下单。系统不做秒杀、不做满减、不做复杂的营销活动这些功能在毕设或课设阶段反而容易喧宾夺主把核心业务逻辑淹没掉。明确了定位之后功能边界也就清晰了用户端做注册登录、商品列表、分类筛选、关键词搜索、商品详情、购物车、订单管理管理端做商品维护、上下架、库存修改、订单发货。这些功能覆盖了一个电商系统最核心的闭环又不至于让开发量失控属于一个麻雀虽小五脏俱全的典型全栈项目。1.2 技术选型分析为什么是Spring Boot Vue MySQL技术选型是这个项目最需要想清楚的第一步。我自己在选型时也对比过好几套方案这里把当时的思考过程写出来供大家参考。第一后端框架。现在做Java方向的项目Spring Boot基本是默认答案。对比传统的SSHSpring Struts Hibernate或SSM框架Spring Boot把大量的配置自动化了内嵌Tomcat一个main方法就能启动项目开发效率完全不在一个量级。更重要的是Spring Boot的生态太成熟了——整合MyBatis、Spring Security、Redis等都有大量的现成案例和文档遇到问题搜一下基本就解决了。对于课设和毕设来说选Spring Boot等于给自己省掉一半的配置折腾时间。第二前端框架。这里我重点考虑过Vue和React。React的生态确实强大但Vue的上手曲线更平缓尤其是Vue 3的组合式API配合Vite开发写页面组件的体感非常顺。对于一个以业务功能为主的管理系统来说Vue的模板语法更直观中文资料也多遇到问题更容易找到答案。另外Vue的响应式机制配合Pinia做购物车的状态管理逻辑写起来很清爽。第三数据库。MySQL是这个场景下最稳妥的选择免费、稳定、通用性强大学课程里基本都教过。对比PostgreSQLMySQL的教程和工具链更丰富Navicat或DBeaver连上就能用。考虑到这是一个中小规模的项目并发量不大单机MySQL完全能扛住不需要上Redis做缓存也不需要引入分库分表避免给项目增加无谓的复杂度。1.3 角色权限与功能边界的划分系统里有三类角色权限划分很简单但必须清晰游客、注册用户、管理员。游客能做的事情有限只能查看商品列表和商品详情这是为了展示系统的基本浏览体验同时引导用户注册。注册用户可以管理购物车、下单、查看自己的订单列表和订单详情。管理员则拥有后台管理的权限可以对商品进行增删改查、上下架、调整库存也能查看和操作所有用户的订单比如修改订单状态为已发货或已完成。权限控制的实现我用的方案是后端统一拦截 前端路由控制。后端用拦截器拦截所有需要登录的接口请求通过Token校验判断用户身份并用角色字段判断是否有管理员权限。前端在路由守卫里判断如果没有登录或没有权限直接跳转到登录页。两处都做了校验才能保证页面显示和接口安全是同步的。注意很多人在做这种系统时只在前端做权限控制后端接口裸奔这是相当危险的。请求是可以被伪造的只要有人拿Postman直接调你的接口就能绕过所有页面限制。后端校验是底线必须做。2. 核心功能模块与数据库设计2.1 用户端功能模块梳理用户端是整个系统最核心的部分我设计的时候按一条完整的使用链路来梳理注册登录 → 浏览商品 → 搜索/筛选 → 加入购物车 → 提交订单 → 查看订单。注册登录模块前端表单做基本校验后端用BCrypt对密码加密存储。注意密码绝对不能明文存数据库这是基本的安全底线。登录成功后返回一个Token前端存到localStorage里后续所有请求都带上这个Token来识别用户身份。商品浏览模块首页展示推荐商品或最新上架的商品商品列表页按分类展示支持分页加载。商品卡片上展示图片、名称、价格和简要描述点击进入详情页。这里有一个细节手工艺品的图片非常重要因为手工商品的卖点就是它的外观和质感所以商品详情页至少需要两张图——一张主图和一张细节图分别在详情页的不同位置展示。购物车模块这是用户使用频率最高的模块之一。用户可以把多个商品加入购物车调整数量然后统一结算。购物车数据的存储用数据库表来持久化这样用户换设备登录后购物车数据还在。下单模块需要做事务处理。创建订单的同时要扣减库存、清空对应的购物车记录这三个操作必须在一个事务里任何一个失败都要回滚否则就会出现库存对不上、购物车残留之类的问题。订单查询模块用户可以在个人中心查看自己的所有订单按状态区分待付款、待发货、已完成等方便用户跟踪购买进度。2.2 管理端功能模块设计管理端的定位是给系统运营者用的功能比用户端简单一些主要是商品管理和订单管理。商品管理是最核心的后台功能。管理员可以发布新商品填写商品名称、分类、价格、库存、描述并上传商品图片。也可以对已有商品进行编辑、上下架操作。上架的商品会出现在用户端商品列表里下架的商品则直接隐藏。订单管理是管理端的另一个重点功能。订单列表展示所有用户下的订单管理员可以查看订单详情针对每个订单进行发货操作也就是把订单状态从待发货改成已发货。对于异常情况比如库存不足或用户取消订单管理员也可以关闭订单。用户管理就相对简单了主要是查看注册用户列表了解用户数量和信息一般不做太多复杂的操作但如果需要也可以禁用某个违规用户账号。2.3 数据库设计表结构、字段与关联关系数据库设计是这类项目最关键的部分表结构设计得不好后面写代码会处处别扭。我最终设计了六张核心表用户表、分类表、商品表、购物车表、订单表、订单明细表。用户表user核心字段有id、username、password、nickname、avatar、role、phone、create_time。id是自增主键username唯一password存的是加密后的密文role字段用来区分用户和管理员角色。分类表category相对简单id、name、sort_order一般分几个大类就行比如陶瓷、编织、木雕、饰品等。分类数量不多不需要做无限级分类这种复杂结构。商品表product字段较多包括id、category_id关联分类表、name、sub_title副标题或卖点、main_image主图、detail_image详情图、price价格用Decimal类型、stock库存、sales销量、status上下架状态、create_time、update_time。status字段我会用0和1来表示上架下架状态这样查询上架商品时直接where status1非常方便。购物车表cart字段有id、user_id、product_id、quantity、checked是否选中、create_time。这里需要注意一个细节同一个用户把同一件商品加入两次购物车不能用两条记录应该在原有记录上增加数量所以user_id和product_id之间要有唯一约束或者插入前先查一下是否已存在。订单表orders核心字段id、order_no订单号、user_id、total_price、status、receiver_name、receiver_phone、receiver_address、create_time、pay_time、deliver_time。订单号我建议用时间戳加随机数生成保证唯一性方便客服和用户查找订单。status用数字表示0待付款、1待发货、2待收货或已发货、3已完成、4已取消。订单明细表order_item字段id、order_id、product_id、product_name、product_image、price下单时商品单价、quantity数量、total_price。这里有几个非常重要的设计细节订单明细要冗余存储商品名称和图片因为商品信息后续可能修改但订单里必须保存下单那一刻的快照否则订单历史记录会被商品修改影响这在大电商里是铁律小项目也同样适用。各表通过外键逻辑关联后台管理查询订单详情时通过order_id找到order_item表记录再通过product_id关联商品信息。整体关系并不复杂但每一步都经得起推敲。3. 实操过程与核心模块实现3.1 项目工程搭建与目录结构我建议把前后端分成两个独立工程后端一个文件夹前端一个文件夹最后整个项目打包交付时结构一目了然。实际操作时后端我用Spring Initializr创建选择Java 8或11、Spring Boot 2.7.x、MyBatis、MySQL驱动这几个依赖因为Spring Boot 3.x对JDK版本有要求容易在环境配置上多花时间对课设来说2.7.x足够稳定。后端包结构我是这样设计的com.handcraft.shop ├── config // 跨域配置、拦截器配置 ├── controller // 控制器层接收前端请求 ├── service // 业务逻辑层 │ └── impl // 业务实现类 ├── mapper // MyBatis接口 ├── entity // 实体类 ├── dto // 数据传输对象请求参数、返回结果 ├── common // 通用类统一返回体、异常处理 ├── utils // 工具类JWT工具、文件存储工具 └── ShopApplication.java // 启动类前端用Vite创建Vue 3项目运行npm create vitelatest选择Vue模板后安装vue-router、pinia、axios这几个核心依赖。目录结构按功能分views放页面组件、components放公共组件、router放路由配置、store放Pinia状态、api放接口请求封装。目录结构清晰的好处是后续扩展功能或者找bug的时候能快速定位到对应文件不用满项目翻。很多同学喜欢把所有代码堆在一起这在课设规模不大时还能忍一旦功能变多你会非常痛苦。3.2 后端实现统一返回体与商品列表接口先写统一返回体。这个类的作用是把后端返回的数据包装成固定格式前端拿到数据后不用每次单独判断结构这是前后端分离项目的通用做法public class ResultT { private Integer code; // 状态码200成功500失败 private String message; // 提示信息 private T data; // 返回数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }商品分页查询接口是用户端最基础的接口对应的Controller层和Service层代码如下RestController RequestMapping(/api/product) public class ProductController { Resource private ProductService productService; // 分页查询商品列表支持分类和关键词筛选 GetMapping(/list) public ResultPageResultProduct list( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { return Result.success(productService.pageQuery(pageNum, pageSize, categoryId, keyword)); } }Service层的核心逻辑是拼接查询条件MyBatis的XML里通过动态SQL实现条件过滤public PageResultProduct pageQuery(Integer pageNum, Integer pageSize, Long categoryId, String keyword) { // 计算偏移量 int offset (pageNum - 1) * pageSize; // 查询当前页数据 ListProduct records productMapper.selectPage(offset, pageSize, categoryId, keyword); // 查询符合条件的总记录数 Long total productMapper.count(categoryId, keyword); return new PageResult(records, total, pageNum, pageSize); }分页这块最容易出错的就是页码和偏移量之间的换算。前端传的是pageNum1、pageSize10后端执行SQL的LIMIT语句需要的是offset0、limit10这个(pageNum - 1) * pageSize的公式必须写对不然就会出现第一页数据漏掉的问题。3.3 前端实现商品列表页与购物车状态管理商品列表页是用户端颜值担当布局上用卡片式展示一行三到四个商品每张卡片显示商品图片、名称、价格。交互方面顶部的分类栏可以切换分类筛选条件变化后重新请求接口。我用Vue 3的setup语法来写页面组件axios封装在api层这样页面里不用关心请求细节// api/product.js import request from ../utils/request export function getProductList(params) { return request.get(/api/product/list, { params }) }购物车状态用Pinia管理因为购物车数据在多个页面都要用比如添加购物车时要在商品详情页更新结算时要在购物车页面读取。用全局状态管理可以避免在多个组件之间反复传值// store/cart.js export const useCartStore defineStore(cart, { state: () ({ cartCount: 0 }), actions: { async addToCart(productId, quantity) { const res await request.post(/api/cart/add, { productId, quantity }) if (res.code 200) { // 更新购物车角标数量 this.cartCount } } } })当前端页面写好、后端接口也调通了之后整体的开发节奏就比较顺了先完成一个模块的接口再对接对应的前端页面边开发边联调避免最后集中联调时出现一堆不知道是谁的问题。3.4 订单模块的完整流程解析订单模块是业务逻辑最复杂的部分核心在于下单操作的事务一致性。我把下单拆成了三个步骤创建订单主记录、创建订单明细、扣减库存并清空购物车。Transactional public OrderVO createOrder(OrderCreateDTO dto, Long userId) { // 1. 计算总价 BigDecimal totalPrice calculateTotalPrice(dto.getItems()); // 2. 创建订单主记录 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalPrice(totalPrice); order.setStatus(0); orderMapper.insert(order); // 3. 创建订单明细 for (CartItem item : dto.getItems()) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } // 4. 扣减库存、删除购物车记录 productMapper.decreaseStock(dto.getItems()); cartMapper.removeByUserAndProducts(userId, dto.getItems()); return order; }方法上加Transactional注解是关键。如果不加事务假设第三步创建订单明细失败前两步的订单主记录已经写进数据库了就会产生一笔没有明细的脏订单如果扣库存失败则会超卖卖了货却没有把库存减掉后面发货时一对库存就发现对不上。所以三件事要么都成功要么都回滚。订单状态流转我也画了清晰的状态机下单后是待付款支付后变成待发货管理员发货后变成已发货用户确认收货后变成已完成。用户可以在订单列表页看到这些不同的状态前端用中文标签显示对应的数字状态码。4. 常见问题与排查技巧实录4.1 前后端联调跨域问题的原因与解决跨域问题几乎是每个做前后端分离项目的同学都会遇到的。我当时前端跑在Vite的5173端口后端跑在8080端口前端页面请求http://localhost:8080/api/product/list时浏览器直接报错提示跨域。跨域的根源在于浏览器同源策略前端和后端端口不同就属于跨域。解决办法有两种一是在后端加CORS配置二是在前端配置代理。两种方案我都用过更推荐后端配置CORS的方式因为前端部署到服务器后代理配置不一定好维护而且后端统一处理跨域更规范Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) // 所有接口 .allowedOriginPatterns(*) // 允许所有来源 .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true); } }如果前端用Vite开发也可以在vite.config.js里配置代理把/api开头的请求转发到后端地址这样游览器看到的请求是同源的也能解决跨域。但要注意代理只在开发环境生效项目打包部署到真实服务器后还得靠后端CORS配置。4.2 商品图片上传与访问路径问题图片上传这个功能我踩过一个不小的坑。刚开始我把图片保存到项目的static目录下本地开发环境测试一切正常但项目打成jar包部署后上传的图片保存到临时目录下次重启就丢了。这是因为jar包内的static目录是不可写的。后面我改成了外置存储方案在项目根目录下创建一个upload文件夹图片上传到这个文件夹同时通过Spring Boot的静态资源映射把/upload/**这个URL映射到本地的上传目录Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: System.getProperty(user.dir) /upload/); } }这样商品图片在数据库里存相对路径比如/upload/20240501.jpg前端展示时直接拼上后端地址就能访问。这种做法的好处是图片和代码分离重新部署项目不会丢失用户上传的图片也更贴近真实项目的文件管理方式。4.3 购物车库存与订单状态的一致性把控库存一致性是电商系统最容易出问题的地方虽然课设阶段并发量不大但代码里如果完全不管多线程测试时就会暴露超卖问题。我之前在做下单扣库存时一开始用的是先查库存如果大于0就扣减的写法-- 错误的写法 SELECT stock FROM product WHERE id 1; -- 在Java代码里判断 stock 0然后执行 UPDATE product SET stock stock - 1 WHERE id 1;这种写法在并发场景下是有bug的两个请求同时读到stock1都判断可以扣减然后都执行减一操作最终库存变成负数订单却下成功了。后面我改成了一条SQL原子扣减UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这条SQL通过stock #{quantity}条件在数据库层面保证了库存不足时不会扣减影响行数为0时就说明库存不够抛出异常让事务回滚。这样在课设的场景下已经足够可靠了不需要引入分布式锁这种重武器也容易给答辩老师讲清楚原理。另一点购物车和订单的一致性也很重要。下单成功后必须清空对应购物车条目否则用户下次进购物车发现那些已下单的商品还在会造成重复下单。这个逻辑要放在同一个事务里用userId加商品id列表批量删除。4.4 一套很实用的接口联调排查方法项目联调阶段难免遇到前端拿不到数据或页面报错的问题我总结了一套排查顺序屡试不爽第一步先用Postman直接请求后端接口带同样的参数。如果Postman返回正常那问题多半在前端如果Postman也报错那问题在后端直接看后端日志。第二步打开浏览器开发者工具的Network面板看具体请求的URL、状态码和响应内容。很多问题是请求路径写错了或者参数名对不上比如后端定义的参数名是categoryId前端传了category_id后端就接收不到。这类问题在Network面板里一眼就能看出来。第三步看后端控制台的日志。Spring Boot默认会打印SQL日志如果你的项目配置了mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImplMyBatis执行的SQL语句会直接输出到控制台检查一下SQL是带上了错误的条件还是LIMIT语句算错了偏移量基本就能定位。最后如果页面渲染有问题比如图片不显示、样式错乱优先检查前端的静态资源路径。这类问题往往和接口无关是自己把自己绕进去了。5. 我看到过的扩展方向与实用小技巧虽然课设做到这个程度已经可以交付但如果你想让项目在答辩时更有亮点或者学有余力我建议在现有基础上做两个低成本但高曝光的扩展。第一个是增加商品评价功能。在订单完成后用户可以对该订单里的商品进行评价评价内容包括文字和星级。这个需求在数据库层面加一张comment表就行用户端增加一个评价入口商品详情页展示历史评价列表。加了评价功能系统的完整性会明显提升答辩时也有更多业务逻辑可以讲。第二个是接入一个简易的报表统计。管理端增加一个数据看板展示平台的总销售额、今日订单数、热销商品Top10。实现方式很简单写几个统计SQL去order和order_item表里聚合数据前端用Chart.js或ECharts画几个图表视觉冲击力比纯表格强得多。再分享一个小技巧项目里的公共组件值得花时间打磨一下比如上传图片组件、分页组件、状态标签组件。当时我把分页和状态标签抽成了公共组件后面写订单管理页面、商品管理页面时直接复用省了不少重复劳动。代码的复用能力是答辩时老师比较看重的素质也是从能跑走向工程化的关键一步。

想做一个「会获客」的企业网站?

留下需求,1 小时内获取专属建站方案与透明报价。

免费咨询方案
↑