ARTICLE DETAIL

资讯详情

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

基于SpringBoot的免税商品优选购物商城系统开发实战

基于SpringBoot的免税商品优选购物商城系统开发实战 又到了毕设选题的季节后台咨询量明显涨起来了。Java方向的选题翻来覆去就那么几类各种管理系统、二手交易平台、博客系统、通用商城。管理系统太单薄答辩容易空通用商城又同质化到导师看了都打哈欠。如果你正在纠结SpringBoot实战项目选什么题或者拿到的项目源码不知道从哪下手我建议你看一看基于SpringBoot的免税商品优选购物商城这个方向。它比普通商城多了一层业务差异化又不像高并发秒杀那样难以落地既能讲清楚技术细节又能体现你对业务规则的理解。这篇文章就围绕我当时做这个项目的完整思路、核心模块设计、源码运行步骤以及我在开发和答辩过程中踩过的坑一次性写清楚基本上拿来就能用。1. 为什么我选了免税商品优选购物商城选题动机与业务边界1.1 毕设选题的常见坑为什么别做通用商城先说说大实话。每年都有大量学生做网上购物商城但绝大多数都长一个样用户注册登录、商品列表、购物车、下单、后台CRUD。功能是齐全但答辩的时候导师问一句你这个和淘宝有什么区别项目难点在哪里就答不上来了。原因很简单通用商城是基础设施级别的产品单个学生不可能做得比淘宝好导师也不指望你做出来他指望的是你能说出你这个场景下的特殊问题、特殊设计。选免税优选这个主题的好处在于它把电商系统这个通用底座放在了一个有明确业务规则的场景里。免税商品有额度控制、有价格计算差异、可能有件数限制这些规则是普通商城没有的这天然就是论文里的业务创新点和答辩时的为什么这么设计素材。说白了这个题目能让你在毕业答辩时讲出东西来而不是全程聊增删改查。1.2 免税商城的核心业务特征拆解我自己在设计和实现的时候没有急着搭框架而是先把免税购物场景的业务逻辑画了一遍。普通商城是商品-库存-订单三件事免税商城则要额外处理四件事商品维度哪些商品属于免税商品哪些是完税商品展示价格时是否区分原价、会员价、到手价。大多数商城只是打折价免税商城的价格标签背后是税费逻辑。用户维度每个用户有年度免税额度额度使用是累计的只有当笔订单完成后才扣减未支付订单不能占用额度——这一条会直接影响下单接口的事务边界。订单维度是否允许拆单、超额时是拦截还是转完税渠道、提交订单后额度是预占还是实时校验不同选择对应不同的并发场景。营销维度免税商品通常有限购件数同一身份证件下同款SKU的累计购买件数需要控制这和普通商城的每人限购一件在实现上差不多但业务语义完全不同。你不一定每个点都做但至少要能说出来这几个点的存在。我当时就在项目说明里写了本系统重点实现了免税额度累计校验与预占释放机制——导师看到这一句就知道你不是在套模板。1.3 这个项目适合谁、能锻炼哪些能力如果你是学Java、以后想走后端开发方向的这个项目的含金量较高主要体现在几个层面。第一是框架整合能力SpringBoot是主干但你要把MyBatis Plus、Redis、JWT、定时任务、参数校验这些组件串起来这是真实开发的常态。第二是业务建模能力免税额度不是简单的字段加减它是带状态的业务对象你得想清楚在什么时机扣减、什么时机释放。第三是应对不知道从哪看源码的问题拿到一份完整的SpringBoot项目源码能顺利跑起来并完成自己的二次开发本身就是必须过关的环节。当然如果你是前端为主或者几乎没有Java基础这个项目的前置门槛也不低。至少要能读懂Controller-Service-Mapper三层结构能把数据库跑起来能在配置里改数据源。好在SpringBoot约定优于配置比起SSH时代的XML地狱现在起步已经轻松不少。2. 技术选型与整体架构SpringBoot整合方案的关键决策2.1 技术栈清单与选型理由我当时选择的技术栈如下你可以直接抄组件选用方案说明与选择理由后端核心Spring Boot 2.7.x稳定版本适配JDK 8/11避免过高的版本来回折腾依赖ORM框架MyBatis Plus单表CRUD手写量小分页、条件构造器、代码生成器都有适合快速出活数据库MySQL 5.7 / 8.0电商项目最通用的选择主库订单、商品、用户三块核心数据缓存Redis Spring Data Redis承担购物车缓存、首页热点商品缓存、额度预占临时标记也演示了缓存一致性处理鉴权方案JWT 拦截器无状态鉴权前后端分离最常用的方式不引入Spring Security避免学习成本过高前端页面Vue 2 Element UI或Thymeleaf如果答辩要求独立部署用Vue更直观如果还想省事后端模板引擎也行接口文档不强制但推荐Knife4j加分项可以让答辩老师现场看接口文档结构定时任务Spring Scheduled用于处理超时未支付订单、释放预占额度这里我想多说一句为什么不用Spring Security。毕设场景追求的是把核心业务闭环跑通而非把整个安全框架内部机制搞多深。JWTHandlerInterceptor可以让你亲手控制登录校验、角色鉴权、接口放行逻辑反而更能在答辩时说清楚token是什么、为什么无状态、拦截器拦截了哪些路径。真用了Security问深一点就是FilterChain、AuthenticationManager、UserDetailsService一堆东西背都背不完。2.2 项目目录结构与分层设计项目源码拿到手后先看目录结构。典型的分层如下springboot-duty-free-mall ├── src/main/java/com/example/dutyfree │ ├── common // 通用结果封装、异常、常量、工具类 │ ├── config // 配置类redis、跨域、拦截器、定时任务 │ ├── controller // 接口层分离前台与后台包 │ ├── service // 业务接口与实现 │ ├── mapper // MyBatis Plus的Mapper接口 │ ├── entity / po // 数据库实体 │ ├── dto / vo // 入参出参对象 │ └── DutyFreeMallApplication.java ├── src/main/resources │ ├── mapper // XML非必要MyBatis Plus注解够用 │ ├── static/public // 图片资源、静态文件 │ └── application.yml ├── sql // 初始化SQL脚本 ├── pom.xml └── README.md分层的核心原则Controller不写业务逻辑只管接收参数、调用Service、返回结果Service写业务流程事务边界在这里声明Mapper只做数据访问。我看过很多人把Service写成一个大杂烩几百行代码一个方法打天下改起来痛不欲生。这个体量的项目宁可多拆几个方法也别想着BeanUtils.copyProperties一把梭。2.3 数据库表设计除了五张核心表还要有哪几张项目源码里的SQL脚本我建议你认真读一遍因为那些建表语句本身就是论文的数据模型部分。免税商城数据库大致包含用户表、会员等级表、分类表、商品表、商品SKU表、购物车表、订单表、订单明细表、额度流水表、收货地址表、轮播图表、操作日志表、支付记录表。订单表和订单明细表是核心中的核心建议拆分。原因很简单一个订单包含多个商品商品信息在下单后可能改价所以订单明细必须快照商品名称、单价、数量、实付金额不能通过关联当前商品表来算历史订单金额。很多同学图省事把商品直接存进订单表的一个字段里答辩时被问用户历史订单里的商品价格怎么保证不被后续改价影响就卡住了。这个问题你在设计之初就该想明白。额度流水表的用途很多人会忽略。既然是优选免税商城每位用户的免税额度来源于交易流水累计你总不能只存一个当前余额字段——用户投诉、对账、展示已用额度都需要流水明细。所以表里要有用户ID、额度类型增加/扣减/释放/过期、变动金额、当前累计值以及关联业务单号。每一笔扣减都对应一个订单号每一笔释放也对应一个订单号。这样任何额度问题都可以反向追溯到具体订单。3. 核心业务模块的实现思路与关键代码3.1 商品模块免税标记、SKU与多维价格展示商品模块从外面看是列表、详情、搜索但核心其实是数据模型。免税商品需要比普通商品多几个字段我在设计商品表时加了这些is_duty_free0否1是标记商品是否属于免税商品duty_free_type免税类型用于描述适用场景比如完税商品、跨境商品等origin_price原始定价sale_price商城售价duty_free_price免税价通常是活动价或线下参考价member_price会员价非必填limit_quantity该SKU的限购件数商品列表页展示价格时不能只读一个字段需要根据用户状态和商品标记计算展示价。这里我写了一个简单的逻辑// 简化逻辑价格选择策略 BigDecimal getDisplayPrice(Product product, User user) { // 免税商品优先展示免税价否则展示售价 BigDecimal price product.getSalePrice(); if (product.getIsDutyFree() ! null product.getIsDutyFree() 1) { price product.getDutyFreePrice(); } // 会员折扣MVP阶段只做固定折扣率 if (user ! null user.getMemberLevel() ! null user.getMemberLevel().getDiscount() ! null) { BigDecimal discount user.getMemberLevel().getDiscount(); price price.multiply(discount).setScale(2, RoundingMode.HALF_UP); } return price; }这段代码解决的问题是同一个商品在不同用户眼里价格不同。你需要意识到展示价不等于结算价用户把商品加入购物车那一刻就应当锁定价格快照否则结账时价格变动会造成非常糟糕的体验。我在购物车表里冗余了product_snapshot_price字段下单时以购物车快照为准这样也便于后续做促销活动。库存扣减是商品模块的另一个重点。不建议用select stock; if stock 0; update stock这种三步操作并发一高就出超卖。我当时用的是乐观锁加条件更新Update(UPDATE sku SET stock stock - #{count} WHERE id #{id} AND stock #{count}) int deductStock(Param(id) Long skuId, Param(count) Integer count);这一行SQL的意思很直接更新时带上库存充足的条件影响行数为0就是扣减失败立刻返回库存不足。配合Service层的事务商品秒杀场景下的超卖问题基本就堵住了。3.2 订单模块状态机设计与超时释放订单状态这个坑我见过太多人用一个字符串字段靠直觉更新。正确做法是先定义状态流转规则再写代码。我建议在项目里定义订单状态枚举public enum OrderStatus { PENDING_PAYMENT(0, 待付款), PAID(1, 已支付), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CLOSED(4, 已关闭); private final int code; private final String description; }然后把这个枚举用在哪里一方面是订单表存status_code另一方面在Service层写一个状态流转校验方法待付款可以取消或去支付已支付可以发货已完成只允许申请退款已关闭就是终态。不要把状态流转散落在各个Controller里否则改一个需求要翻遍全项目。超时未支付怎么处理我在项目中开了一个Scheduled(cron 0/60 * * * * ?)的定时任务每分钟扫描一次待付款且创建时间超过30分钟的订单一次性做两个操作把订单状态改为关闭同时调用额度释放逻辑如果用户在下单时预占了额度。用定时任务的好处是代码简单、稳定可控。要是为了体现高并发才去用延迟队列答辩时反而容易被追问消息丢失怎么解决时间精度够不够自己给自己挖坑。配套的还有支付回调接口。毕设肯定是模拟支付但接口设计一定要正规。我定义了一个payNotify方法要求调用方传订单号、支付流水、金额三个参数然后做三件事验单订单是否存在、金额是否一致、状态是否为待付款、改状态、把流水写入支付记录表。很多同学直接把支付和下单写在一个方法里却没考虑用户点了支付但支付失败重新发起的情况这就是状态机缺失导致的。模拟支付时你完全可以手动调用接口但逻辑上要像真实支付一样具备幂等意识——同一笔订单重复回调不能把状态从已支付改成已支付之外的东西。3.3 免税额度校验预占与释放的实现逻辑这是整个项目我最想说清楚的部分也是答辩时的核心亮点。用户下单一件免税商品订单金额为2000系统要先校验当前用户已用免税额度 本单金额是否超过年度限额。这里涉及到两个关键决策。第一个决策校验的时机。我选择提交订单时预占支付成功后确认超时关闭后释放。也就是订单从待付款开始系统就认为这笔额度被占住了。为什么不等支付成功再扣减因为高并发场景下两个订单同时提交如果都以当前已用额度做判断同时通过后有一笔支付成功、另一笔也成功就会超额。所以提交订单时就锁定额度相当于先占坑再结算。实现层我用Redis做了分布式锁的简化版本// 额度预占核心思路简化版 Transactional public boolean tryOccupyQuota(Long userId, BigDecimal orderAmount) { String lockKey quota:lock: userId; // 用setnx避免同一用户并发请求超额 Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS); if (!Boolean.TRUE.equals(locked)) { throw new BizException(操作太频繁请稍后再试); } try { BigDecimal currentUsed quotaService.getUsedQuota(userId); BigDecimal limit quotaService.getAnnualLimit(userId); if (currentUsed.add(orderAmount).compareTo(limit) 0) { throw new BizException(当前订单金额已超出个人免税额度); } // 写入额度流水状态为预占 quotaService.insertQuotaLog(userId, orderAmount, OCCUPY, orderNo); // 注意此时不会累加到“已用额度”字段而是通过预占汇总来计算 return true; } finally { redisTemplate.delete(lockKey); } }这里有个重要技巧不要把预占直接加到用户表的used_quota字段。因为你还需要支持取消订单释放额度订单关闭释放额度。如果预占和确认共用同一个累计字段释放操作就会变成扣减已用额度一旦重复释放就会导致负数对账极其痛苦。我把已用额度定义为累计确认额度预占只是写流水不更新用户汇总字段。支付成功时再把预占流水更新为确认同时给用户表的used_quota加上这笔金额订单取消或超时关闭时把预占流水标记为释放used_quota不变。这个设计带来了一个额外好处对账时可以查询预占流水有没有超过确认与释放之和逻辑非常清晰。第二决策额度不足时系统怎么处理。我的做法是如果订单金额超过剩余额度接口不直接失败而是提示超额部分需按完税商品处理是否允许拆分订单由前端提示用户选择。现实中免税购物也确实有完税与免税并行的复杂场景毕设做到提示这一步已经够讲了。3.4 购物车与结算链路防止价格不一致的细节购物车模块本身不算难但有些细节不注意会在联调时反复返工。我踩过的坑主要有四个未登录用户是否能用购物车我最终做了两个版本兼容——本地Redis临时购物车和用户ID维度数据库购物车。未登录时缓存Key为临时会话ID登录后提供合并购物车接口把临时商品并入账号。这个功能不写复杂但演示起来非常自然。加入购物车时校验商品上下架状态如果商品已下架直接提示商品已下架无法购买不要把它静默加进去。结算页重新查询价格而不是直接用购物车里的价格购物车里的价格是加入时的快照可能已有变化。结算页应该重新查一遍商品实时价并在后端校验提交结算金额 Σ商品实时单价 × 数量。允许后端重新计算总价但不能允许前端传一个总价就让后端直接改订单金额。下架或库存不足的商品在结算时给出明确提示用列表一次性返回哪些失败、失败原因是什么而不是直接在加入购物车阶段卡死。结算链路完整的调用顺序大致是确认收货地址 → 读取购物车勾选商品 → 校验上下架与库存 → 校验免税额度单件限购年度额度 → 生成订单主记录 → 生成订单明细 → 扣减库存 → 预占额度 → 清空购物车对应商品。这里订单、明细、库存扣减、额度预占必须在同一个事务里。清空购物车可以在事务外执行因为就算清空失败也不影响订单已经生成的事实下次用户还能看到勾选历史。4. 从源码到本地运行环境准备与踩坑日志4.1 本地运行需要准备哪些东西很多同学拿到免费分享的源码后第一关不是写代码而是把项目跑起来。这关卡到不少人是真的可惜因为大部分问题不在代码本身而是环境不一致。建议按这个清单准备JDK 8或11注意pom.xml里的java.version如果写的1.8就别用17跑SpringBoot早期版本和较新JDK会有兼容问题Maven 3.6并配置好阿里云镜像否则依赖下到天荒地老MySQL 5.7或8.0执行SQL脚本注意字符集选utf8mb4排序规则用utf8mb4_general_ciRedis服务确保本地6379端口可连IDEA 2020安装Lombok插件如果项目用了Lombok启动顺序也有讲究先Redis再MySQL再启动SpringBoot应用。如果控制台没有任何报错并且出现Started Application in xx seconds再用浏览器或Postman访问接口不要一上来就点前端页面的登录按钮。4.2 配置文件里最容易改错的地方application.yml是第一个要动的文件。我把自己改过的配置贴一下方便你对照server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/duty_free_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto jwt: secret: your-secret-key expire-hours: 72最容易出错的三个点我单独说一是数据库URL里务必带上serverTimezoneAsia/Shanghai不加的话MySQL驱动会报时区错误。二是Redis没启动的话应用本身可能不会挂但登录验证码、Token校验、购物车接口都会显示连接失败排查时先打开cmd敲一句redis-cli ping能返回PONG就说明Redis活着。三是JWT的secret不要写的太短至少要16个字符否则某些签名算法会启动时报错。提示如果启动报端口被占用在application.yml里换个端口即可比如8081。不要为此重装环境。4.3 拿到项目源码后的二次开发路线如果你是想把这个项目作为毕设拿到手别急着大改我建议按如下顺序做三轮处理第一轮是通读跑通启动项目把所有菜单点一遍对照数据库表搞清楚每个页面读的是哪张表。这个过程大概半天。第二轮是抽核心代码仔细读重点读订单Service、额度Service、购物车Service这三个地方把下单-支付-额度-库存的调用关系画一遍不用画得多标准自己能说明白就行。第三轮是替换痕迹把项目的默认标题、Logo、首页文案改成自己的毕业设计主题清除README里原作者的说明把数据库初始数据改成符合你论文场景的数据比如换成你即将写进论文里的商品分类。这是学术规范上的要求也是实际答辩时前的必要步骤。如果第一次在IDEA里打开项目后代码报错cannot resolve symbol多半是依赖没有下载成功。先检查Maven设置中的仓库路径再把IDEA的Maven设置为始终更新快照执行mvn clean package -DskipTests只要这一步能过启动基本没大问题。5. 我在开发和答辩过程中总结的实战经验5.1 命名与服务拆分这些不值钱但重要的事这个项目规模不难但要把代码写得能让答辩老师看得出受过训练至少要做三件事。第一Controller层的返回统一封装。很多人直接把HashMap甩给前端字段一会儿是code/message/data一会儿是status/msg/result前端联调想打人。我在common包里统一了返回类Getter public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { return new Result(200, success, data); } public static T ResultT fail(String message) { return new Result(500, message, null); } }不要觉得这个太基础。绝大数毕设的可读性不是要什么高深设计模式而是接口风格统一全局异常有人兜底。第二切面的日志不要漏。自己定义一个OperationLogger注解落到Controller方法上在AOP切面里打印方法名、入参、出参、耗时。论文里能写使用AOP实现了统一操作日志而且你演示的时候翻日志给老师看这个功能有日志记录特别加分。第三页面跳转与接口路径要规范。前后台尽量分前缀比如/api/store/**表示商城前台接口/api/admin/**表示管理后台接口。拦截器放行路径也按前缀管理别写一长串魔法路径。5.2 答辩前一定要练的三类追问我把当时被问到的问题做了汇总主要集中在三个方向提前准备一下现场不会冷场。第一类项目基础。你的系统有几种用户角色权限是怎么控制的答普通用户和管理员JWT拦截器按角色路径判断用户下单流程是怎样的余额还是模拟支付答模拟支付回调接口能记录支付流水第二类业务规则。免税额度什么时候扣减订单关闭后怎么释放重复释放怎么办答预占流水状态机区分OCCUPY/CONFIRM/RELEASE查询对账可以核验如果同一用户并发提交多个订单会超额吗答Redis分布式锁对同一用户串行化加上数据库额度累计校验作为兜底第三类技术原理。SpringBoot自动配置的原理MyBatis Plus分页插件底层是什么拦截器JWT为什么无状态Redis缓存和数据库一致性怎么解决这一块不需要你讲得跟源码分析一样深只要能把底层原理→你自己怎么用→遇到问题怎么处理的链路说清楚就算及格。5.3 后续可以怎么扩展如果你还有余力我是建议在答辩前做一两个扩展点的不用面面俱到选一个做透即可。比较简单的是首页的缓存优化。把首页分类、轮播图、热销商品缓存进Redis设置简单过期时间然后写一个页面来对比缓存命中与否的响应时间差异。另一个是后台的商品导入导出。用EasyExcel做一个Excel批量导入商品与导出订单报表的功能和管理后台天然契合工作量不大但演示效果很直观。还有一个是会员积分体系。在用户表上增加积分字段订单完成后积分累加购物车结算时可抵扣这对论文的会员模块也是一个极大补充。我个人最推荐第二个因为免税商城的商品数据相对标准Excel导入导出是日常运营需求评委不会觉得突兀。至于那些分布式事务、消息队列、前后端分离拆微服务之类非必要不要自己给自己加戏。毕设的目的是展示你能独立完成一个可运行的完整系统不是展示你会背多少中间件组件名。6. 写在最后一些关于免费分享源码的大实话标题既然说了免费分享我就多说两句关于源码使用的经验。网上这类SpringBoot商城项目的源码一抓一大把但质量参差不齐。能跑通、结构清晰、有表设计SQL、有说明文档的已经算优质资源拿到后能不能沉淀成自己的东西主要看你能不能回答清楚两个问题这个项目的核心表和核心状态有哪些你准备在答辩时介绍哪个功能点我的建议是不要试图把整个系统每一个功能都背下来那既不现实也没必要。挑一个你的系统区别于普通商城的功能重点发力——免税额度就是最好的切入点。把它的状态流转、并发控制、失败处理讲明白让导师知道你是真的理解了业务而不是照着模板改了改界面。这套思路不仅适用于免税商场题也适用于比如校园二手交易、亲子活动预约、宠物医院管理这类带特定业务规则的系统。最后再分享一个小技巧项目运行起来第一件事把数据库连接改成你自己的密码之后先跑一次后台初始化流程看看默认管理员账号是否存在不存在就手动INSERT一条。很多源码的初始SQL不会包含管理员账号到演示前一晚才发现登录不进后台的大有人在。先把这条命脉打通其他的按节奏来。希望这份梳理能帮你少走几步弯路。
返回列表