ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手工艺品销售系统设计与实现:Spring Boot + Vue前后端分离全过程

手工艺品销售系统设计与实现:Spring Boot + Vue前后端分离全过程 手工艺品销售系统这种项目在课程设计和毕业设计里出现的频率一直很高。原因很简单它既有电商系统的通用性又有垂直领域的特殊性而且手工艺品本身非标准化、价格浮动大、图片展示要求高这些特点决定了它不能照搬普通商品系统的那套逻辑。我接手过几个类似项目这次把完整的实现思路、数据库设计、核心代码逻辑以及我踩过的坑全部整理出来希望能帮正在做同类系统的朋友少走弯路。这个项目的交付物分两部分一份全套文档含需求分析、概要设计、详细设计、数据库设计等和一套可直接运行的源码。如果你是学生或者刚入行的开发者拿它来学习一个完整系统的架构思路、理解订单状态机的设计、掌握文件上传和图片处理的细节是非常合适的。切不要只停留在代码能跑我后面会花篇幅专门讲几个容易被忽略、但面试和答辩时很容易被追问的点。1. 项目整体思路与方案选型1.1 手工艺品销售系统的特殊性先聊聊为什么不能把通用电商代码直接套过来用。手工艺品有几个很明显的特征SKU不稳定、价格可浮动、强调图片和故事性、库存管理粒度粗。比如一件手工陶瓷杯它可能没有颜色尺码这些标准规格属性有的是“手作人”“制作时间”“瑕疵程度”这类个性化字段再比如价格手工艺品经常有“孤品”概念每件价格可能都不一样而不是一个SPU对应多个SKU的统一价格体系。这意味着在设计数据库和展示逻辑时需要把“商品”作为核心实体尽可能让它的字段具有扩展性不要把属性写死。我在设计里采用了经典的商品表商品图片表商品属性扩展表的组合即一个大类商品product对应多个图片product_image同时用一个json字段去存储动态属性这样在后续新增手工艺品类型时不用频繁改表结构。1.2 技术栈选型与理由这个项目我选用了Spring Boot Vue的前后端分离方案数据库用的是MySQL。选择它的核心原因有三职责清晰前端Vue负责页面渲染和用户交互后端Spring Boot负责业务逻辑和接口这种分离架构维护起来很方便也方便分组开发。生态成熟Spring Boot的starter机制能极大简化配置一个简单的销售系统用不到重型的分布式组件它的开箱即用特性很合适。就业主流无论课程设计还是实际工作这套技术栈都是需求量最高的做完以后的可迁移性最好。如果你拿到的是一个单体JSP/EasyUI版本也不要觉得过时它胜在直接、简单、好部署适合底层基础的场景。关键是把业务逻辑理清楚表现层技术反而是次要的。1.3 系统角色与用例分析这个系统我划了三个角色游客未登录用户可以浏览商品、查看详情、搜索商品但不能下单。注册会员登录后可维护收货地址、加入购物车、提交订单、查看订单、管理个人资料。管理员后台管理商品上下架、处理订单状态发货、退款、管理会员账号、查看销售统计。这组角色基本覆盖了一个小型销售系统的全部业务面。角色的权限控制在后端通过拦截器做简单但够用。登录态用JWT维护无状态方便前端跨页面携带token。2. 数据库设计与核心表结构2.1 总体ER设计与设计原则数据库设计是这类系统的地基地基不稳后面写代码全是坑。我遵循的原则是不做过度的表拆分但也不把所有东西塞进一张表里。整个系统我拆了九张核心表用户表member、商品表product、商品图片表product_image、购物车表cart_item、订单表orders、订单明细表order_item、收货地址表address、分类表category、管理员表admin。如果按源码的交付版本走可能还会带一张轮播图表banner和公告表notice这里按基础版本来讲。我画了基本的ER关系后一个比较容易被忽视的坑就浮出来了订单金额的计算来源。一张订单对应多个订单明细明细保存购买时的商品快照名称、图片、单价订单表保存汇总金额。为什么要做快照因为商品价格和描述是会变的如果订单生成之后商品改价了再回查订单明细就会对不上账。快照的意义就是锁定下单那一刻的信息不做实时联查。2.2 核心表字段设计与SQL要点下面贴一段核心的商品表结构我用MySQL的语句来说明具体字段以源码中的建表语句为准。CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(128) NOT NULL, subtitle VARCHAR(255) DEFAULT , main_image VARCHAR(255) DEFAULT , price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, detail TEXT COMMENT 详细描述含html, attributes VARCHAR(2000) DEFAULT NULL COMMENT 动态属性json, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );有几个字段我额外解释一下price用DECIMAL而不是FLOAT浮点型在精度上会有问题尤其结算时算总价DECIMAL(10,2)能满足绝大多数售价需求避免金额出现 0.1 0.2 不等于 0.3 的尴尬。stock是库存余量需要注意下单扣减库存时使用UPDATE product SET stock stock - ? WHERE id ? AND stock ?这种条件更新防止超卖误差。attributes用JSON字符串去存手工艺品的动态属性例如“材质”“尺寸”“手作人”。虽然严格意义上不在第一范式但中小型项目里的灵活性和扩展性是值得的查询时用JSON_SEARCH或直接在应用层解析都可以。用户表的设计不算复杂但没有冗余字段的必要手机号加索引密码存BCrypt加密后的哈希值而不是明文。订单表是相对复杂的核心它的状态用一个TINYINT维护从待支付1到已支付2、已发货3、已完成4、已取消5、退款中6、已退款7。我用了状态流转约束但并没有在数据库层面做CHECK而是在代码Service层统一做状态机校验这样既灵活又清晰。2.3 购物车设计套路购物车比较简单但有一个细节清空购物车时要只删除已勾选并成功生成订单的那几项而不是把整个购物车全删了。很多新手在这里直接写DELETE FROM cart_item WHERE member_id ?结果把用户手动加的其他商品也清了这是明显的逻辑错误。正确做法是提交订单时前端回传勾选的itemId列表后端生成订单成功后按这个列表逐项删除。这个细节如果做得不对演示时特别容易翻车。业务逻辑写在delKeys这个方法里本质上是一次循环删除。3. 核心功能模块实现解析3.1 用户注册登录与密码安全用户模块是系统的入口虽然逻辑不复杂但密码处理安全细节不能忽视。前端提交注册信息时后端必须在save方法里对密码做BCrypt加密绝不能直接明文入库。登录时用matches校验意外地解决了很多“数据库密码看不到”的尴尬问题。这里顺带提双向的登录态方案JWT生成一个过期时间为两小时的token前端把它存在localStorage里每次axios请求带上Authorization请求头后端拦截器检查token有效性和角色权限。有几点是课程设计答辩时很常见的追问点为什么要用token而不是session因为前后端分离后后端不做页面跳转session跨域跨端口不好维护而token天然无状态、可以被前端记住。token过期了怎么办前端在axios响应拦截器里统一处理401状态弹提示并跳登录页。3.2 商品浏览与检索商品列表和搜索是用户买不买得到东西的体验核心。前端首页通过接口拿轮播图、分类列表、热销商品核心是接口设计要尽量减少一次渲染所需请求次数。我做了这样的拆分首页聚合接口返回轮播图分类菜单推荐商品一次请求渲染整页。商品列表接口支持分类ID、关键词、价格排序、分页这四个参数满足基本的检索。商品详情接口返回商品信息、图片列表、动态属性并计算库存充足状态。价格排序如果用的是Vue前端需要本地排序的话很抱歉性能并不好必须让后端SQL排序。在Service层判断排序字段动态拼接ORDER BY price ASC/DESC分页参数通过MyBatis的PageHelper插件处理。体感上有个容易被忽略的优化点商品详情页图片多最好用懒加载组件同时对大图做压缩处理。我在后面项目里把上传的图片压缩后存一份缩略图列表页加载速度明显提升。手工艺品照片通常很精美但也很大直接丢原图会让列表卡顿。3.3 购物车与下单流程先聊聊购物车前端的存储。购物车的数据是应该持久化到后端表的因为用户换设备登录后购物车还在这个体验才是完整的。每次增删改都调后端接口同步。下单流程有几个关键步骤千万不要打乱顺序校验用户登录态。根据前端传的商品ID列表和数量查出对应的当前最新价格重新计算总价防止用户在页面停留期间价格被改动。校验每个商品的库存是否足够不足则返回具体是哪个商品库存不足。扣减库存生成订单主记录和订单明细。生成订单后从购物车删除对应已购项。我见过不少版本是直接用前端计算的总价直接入库那是个定时炸弹。正确的做法是以后端重新计算价格为准前端金额只做展示用途这个习惯在线上项目中也同样适用。订单生成时还需要生成订单号不要用数据库自增ID直接给用户看那会暴露订单量是很不专业的。我用了时间戳加随机数的拼接方式比如OTM yyyyMMddHHmmss 6位随机数。虽然理论上可能重复但概率极低配合唯一索引约束一下就够了。3.4 支付与订单状态管理支付功能是这个系统里最微妙的点。真实的在线支付接口需要商户资质课程设计或个人项目不太可能申请下来。所以这个项目里我的处理方式是模拟支付用户下单后可跳转到一个“模拟收银台”点“确认支付”后由后端把订单状态从待支付改成已支付。这套mock逻辑设计的好处是不依赖第三方接口系统功能依然完整业务流是闭环的。如果答辩时老师问“为什么不做真实支付”可以回答真实支付涉及商户资质和安全证书教学环境不适合接入改造成支付宝当面付或微信Native支付时只需替换支付Service层中的一个实现类即可。订单状态机我单独在Service里封装了changeOrderStatus方法每次状态变更前检查当前状态是否合法例如只有已支付订单才能发货已发货后才能确认收货。这样一块逻辑独立出来后面接真实支付时也不用大改。还有一个需要特别处理的是超时未支付订单的取消。简单做法是前端在待支付页面做倒计时到时自动调取消接口更可靠的方案是后端定时任务定期扫描把超过30分钟仍未支付的订单改状态并恢复库存。我两个方案都做了注释版实际部署时优先跑定时任务方案防止前端被关闭页面留下脏数据。4. 前后端关键实现与联调心得4.1 前端路由与页面结构我用Vue 2 Element UI搭建管理端用Vue 2 Vant搭建用户端H5风格页面。管理端结构大致如下登录页/login商品管理页/product/list列表上下架操作、/product/edit新增/编辑订单管理页/order/list订单列表、发货、退款处理分类管理页/category/list会员管理页/member/list用户端的核心页面首页/商品列表/goods/list?categoryId商品详情/goods/detail/:id购物车/cart结算页/checkout订单列表/order/list、订单详情/order/detail/:id路由守卫生效在用户端很关键未登录用户访问购物车、结算页、订单列表时直接重定向到登录页登录成功后再回跳之前的页面。Element UI的Loading组件在接口请求期间开着可以避免页面“白屏一下”。4.2 接口风格与统一返回体前后端联调最容易出现的问题就是接口返回格式不统一。这个项目我定义了一个统一返回体{ code: 200, msg: success, data: {...} }code为200代表成功10086代表未登录10010代表参数错误其他为业务失败。前端axios拦截器统一处理这样前端代码里不需要散落各种alert。后端Controller里不要直接返回Map或Entity尽量返回统一包装的结果对象。我一开始改写过几次如果每个接口返回裸数据前端处理出错时毫无安全感。我贴一个典型的Controller方法PostMapping(/product/list) public Result list(RequestBody PageQuery query) { PageInfoProductVO page productService.queryPage(query); return Result.success(page); }Service层返回VO而不是数据库Entity这里有一个细节值得强调商品主图、价格展示格式这类内容尽量在VO层组装不要每个字段都让前端去判断null。这样前端模板里的逻辑会清爽很多。4.3 文件上传与图片处理手工艺品的图片是销售转化的核心所以图片处理不能敷衍。后端我用MultipartFile接收上传文件存储到本地磁盘的/upload/目录并通过一个虚拟路径映射把磁盘目录映射成/upload/**可访问的静态资源形成“图片URL直接可访问”的效果。关键点是上传时要校验文件类型和大小。我限制只允许jpg、png、gif、webp这几类最大5MB防止用户传个exe、php文件到服务器上造成安全风险。扩展名校验不能只检验后缀我用文件头魔数来校验真实类型具体可以用字节流首几个字节判断对应的格式标识比如jpg是FF D8 FFpng是89 50 4E 47gif是47 49 46 38。如果前端骗后缀上传后端仍然能挡住。更完善一点的是保存缩略图。用Java的ImageIO读取原图等比缩放后输出一份宽300的缩略图列表页引用缩略图详情页引用原图。虽然增加了一点点开发量但用户端首屏速度会有肉眼可见的提升。5. 常见问题与排查技巧实录5.1 数据库连接与中文乱码课程设计里乱码问题太常见了。根源有两个一是MySQL建库时字符集没有指定utf8mb4二是JDBC连接串没有设置characterEncodingutf8。我建议大家建库时直接指定CREATE DATABASE craft_sale DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;同时在Spring Boot的application.yml的JDBC连接串上写上useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。三处统一设置后中文乱码基本断绝。另外一个坑是时区问题。MySQL 8默认时区是UTC导致插入的create_time比本地时间晚8小时。需要把连接字符串里的serverTimezone设置为Asia/Shanghai或者写GMT%2B8。5.2 跨域问题解决前后端分离开发时Vue跑在8080端口Spring Boot跑在8181端口跨域是必然出现的。解决方案是后端加一个全局CORS配置类允许所有来源、所有请求头。注意不要依赖前端代理因为在后端API部署到服务器上后跨域问题依然存在。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); } }如果集成了Spring Security还要确保预检请求OPTIONS不被拦截。这一点我在没使用Security时还好集成Shiro或Security的项目里最容易踩到。5.3 订单数据对不上与库存异常订单金额不一致通常有三种原因前端篡改数据、后端未重新查询最新单价、结算逻辑对不上的。我的排查习惯是先看打印出的SQL日志MyBatis开启logging.level.com.example.mapperdebug再比对订单表和明细表的数据就能快速定位是哪一层算错了。库存异常则多半是并发问题。比如两个用户同时买了最后一支手工木雕如果不加条件更新库存可能变成负数。我之前已经把扣减语句写成了stock stock - ? WHERE id ? AND stock ?还有一点容易忽略的是修改货品时要删除本地Redis里的缓存本项目没有用缓存但如果你加了否则会有脏读。5.4 上传图片无法显示这个问题在本地开发时频率最高。排查路径一般是这么几步先看文件是否真的落盘了没落盘说明目录权限或路径问题文件在但访问404说明静态资源映射配置有问题文件在且映射正常但图片变形说明前端容器在控制图片高度时被固定等比的样式影响或者上传时压缩算法问题。我项目里遇到过Windows路径和Linux路径分隔符不同导致移动文件失败的情况后来统一用File.separator拼接路径不再硬编码/或\\后问题消失。还有一点如果打包成jar跑文件需要存到外部目录不要塞进classpath里否则重启就丢图片。6. 部署与扩展建议6.1 打包与运行前端分别打包管理端和用户端Vue项目执行npm run build生成dist目录可以扔到nginx下托管API反向代理到8181端口。后端执行mvn clean package编译出jar通过java -jar启动。如果你打算在课程设计的演示环境里只想用一个端口搞定更简单的办法是把前端dist目录放入Spring Boot的src/main/resources/static下打包后直接访问8181根路径就能看到用户首页。管理端也可以同样处理通过不同的前缀路径区分。这个做法适合本地答辩演示纯分布式项目不建议。6.2 功能扩展方向如果做完基础功能后还有余力我推荐按以下方向去扩展既能加分又不至于太复杂搜索优化引入Elasticsearch成本较高其实是可以用MySQL的全文索引先顶一下或者从数据库模糊查询改成分词匹配都会有明显改进。优惠券加一张优惠券表coupon和用户已领取表user_coupon下单时核对券的有效期和适用范围。订单导出后台订单列表加一个“导出Excel”按钮用EasyExcel生成报表这在答辩展示时很亮眼。消息通知下单成功后发一封邮件或短信通知可以接入阿里云短信或者简单用JavaMail发邮件。我个人建议课程设计不要一开始想着大而全把核心流程做到稳定、可演示、可解释比堆一堆半成品功能要强得多。你做完基础版之后再挑一两个扩展点去深入那才是模拟真实项目节奏的状态。我之前做这个系统时花时间最长的地方反而不是写代码而是让订单状态流转逻辑做到“怎么走都不会乱”。把状态机理清了后面接手支付、退款、售后都会轻松很多。希望这篇整理能帮你把系统做扎实同时也让你在写文档和答辩时对每一个设计决策都有话可说。
返回列表