
1. 项目概述格子铺管理系统到底在管什么1.1 格子铺的业务逻辑拆解拿到SpringBoot格子铺管理系统这个毕业设计题目的时候我第一反应不是急着建工程而是先想清楚一件事格子铺这种商业模式跟普通的电商系统差在哪格子铺的核心运营模式是把一个大的实体经营空间切割成一个个独立的玻璃格子每个格子出租给不同的商户或个人用来寄售、展示和售卖商品。你在商场、步行街看到的那些摆满手办、潮玩、饰品的格子墙就是典型场景。所以这套管理系统的业务重心不是单纯的商品上架和订单处理而是格子租赁 寄售代卖 订单分账三条业务线的综合管理。我把它拆成一条完整的主链路平台方创建格子资源 → 商户申请入驻并租赁格子 → 商户在所属格子上架商品 → 顾客浏览下单 → 订单支付与确认收货 → 平台按商品归属拆分订单 → 平台抽佣后把剩余货款结算给商户。任何一个环节做漏了系统就不完整。比如只做商品管理和订单那跟普通商城没区别完全没有体现格子铺的业务特征但如果只做格子租赁又没有交易闭环答辩时很难把业务价值讲清楚。1.2 为什么这个题目适合用SpringBoot来落地选题确定之后技术选型几乎是顺理成章的事。SpringBoot现在是Java后端开发的事实标准尤其适合毕业设计这种需要快速交付、稳定演示、方便扩展的项目类型。一方面SpringBoot把Spring生态里大量繁琐的XML配置收敛成了自动装配。以前用SSM搭建一个带MyBatis的Web项目要手写spring.xml、springmvc.xml、mybatis.xml还要配置数据源、事务管理器、视图解析器光配置就能折腾大半天。SpringBoot引入几个starter依赖写一个application.yml项目骨架就立起来了。这个差距对时间紧张的毕设来说非常关键。另一方面SpringBoot内置Tomcat项目打成可执行Jar包后java -jar一条命令就能跑起来不需要额外安装部署容器。这对最后交付源码、在答辩环境快速演示来说价值巨大。很多管理系统折腾半天最后卡在跑不起来上SpringBoot可以把这种风险压到最低。还有一个隐性优势社区生态和资料丰富度。SpringBoot相关的踩坑记录、封装方案、面试题遍地都是做毕设过程中遇到问题搜一下基本都有解决方案这个有人替你踩过坑的优势比想象中重要得多。1.3 系统要服务的三类角色我设计这套系统时把用户角色拆成了三类所有功能模块都围绕角色需求展开平台运营方管理格子资源、审核商户入驻、查看整体经营数据、设置平台佣金比例。格子承租商户申请租赁格子、在自身格子内上架和管理商品、查看订单销售情况、查看待结算金额。顾客买家浏览格子与商品、加入购物车或直接下单、查看个人订单。这三类角色不是各玩各的而是通过格子这个核心资源串联起来。商户必须先租格子才能上架商品商品必须挂在某个格子上订单明细必须能反查到商品所属的格子和商户结算时才能准确地拆账。我在数据库设计和接口设计上自始至终都紧扣这条角色与业务链路避免把系统写成一堆互不相干的功能集合。2. 技术栈选型SpringBoot MyBatis Plus Vue的取舍2.1 后端框架选择SpringBoot的优势在哪后端我最终确定的是SpringBoot 2.7.x MyBatis Plus 3.5.x MySQL 8.0 Redis JWT这套组合。SpringBoot版本这里要特别说一句。网上很多教程默认带你去建最新版但做毕设我建议选2.7.x而不是3.x。SpringBoot 3.x强制要求JDK17同时部分第三方starter尤其是老的MyBatis整合方案兼容性会有问题如果你对整个生态还不够熟悉光是版本兼容问题就能消耗大量时间。2.7.x属于2.x系列里最成熟的版本JDK8就能跑资料齐全用来做毕设完全够了。当然如果你本机环境就是JDK17而且有信心踩平3.x的坑那也没问题但不要为了用新版而给自己埋雷。Redis在这个项目里主要做了两件事一是缓存格子列表、商品列表这类读多写少的数据二是在登录验证码和支付模拟流程里做短期数据存储。虽然毕设规模谈不上高并发但把缓存设计加进去答辩时可以讲清楚为什么用缓存、缓存和数据库如何保持一致这个是实打实的加分点。登录鉴权我用的是JWTJSON Web Token无状态、跨域友好。用户登录成功后后端签发一个token前端把token存在本地每次请求在请求头里带上后端通过拦截器统一校验。这样做的好处是以后想扩展小程序端、移动端后端接口几乎不用改天然支持多端接入。2.2 ORM选择为什么用MyBatis Plus而不是JPAORM框架我选MyBatis Plus而不是Spring Data JPA核心就一个原因毕设周期内MyBatis Plus的单表CRUD封装得足够省事同时又能保留手写SQL的灵活性。MyBatis Plus的BaseMapper接口直接把insert、deleteById、selectById、selectPage这些常用方法封装好了格子表、商品表、用户表这些单表操作基本不用写XML直接在Service里调用即可。但遇到多表关联查询比如查询某商户所有格子下的商品销量我还是选择手写SQL放在XML里因为这种聚合查询用MyBatis比JPA更直观SQL本身就是很好的答辩素材。需要特别强调的是MyBatis Plus 3.5.x的分页插件写法跟老版本不一样。新版本必须通过MybatisPlusInterceptor来注册分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }如果你在代码里用了老版的PaginationInterceptor在新版本中直接编译不过去。这个坑我身边不止一个同学踩过。2.3 前端方案与项目整体结构前端我用了Vue3 Element Plus Vite开发时前后端彻底分离部署时将前端打包产物放进SpringBoot的static目录最终由一个Jar包对外服务。这样既享受了前后端分离开发的高效率又避免了需要同时部署两个服务的麻烦。项目结构上我遵循了标准的Controller-Service-Mapper三层架构同时增加了dto、vo、common、config等分包src/main/java/com/example/gridstore/ ├── GridStoreApplication.java ├── common/ # 统一返回、异常处理、JWT工具、常量 ├── config/ # CORS、拦截器、MyBatis Plus配置 ├── controller/ # REST接口层 ├── service/ # 业务接口 ├── service/impl/ # 业务实现 ├── mapper/ # MyBatis Plus Mapper ├── entity/ # 数据库实体 ├── dto/ # 入参对象 └── vo/ # 出参视图对象一个容易被忽略的规范是实体类Entity不要直接暴露给前端。数据库字段和页面展示往往不一致比如商户列表需要返回商户名 当前租赁格子数 待结算金额这些聚合数据在Entity里没有我就在VO里组合。这样做还有个好处数据库表结构变动时只要调整VO或者Service层的转换逻辑前端接口格式可以保持不变。3. 数据库建模格子、商户、商品、订单、结算的关联设计3.1 核心表结构拆解格子铺系统的数据库表我建了十几张但真正可以说清楚业务逻辑的是下面这几张核心表表名核心字段作用说明userid、username、password、phone、role系统用户role区分管理员/商户/顾客gridid、grid_no、location、spec、rent_price、status、current_merchant_id格子资源状态区分空闲/在租/停用lease_recordid、grid_id、merchant_id、start_time、end_time、rent_amount、status格子租赁历史记录支持到期自动释放productid、grid_id、merchant_id、name、description、image、price、stock、status商品信息挂在具体格子上merchant_infoid、user_id、shop_name、contact、audit_status商户入驻资料与审核状态orderid、order_no、buyer_id、total_amount、pay_status、order_status订单主表覆盖多商户多格子场景order_itemid、order_no、grid_id、merchant_id、product_id、amount订单明细记录商品归属settlement_recordid、order_no、merchant_id、product_amount、commission、income、status结算记录平台抽佣与商户实收这里有一个关键设计格子和商户不是简单的多对多而是一对多关系——一个格子同一时刻只能被一个商户租赁但一个商户可以租多个格子。为了保留历史租赁轨迹我额外加了lease_record表这样维护人员可以看到某格子从开业到现在被谁租过、租金分别是多少而不是只能看到当前状态。3.2 订单与分账的数据链路格子铺的订单跟普通商城订单最大的区别在于一个订单可能同时包含多个商户、多个格子的商品。顾客可能先在一个格子买了一只手办又去隔壁格子买了一盒盲盒最后一起结算生成一个订单号。所以订单表我拆成了order主表和order_item明细表。主表只管买家、总金额、整体订单状态明细表记录每一件商品来自哪个格子、哪个商户、金额多少。这样设计的好处在结算环节体现得最明显。当订单状态流转到已完成时系统遍历订单明细按明细里记录的merchant_id分组结合平台设置的佣金比例为每个商户生成一条结算记录。由于明细表里已经保存了格子ID和商户ID不再需要反向关联查询结算逻辑既简单又不会出错。3.3 金额字段的精度设计金额处理是这种带分账逻辑的系统最容易翻车的地方。我的原则很简单数据库里所有涉及金额的字段一律用decimal(10,2)Java代码里统一用BigDecimal接收和运算绝对不用double或float。原因很基础浮点数在二进制中无法精确表示0.1加0.2不等于0.3这种问题在金额计算中一旦出现就会导致账目不平。而decimal配合BigDecimal能保证精度可预期。关于存储单位我最后选择了存元而不是存分。毕设项目演示时直接看数据库表里显示的是价格数字而不是一堆零更直观如果做真实商业项目我反而建议用分或更小单位存储避免累积精度误差。另外所有金额计算后面都要加setScale(2, RoundingMode.HALF_UP)把四舍五入的规则显式写出来不要依赖默认行为。4. 核心功能实现与关键代码4.1 格子租赁流程与定时任务格子租赁的完整流程是管理员在后台添加格子资源 → 格子处于空闲状态 → 商户提交租赁申请 → 管理员审核 → 审核通过后生成租赁记录、格子状态变为在租 → 租期到期后自动释放格子并下架商品。这里我用了两张表来支撑lease_record记录每次租赁的起止时间和租金grid表的status字段只表示当前状态。审核动作操作的是申请状态通过后再去更新格子状态这样申请记录完整保留方便追溯和拒绝原因查看。格子到期后的自动释放我用了SpringBoot自带的Scheduled定时任务每小时扫描一次在租记录Component public class LeaseExpireTask { Resource private LeaseRecordMapper leaseRecordMapper; Resource private GridMapper gridMapper; Resource private ProductMapper productMapper; Scheduled(cron 0 0 * * * ?) public void expireLease() { ListLeaseRecord expiredList leaseRecordMapper.selectList( new LambdaQueryWrapperLeaseRecord() .eq(LeaseRecord::getStatus, 1) .lt(LeaseRecord::getEndTime, new Date()) ); for (LeaseRecord record : expiredList) { record.setStatus(2); leaseRecordMapper.updateById(record); gridMapper.update(null, new LambdaUpdateWrapperGrid() .eq(Grid::getId, record.getGridId()) .set(Grid::getStatus, 0)); productMapper.update(null, new LambdaUpdateWrapperProduct() .eq(Product::getGridId, record.getGridId()) .set(Product::getStatus, 0)); } } }注意状态更新我用LambdaUpdateWrapper直接生成Update语句而不是先查出实体再修改这样省了一次查询。定时任务这部分的日志要打清楚答辩演示时可以直接展示格子到期后状态自动回滚、商品自动下架的效果这是系统业务深度的体现。4.2 商品上架与订单状态机商品上架接口看起来是普通CRUD但有一个细节必须处理商户只能在自己租赁的格子里上架商品。所以在商品新增的Service层里第一件事不是插入记录而是校验当前登录用户的商户身份再校验他要挂载的格子是否属于自己。这一步如果只在前端页面做后端接口不校验商户A完全可以拿着商户B的格子ID直接上架商品这是实际开发中很容易被忽视的越权漏洞。订单流程我采用模拟支付 状态机驱动的方式。由于毕设不接入真实支付渠道我实现了一个mockPayment接口订单创建后状态为待支付前端调用模拟支付接口后端把订单状态置为已支付然后执行库存扣减。订单状态的流转必须严格控制。我在Service层里判断每个状态的合法跳转已支付订单可以取消但一旦商户发货买家就不能直接取消订单完成后才能进入结算流程。这些状态机逻辑写在一处不要散落到Controller里否则后面排查问题会非常痛苦。4.3 基于订单明细的自动分账分账结算是格子铺系统的核心亮点。我的设计是订单状态变为已完成时系统自动遍历该订单的所有明细按明细中记录的merchant_id聚合乘以平台佣金比例生成结算记录。佣金比例我放在平台配置表中管理员可以在后台自由修改而不是写死在代码里。这样做不只是为了灵活性更重要的是答辩演示效果你在界面上把佣金比例从10%改成5%新订单的结算结果立刻变化这个动态演示比静态参数有说服力得多。public void settleOrder(String orderNo) { Order order orderService.getByOrderNo(orderNo); ListOrderItem items orderItemMapper.selectList( new LambdaQueryWrapperOrderItem().eq(OrderItem::getOrderNo, orderNo)); for (OrderItem item : items) { BigDecimal commission item.getAmount().multiply(platformConfig.getCommissionRate()) .setScale(2, RoundingMode.HALF_UP); BigDecimal merchantIncome item.getAmount().subtract(commission); settlementMapper.insert(new SettlementRecord( orderNo, item.getMerchantId(), item.getAmount(), commission, merchantIncome, 0)); } }分账这里有一个很隐蔽的坑如果每笔商品佣金分别四舍五入再相加跟先相加再四舍五入结果可能差一分钱。为了保证商品原金额 平台佣金 商户实收这个等式恒成立我把商户实收改为原金额 - 佣金而不是把两笔独立计算的结果相加。这样每条明细都是平的对账时不至于出现1分钱差额。4.4 接口权限与统一返回封装后端接口我用RESTful风格返回结构统一用Result对象包裹前端只用判断code是否为200不用为每种错误单独写处理逻辑。public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } }参数校验统一用javax.validation注解加NotNull、NotBlank、DecimalMin等Controller层不需要写一堆if判断。全局异常处理器捕获校验异常和业务异常返回标准错误结构同时用日志记录堆栈。权限控制我用了自定义注解RequireRole加拦截器的方式。管理员接口标注RequireRole(ADMIN)商户接口标注RequireRole(MERCHANT)拦截器解析JWT里的角色字段做判断。这比在每个方法里手写角色判断简洁得多也方便后续增加角色权限。5. 实操全记录从初始化到打包部署5.1 项目初始化与依赖版本选择我用IDEA自带的Spring Initializr创建项目Java版本选JDK8SpringBoot版本选2.7.18。这里强调一下创建时不要贪新选SpringBoot 3.x除非你明确知道自己在做什么。pom.xml里的核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml里最关键的配置是数据源和MyBatis Plus的驼峰映射开关spring: datasource: url: jdbc:mysql://localhost:3306/grid_store?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto5.2 前端Vue如何打包进SpringBoot前端我用Vite开发本地开发通过代理把接口请求转发到后端避免跨域问题// vite.config.js server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }前端联调没问题后执行npm run build把生成的dist目录里的所有文件复制到SpringBoot项目的src/main/resources/static下。后端接口路径统一以/api开头这样部署后前端页面与接口同源不需要任何跨域配置。有一个细节要注意如果前端用了history路由模式部署后刷新某个深层级页面比如/admin/grids会404因为SpringBoot并没有为这个路径配置MVC路由。我直接让前端改成hash模式URL里带#刷新不会出问题。如果想保留history模式后端需要额外配置转发规则把非API路径都转发到index.html成本不大但容易被忽略。5.3 部署配置与运行验证本地和远程环境的连接配置不一样我用application-prod.yml放生产配置通过spring.profiles.activeprod切换。打包发布用mvn clean package -DskipTests生成的可执行Jar包在服务器上一条命令运行java -jar gridstore-0.0.1-SNAPSHOT.jar部署时最容易跳过的坑是数据库地址。本地连localhost没问题放到另一台服务器或云主机上跑一定要把application-prod.yml里spring.datasource.url改成目标数据库实际可达的地址。如果数据库和Jar包在同一台机器用localhost可以数据库在另一台机器就必须用内网IP否则连接超时。6. 踩坑实录版本冲突、跨域拦截、分账对账的修复过程6.1 SpringBoot版本过高引发的循环依赖报错SpringBoot 2.6开始官方默认禁止循环依赖。如果你的Service层出现A依赖B、B又依赖A的情况启动时会直接报错提示The dependencies of some of the beans in the application context form a cycle。老版本SpringBoot是不会报这个错的所以很多老教程里的代码搬到新版就启动失败。我的处理建议是优先重构代码而不是绕开关闭检查。把相互依赖的逻辑抽到第三个Service或者直接重新设计调用方向循环依赖本身就是设计坏味道答辩时解释重构思路反而加分。如果临时调试想快速跑起来可以在某个Bean上加Lazy注解但这不是治本方案。6.2 跨域配置与JWT拦截器的冲突如果你是前后端分离部署前端一个端口、后端一个端口跨域问题就跑不掉。我刚开始加了CorsConfig后发现前端带token的请求依然报跨域错误排查半天才意识到跨域请求会先发一个OPTIONS预检请求而这个预检请求被我的JWT拦截器拦住了根本没到达CORS处理链。解决方式很简单在拦截器里放行OPTIONS请求public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 这里再执行token校验 return true; } }另外注意CorsConfig里不要用allowedOrigins(*)在SpringBoot 2.7里推荐用allowedOriginPatterns(*)否则带凭证的跨域请求会被拒绝。6.3 MyBatis Plus的分页陷阱分页查询失效的原因九成是没配置分页插件这个前面提过了。还有一个隐蔽问题如果你在XML里手写了多表关联查询返回的实体类里某些字段总是为空大概率是查询结果的列名和实体属性映射不上。比如SQL里写了p.product_name而实体属性是productName虽然MyBatis Plus默认开启驼峰映射但这个映射只对MyBatis自动生成的SQL生效手写XML需要显式给列名起别名AS productName或者在application.yml里确认map-underscore-to-camel-case: true确实是开的。多表查询时养成写别名的习惯能省很多排查时间。6.4 金额精度导致账目不平的修复金额计算如果不用BigDecimal和setScale显式指定舍入模式很容易在累积计算中出现1分钱误差。我实际修过的一个场景是订单里三件商品每件的佣金都是amount * 0.08三件各自四舍五入到两位再加总跟总金额直接乘0.08再四舍五入差了0.01。这个差值单独看微不足道但对账脚本一旦报出差额不等于0问题就被放大了。我的修复策略是保证等式恒等每条明细里佣金和商户实收只算一个另一个用减法推导。这样无论怎么舍入商品金额等于两者之和账是平的。6.5 前端刷新404与静态资源目录问题前端打包后放进SpringBoot有两个404相关的坑。第一个是history路由刷新404解决办法前面提过要么用hash模式要么后端转发。第二个是静态资源目录放错位置。SpringBoot默认只从classpath:/static/读取静态文件如果你把dist里的文件放到了src/main/resources的根目录或者templates目录页面访问时就会404。正确的做法是放到src/main/resources/static/下确认重建后Jar包里能看到对应的静态文件。还有一个细节如果部署在Tomcat的webapps子目录里前端资源引用的绝对路径/assets/xxx.js会因为上下文路径而失效。我们自己用Jar包启动不存在这个问题但如果被人拿到源码后用war包部署到Tomcat就可能遇到源码交付时最好注明推荐用Jar包方式运行。7. 功能扩展方向与个人体会7.1 值得加的功能模块基础功能跑通之后有几个高性价比的扩展方向。第一个是数据可视化用ECharts在管理员后台加几个图表展示格子出租率、各商户销售额排行、订单量趋势整个系统立刻有了经营看板的感觉答辩演示冲击力强很多。第二个是消息通知可以利用Scheduled定时任务扫描即将到期的租赁记录生成站内信或模拟短信通知商户续租这个功能跟格子租赁业务结合得非常自然。第三个是支付对接如果时间允许用支付宝沙箱环境把模拟支付替换成真实支付流程项目的完整度和说服力会明显提升。底层架构上由于后端已经统一走JWT REST接口未来不管加小程序端还是移动端后端几乎不需要改动只需要新增前端客户端这也是当初坚持前后端分离设计带来的红利。7.2 我做这套系统的三点体会第一个体会也是最深的教训不要一拿到题目就建工程写代码。我最初急于搭框架业务链路没完全想清楚就建了表结果做到分账环节发现订单表和商品表的关联设计不合理回头改表结构、改接口、改前端返工的代价非常大。后来我重新把格子-商户-商品-订单-结算的完整时序画清楚确认每个状态流转和字段归属后才动手后续开发顺畅了很多。第二个体会是毕设项目除了能用还要能讲。我在平台配置里故意设计了可配置的佣金比例在定时任务里打印了格子到期自动释放的日志在分账逻辑里保留了完整的结算记录这些设计不只是业务需要更是答辩时的演示素材。把一个业务规则的动态变化展示出来比空口说我做了个管理系统有说服力得多。第三个体会是源码交付要替对方考虑。题目写了附源码那就不是简单丢一个代码压缩包。我把生产环境的数据库连接改成占位符或本地默认配置删掉了本地测试产生的脏数据写清楚README里的启动步骤、数据库初始化脚本、默认账号密码。拿到源码的人能顺利跑起来这个项目才算真正交付完成。我在实际整理源码包时还会顺手检查一下有没有把本地环境的绝对路径写进配置文件这类细节容易遗漏但影响很大。这套SpringBoot格子铺管理系统做完最大的收获不是熟悉了几个注解和框架而是完整走了一遍从业务分析、表结构设计、接口开发到部署交付的闭环。希望这篇整理出来的思路和踩坑记录能给正在做同类项目的同学一些实际参考。