ARTICLE DETAIL

资讯详情

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

SpringBoot眼镜商城系统:从零搭建电商毕业设计完整实战

SpringBoot眼镜商城系统:从零搭建电商毕业设计完整实战 要说毕业设计选什么方向电商系统真的是计算机专业里永远不会过时的选题。但正因为做的人多老师的要求也水涨船高——光是个CRUD页面根本交不了差。我做了一个基于SpringBoot的眼镜销售网站就是在线眼镜商城从选题、设计到最终写完代码完整走下来差不多两个月。这篇文章把整个项目的思路、核心流程、踩过的坑一次性讲清楚给正在头疼毕设的同学一个能直接落地的参考。这个项目能解决什么问题简单说它不是一个“玩具项目”而是覆盖了电商业务主链路的一整套系统商品展示、用户登录、购物车、订单、支付模拟、后台管理再配合Redis缓存、JWT鉴权、拦截器这些在企业开发里真正用得上的技术点。无论是做毕设、准备校招项目经验还是想学SpringBoot整合实战这套东西都够用。1. 项目整体设计与技术选型1.1 为什么用SpringBoot而不是SpringMVC这是很多同学会纠结的第一个问题。我在做之前也很犹豫学校课程教的是SSM框架XML配置写了一堆。但真到做项目的时候SpringBoot的优势太明显了——它把配置简化到了极致原来SSM里要配一大堆bean、扫描器、视图解析器SpringBoot一个SpringBootApplication注解全搞定。我当时的选型思路是基础框架SpringBoot 2.7.xJDK 1.8兼容性最好3.x需要JDK17学校老师环境未必支持持久层MyBatis-Plus比原生MyBatis省太多事BaseMapper自带CRUD分页插件一行搞定前端Vue2 Element UI前后端分离是加分项答辩时能多说几句亮点数据库MySQL 8.0存储过程、事务支持都成熟缓存Redis做验证码存储、热数据缓存、购物车临时存储鉴权JWT无状态登录适合前后端分离这个组合的好处是技术栈没有特别偏门的东西老师看着熟悉但每一个都在实际项目里用得着问到也能答得上来。1.2 功能模块怎么拆分我把系统拆成了两个大端前台商城和后台管理。前台商城面向普通用户包含这七个模块用户模块注册、登录、个人信息维护、收货地址管理商品模块眼镜商品分类浏览、关键字搜索、商品详情、库存展示购物车模块加入购物车、修改数量、删除、勾选结算订单模块确认订单、选择地址、提交订单、订单列表、取消订单支付模块模拟支付对接真实支付需要资质毕设用模拟流程即可评价模块订单完成后对商品进行评价打分优惠券模块领取优惠券、下单抵用这个模块属于锦上添花但加上之后整个项目的完整度一下子拉高后台管理面向管理员包含商品管理商品上架下架、库存调整、价格修改、图片上传订单管理订单列表、订单状态流转待付款→已付款→已发货→已完成/已取消用户管理用户列表、状态禁用数据统计简单销售额统计、热门商品Top10模块拆分的逻辑是按电商业务链路走的商品→用户→购物车→订单→支付→评价。答辩的时候老师一定会问模块划分依据用这条业务链路来回答最顺。1.3 技术选型背后的成败点说实话选型这一步决定项目最终能做到什么程度。我见过太多人一上来就上Spring Cloud微服务结果被各种分布式问题折磨到放弃。毕设项目的核心是“完整”和“能跑”单机单体架构完全够用重点精力应该放在业务逻辑的完整性和代码质量上。另外JDK版本千万别选太新的。我当时用JDK 17配SpringBoot 2.7遇到一堆兼容性问题后来老实切回JDK 1.8一路顺畅。毕设图的是稳定不是新潮。2. 数据库设计与核心实现细节2.1 数据表怎么设计才不被老师挑毛病数据库设计是答辩必问的重头戏。我当时做了9张核心数据表每张表的字段都考虑了实际业务需要而不是凭空定义。用户表userid、username、passwordMD5加密存储、phone、email、avatar、status0正常1禁用、create_time。密码加密这一点必须做如果密码明文存储老师会直接判定安全意识不合格。商品表productid、product_name、category_id关联分类表、description、price、stock、cover_image、sales_count虚拟销量、status0下架1上架。分类表categoryid、category_name、parent_id支持二级分类比如“太阳镜”→“男士太阳镜”、sort_order。购物车表cart_itemid、user_id、product_id、quantity、checked是否勾选、create_time。这里有个细节购物车数据如果用数据库存那Redis就少了一个发挥空间。我实际是数据库为主、Redis做缓存这样既稳定又能在答辩时展示Redis使用场景。订单表orderid、order_no唯一订单号、user_id、total_amount、pay_amount优惠后实际支付、coupon_id、status、receiver_name、receiver_phone、receiver_address、create_time、pay_time、ship_time、finish_time。订单明细表order_itemid、order_id、product_id、product_name快照字段防止商品改名后订单记录跟着变、product_image、price、quantity、total_price。地址表addressid、user_id、receiver_name、receiver_phone、province、city、district、detail_address、is_default。优惠券表couponid、coupon_name、amount面额、min_amount满减门槛、stock、start_time、end_time。用户优惠券表user_couponid、user_id、coupon_id、status0未使用1已使用2过期、receive_time、use_time、order_id。这里最容易被忽略的就是“订单明细里要冗余商品快照字段”。如果是单纯关联查询商品表拿名称一旦后台改了商品名历史订单显示就会变这是数据一致性的大坑。做过的项目越多越觉得这种细节才体现水平。2.2 JWT登录认证的前后端交互逻辑登录认证这块我第一版用的是Session后来改成了JWT。改的原因很实际前后端分离之后Session跨域要配很多乱七八糟的CORS配置Token方式跨域天然友好还不用考虑集群环境下Session共享的问题。JDK的JWT方案是加依赖jjwt登录成功后生成Token返回给前端前端存到localStorage里每次请求在Header里带上Authorization: Bearer token。后端我写了一个JwtInterceptor实现HandlerInterceptor接口在preHandle里做三件事取Header里的Token、校验签名和过期时间、把用户信息放到ThreadLocal里方便后续业务方法直接拿。关键代码如下public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.isEmpty(token)) { throw new BusinessException(401, 未登录或登录已过期); } try { // 需要替换成自己项目里生成的secret Claims claims Jwts.parser() .setSigningKey(projectSecretKey) .parseClaimsJws(token.replace(Bearer , )) .getBody(); UserContext.setUserId(Long.parseLong(claims.get(userId).toString())); UserContext.setUsername(claims.get(username).toString()); return true; } catch (ExpiredJwtException e) { throw new BusinessException(401, 登录已过期); } catch (JwtException e) { throw new BusinessException(401, 无效的Token); } } }注意这里有个实操细节UserContext用的是ThreadLocal实现但请求处理完必须remove不然Tomcat线程池复用时数据会串。这个坑在线上项目里很常见我在afterCompletion里专门做了清理。拦截器注册时还要排除放行路径登录接口、注册接口、商品列表、商品详情、图片访问这些不需要登录就能访问的路径。2.3 商品搜索缓存与分页查询优化商品模块是商城系统的门面用户进来第一个看到的就是商品列表。如果每次请求都去数据库做模糊查询数据量大了响应速度会很慢。我在实现时给首页推荐商品和热门分类做了Redis缓存# 缓存key设计 product:list:category:{categoryId}:page:{current} product:detail:{productId} product:hot:top10缓存更新的策略是后台修改商品时同步删除对应缓存等下次查询时重新回填。这个策略在技术上叫Cache Aside Pattern比先更新数据库再更新缓存更靠谱能避免并发情况下数据不一致。分页用的是MyBatis-Plus的分页插件配置一个PaginationInnerInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后是商品查询的Service实现。这里要注意一个分层问题Controller只接收参数和返回结果具体业务逻辑放在Service层不要写在Controller里。很多毕设代码被老师批评“逻辑全堆在Controller”就是分层没做好。2.4 秒杀与库存扣减的并发安全问题库存扣减是商城系统的核心难点。最简单粗暴的写法是先查库存、判断够不够、够就扣减——但高并发下这一定会超卖。我用了数据库层面的原子更新来解决// 使用条件更新库存大于数量时才更新 int updatedRows productMapper.deductStock(productId, quantity); if (updatedRows 0) { throw new BusinessException(500, 库存不足); }对应的SQLUPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这个办法比Java层的同步锁靠谱得多因为UPDATE语句本身在数据库层面就是原子操作多台服务器同时来请求也一样安全。后面结合Transactional事务注解保证订单数据和库存扣减在同一个事务里做到要么全部成功要么全部回滚。提交订单时还要注意先扣库存后生成订单。如果先建订单再扣库存库存不够时订单已经落库了要回滚整个事务逻辑绕而且容易出问题。3. 从零到一的实操搭建流程3.1 项目初始化与依赖配置用IDEA的Spring Initializr创建项目时有一个版本选择容易踩坑。我建议直接选SpringBoot 2.7.x这是2.x系列的最后一个维护版本稳定、资料多、兼容性好。JDK选1.8别犹豫。记住选择依赖时不需要贪多核心就这几个spring-boot-starter-web、mybatis-plus-boot-starter注意版本要用3.5.x太老的3.4版本和SpringBoot 2.7有些隐性问题、mysql-connector-java、lombok、jjwt、spring-boot-starter-data-redis。application.yml里最关键的配置spring: datasource: url: jdbc:mysql://localhost:3306/glasses_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: 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这里有个连接MySQL 8.0的细节serverTimezone必须显式指定不然会报时区错误。另外我还开了MyBatis-Plus的逻辑删除商品、用户这些核心表都加了deleted字段做软删除而不是物理删除这样万一误删了数据还能恢复。3.2 统一返回结构与全局异常处理前后端分离项目的接口规范特别重要。我定义了一个统一的返回体所有接口都返回这个格式前端拿到之后统一处理不用每个请求单独判断Data public class ResultT { private Integer code; 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(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配合全局异常处理器业务代码里抛异常就好不用到处写try-catchRestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public Result? handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) public Result? handleException(Exception e) { log.error(系统异常, e); return Result.error(500, 系统繁忙请稍后再试); } }这个设计的好处是代码里遇到异常情况直接throw new BusinessException(500, 库存不足)统一由异常处理器翻译成用户友好的JSON返回。既避免了每层手动处理异常的冗余代码也让接口风格非常统一。3.3 眼镜商品图片上传与存储方案眼镜商城的商品图片很多每个商品至少三五张图。如果直接把图片Base64存在数据库里数据库体积会迅速膨胀性能直线下降。我用的是本地文件存储方案接口接收MultipartFile保存到服务器指定目录数据库只存路径。public String upload(MultipartFile file) { // 生成唯一文件名避免重名覆盖 String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; // 按日期分目录存储 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); File dir new File(UPLOAD_DIR datePath); if (!dir.exists()) { dir.mkdirs(); } File dest new File(dir, fileName); file.transferTo(dest); // 返回可访问的URL路径 return /images/ datePath / fileName; }前端访问图片时需要配置静态资源映射。用WebMvcConfigurer的addResourceHandlers方法把/images/**请求映射到本地磁盘目录。这个配置很容易漏漏了图片就会404。3.4 订单状态机与支付回调设计订单模块是整个系统业务逻辑最复杂的部分。订单状态我用了一个整数来标记并定义了清晰的状态流转路径状态值含义可触发操作0待付款取消订单、支付1已付款/待发货无等待管理员发货2已发货/待收货确认收货3已完成评价4已取消无状态扭转都封装在OrderService里通过方法名直接表达语义createOrder、cancelOrder、payOrder、shipOrder、confirmOrder、commentOrder。每个方法都做了前置校验比如待付款订单才能取消、待收货订单才能确认收货防止非法状态跳转。支付模块我没接支付宝微信的沙箱环境因为个人账号申请麻烦而且审批周期长。用了最简单的模拟支付订单详情页点击“模拟支付”前端调payOrder接口后端把订单状态从“待付款”置为“已付款”同时更新pay_time。答辩时明确说明这是模拟流程再对比说真实支付需要走第三方支付平台的回调接口逻辑是通的。4. 常见问题与避坑实录4.1 前端跨域问题前后端分离项目启动后前端访问后端接口一定会遇到跨域问题。我在后端加了一个全局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); } }这里有个隐藏的坑如果用了JWT拦截器拦截器必须在CORS配置的允许范围内否则预检请求OPTIONS会被拦截器拦截返回401。我是在preHandle里单独放行了OPTIONS请求前面代码里已经提到了这是前后端分离项目非常容易忽略的前提操作。4.2 MyBatis-Plus字段自动填充创建时间、更新时间这种字段如果每次新加记录都要手动setCreateTime浪费时间还容易漏。MyBatis-Plus提供了MetaObjectHandler接口可以在插入和更新时自动填充Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }对应实体类的字段上需要标TableField(fill FieldFill.INSERT)或TableField(fill FieldFill.INSERT_UPDATE)。一次性配置好后面所有表都能复用。4.3 Maven依赖冲突与版本不兼容做SpringBoot项目最常见的报错就是各种NoClassDefFoundError或者ClassNotFoundException十有八九是依赖冲突。我在整合JWT时遇到过一次后来排查发现是jjwt里传递依赖了老版本的jackson和SpringBoot自带的冲突了。排查方法很简单mvn dependency:tree看依赖树找到冲突的包然后用exclusion排除dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version exclusions exclusion groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /exclusion /exclusions /dependencyMyBatis-Plus版本选择也要注意。SpringBoot 2.7配MyBatis-Plus 3.5.x基本没问题但如果用3.4.3以下的老版本分页插件可能要手动配置网上教程参差不齐容易卡壳。直接用新版省心。4.4 订单号生成的几种实现方式订单号生成要保证唯一还得有点业务含义。我用的是时间戳 用户ID后四位 随机数。代码实现public String generateOrderNo(Long userId) { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String timePart sdf.format(new Date()); String userPart String.format(%04d, userId % 10000); int randomPart (int) ((Math.random() * 9 1) * 1000); return timePart userPart randomPart; }这个方案在单机项目里够了。如果你担心并发高了会重复可以在数据库order_no字段加唯一索引一旦重复就捕获异常重新生成。真实电商系统的订单号设计比这个复杂得多但毕设级别这样做还能在答辩时说出设计思路已经够了。4.5 Redis缓存穿透与缓存雪崩的应对这个知识点答辩时如果主动讲出来老师印象分会明显不一样。我在商品详情查询里做了空值缓存来防缓存穿透查不到数据时也在Redis里存一个空值过期时间设短一点防止恶意流量直接打到数据库。public Product getProductDetail(Long productId) { String key product:detail: productId; Object cacheValue redisUtil.get(key); if (cacheValue ! null) { return (Product) cacheValue; } Product product productMapper.selectById(productId); if (product ! null) { redisUtil.set(key, product, 30, TimeUnit.MINUTES); } else { redisUtil.set(key, new Product(), 3, TimeUnit.MINUTES); } return product; }缓存雪崩的应对是给过期时间加一个随机值比如30分钟加上0到5分钟随机数防止同一时间大量key同时过期导致数据库被压垮。代码里直接30 new Random().nextInt(5)分钟就行。4.6 最容易翻车的答辩问题准备项目做出来只是第一步答辩才是定生死的环节。我把自己被问到的问题整理了一下核心集中在几个方向为什么选SpringBoot不选SSM答自动配置简化开发、内嵌服务器方便部署、生态成熟。表之间怎么关联的答通过外键逻辑关联实际开发为了性能不建物理外键靠Service层保证数据一致性。密码怎么保证安全答MD5加盐存储不能明文。更进一步可以提BCrypt但MD5盐在毕设里够用。如果用户同时下单库存不够怎么办答UPDATE条件锁数据库原子操作保证不超卖。Token和Session差异答Token无状态、适合分布式与前后端分离Session便捷但依赖服务端存储集群环境要额外处理。这些问题一定要提前准备哪怕项目代码有瑕疵只要核心原理讲清楚老师一般不会为难你。5. 项目扩展思路与二次开发方向如果答辩时间富余或者你想让项目更有竞争力有几个低成本高收益的扩展方向可以尝试。第一个是接入支付宝沙箱支付。蚂蚁沙箱环境提供完整的测试账号不用真实商户资质代码上只需要引入支付宝SDK按照官方文档配置应用ID、私钥、支付宝公钥把模拟支付替换成真实调起支付弹窗整个项目会有质的提升。第二个是增加后台数据报表。用ECharts做一个简单的销售额折线图和商品分类占比饼图接口用SQL的DATE_FORMAT和GROUP BY做聚合查询前台数据展示用Vue的v-for和ECharts的setOption绑定整体工作量不大但演示效果拉满而且能体现你对数据可视化这个方向的掌握。第三个是引入Elasticsearch做商品搜索。眼镜商城商品搜索的需求比较简单MySQL的LIKE查询基本够用。但如果商品数据量上来了、搜索要支持分词和相关性排序就需要ES出场了。这个扩展适合想把项目拔高一个维度的同学。考虑到时间和精力我最推荐第一个——接支付宝沙箱支付因为支付是电商系统最核心的一环而且面评效果格外好。做了这个项目最大的感受是毕设不是要把技术选得多花哨而是要把一条业务链路完整走通。从用户注册到最后拿到眼镜、作出评价其中每个环节都有真实企业在面对的问题——登录怎么鉴权、库存怎么扣、订单状态怎么流转、数据怎么缓存。把这些想明白代码写出来就是水到渠成的事。最后再分享一个小技巧项目里的注释不要追求多追求关键。每个方法上面写清楚“做什么、参数是什么、返回什么”每个复杂逻辑里面加一行核心注释解释“为什么这么写”自己回来看得懂老师看了也舒服。如果你的代码结构清晰、关键点有注释、异常处理到位答辩基本就稳了。
返回列表