首页 / 资讯中心 / 文章详情

SpringBoot水族馆商品销售管理系统实战:从技术选型到答辩避坑

SpringBoot水族馆商品销售管理系统实战:从技术选型到答辩避坑 ★ FEATURED ARTICLE
SpringBoot做水族馆商品销售管理系统这个选题我在接私活和带新人时见过的次数不少。表面看是个常规的CRUD项目实际上如果真把它当成“增删改查”来做答辩时很容易被老师几个追问就卡住。水族馆这个场景有个特有难点活体商品和普通商品的库存逻辑完全不同这两者混在一个系统里才是这个项目的真正价值所在。这篇文章我按自己的实战经验把这个项目的选题逻辑、技术选型、核心模块拆解、数据库设计、部署要点和答辩避坑一次性讲透。源码和文档可以直接用但更希望你看完知道每一行配置为什么这么写每一个表为什么这么建。1. 项目到底在做什么先搞清楚需求边界很多同学拿到题目第一反应是“不就是一个商城系统吗”然后套个通用的电商模板就开始写。真这么干最后做出来的东西和题目完全对不上。水族馆商品销售与经营管理系统重点在“经营”二字不是简单的下单收款。经营管理系统和普通商城系统的核心差异在于商城系统只关心前台交易而经营管理系统还要把库存、供应商、进货价、销售数据、过期损耗这些链路全部打通。举个具体例子普通商城卖衣服库存数量错了无非是超卖一件但水族馆卖活体观赏鱼库存错了可能是整批次鱼的耗损统计失真直接影响进货决策。这就是这个项目和通用电商模板的本质区别。从需求角度拆解这个系统至少要覆盖四个核心对象商品管理分活体和非活体两类。活体商品要有批次、存活状态、入缸时间这些特殊字段。销售订单包含线上预定和门店收银两类场景。库存与进货活体商品要按批次管理非活体按SKU管理。经营看板按月统计销售额、毛利率、热销品类、损耗率。如果你是学生做毕设建议把线上支付那一套省掉。微信支付和支付宝的商户资质、回调流程、证书配置一个月都未必能跑通而且答辩时老师更关注你的业务逻辑而不是支付对接。系统内保留“现金收款”“会员储值扣款”两种方式就足够了既避开支付资质问题又能把订单流程完整跑通。2. 技术选型为什么用SpringBoot而非别的方案选型这一块我的建议一直都是毕设不要追新要追稳。SpringBoot 2.x MyBatis-Plus MySQL这套组合在本科毕设里是绝对的稳妥主流网上资料多出问题能搜到解决方案老师看了也认可。2.1 SpringBoot 2.7 的优势SpringBoot的核心价值就一句话约定优于配置。以前用SSH或SSM框架光配置XML就要写几百行SpringBoot把大部分配置自动化了你只需要关注业务代码本身。以本项目为例你引入web、mybatis-plus、mysql这几个starter之后自动配置机制会帮你完成SpringMVC的初始化、数据源的自动装配、事务管理器注册这些底层工作。你写的核心代码只有Controller、Service、Mapper三层项目的百分之八十工作量都集中在业务逻辑上这正是毕设该有的样子。需要注意SpringBoot 3.x已经发布但如果是新手做毕设建议选2.7.x。原因有两个3.x基于Jakarta EE规范很多老教程和现成代码不兼容3.x要求JDK17起步很多学校实验室机器还停留在JDK8。为了兼容性2.7 JDK8是最保险的组合。2.2 MyBatis-Plus 让数据操作更高效MyBatis-Plus不是替代MyBatis而是在MyBatis之上提供了一套CRUD的现成实现。比如你定义一个实体类加上注解标注表名和主键策略就能直接调用selectPage()、insert()这些方法多表查询时再手写SQL灵活性比JPA高开发效率比纯MyBatis快很多。用MyBatis-Plus还有个实用价值它自带分页插件。经营报表、订单列表、商品列表这些都是分页展示的场景用内置分页插件几行代码就能搞定不需要手写LIMIT。2.3 前端方案的选择逻辑如果你自己不擅长写前端不要硬上Vue Element UI 前后端分离那会引入跨域、Token鉴权、路由守卫一大堆额外问题。我的建议是直接用服务端渲染用Thymeleaf模板引擎 Bootstrap 原生JavaScript。这个组合的好处是不需要处理跨域不需要配Nginx一个SpringBoot应用打包后直接跑起来就是完整系统。如果老师明确要求前后端分离那也别慌用Vue3 Axios Element Plus后端统一返回JSON用拦截器处理登录态。后面我会单独说接口鉴权的设计。2.4 环境版本锁定为了避免版本冲突直接照这个组合来JDK 1.8Maven 3.6.3MySQL 5.7 或 8.05.7更稳SpringBoot 2.7.xMyBatis-Plus 3.5.x注意MySQL 8.0以上版本需要额外指定时区参数连接串里加serverTimezoneAsia/Shanghai否则启动时大概率报时区错误。这个报错在答辩前出现会非常尴尬提前配好。3. 核心模块拆解这些功能才是项目的灵魂下面把系统按功能模块拆开讲每个模块我都会说清楚做什么、怎么做、注意什么。这部分建议对着文档自己敲一遍代码理解会深很多。3.1 登录与权限分清两类用户的边界系统的用户角色要设计成三种管理员、店员、顾客。管理员管全店店员管日常销售和库存操作顾客只能在小程序或H5端浏览商品和下单。权限控制最简单可靠的做法是拦截器 用户角色字段public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }在WebMvcConfig里注册拦截器并配置放行路径Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /logout, /css/**, /js/**, /images/**); }这样做的好处是逻辑直观对毕设来说足够用到答辩。如果做前后端分离版本就用JWT 拦截器校验Token思路是一样的只是把session换成了token。3.2 商品管理活体与非活体的差异化设计这是水族馆项目最大的亮点也是和普通商城系统区分开的关键模块建议花最多心思在这块。活体商品观赏鱼、虾、龟不能用普通商品的SKU模型因为它们有几个特殊性有生命状态可能死亡损耗有不同的批次进货批次间品质和进价不同观赏鱼品类还有尺寸区间同样是龙鱼15cm和30cm是两个价格带。我的表设计建议是活体商品单独一张表关联批次表CREATE TABLE live_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(100) NOT NULL, category VARCHAR(50), base_price DECIMAL(10,2), sell_price DECIMAL(10,2), stock_batch_id BIGINT, status TINYINT DEFAULT 0 COMMENT 0在售 1缺货 2下架 ); CREATE TABLE stock_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT, batch_no VARCHAR(50) NOT NULL, quantity INT, quantity_left INT, unit_cost DECIMAL(10,2), entry_date DATE, death_loss INT DEFAULT 0 );quantity_left和death_loss这两个字段很关键每次有活体死亡时记录到death_loss同时更新quantity_left这样定期盘点损耗率和存活率时数据是准的这也是答辩时能讲出的亮点。非活体商品鱼粮、造景石、过滤器、鱼缸就按普通商品模型来沿用通用的product表即可字段包含SKU、库存、进价、售价、预警值。两者都保留一个category字段方便前端做分类展示。3.3 订单销售把“线下收银”和“线上订单”统一管理订单模块的核心是保证事务一致性。用户下单后要同时扣库存、生成订单、计算利润这三步必须在一个事务里完成否则就会出现库存扣了订单没生成或者订单生成了库存没扣的情况。这里有个常见的坑事务方法内部调自己类里的另一个方法事务会失效。因为Spring的事务是基于代理的同类内部调用不走代理。要保证事务生效必须把事务方法放在独立的Service类中从Controller调用。Transactional(rollbackFor Exception.class) public Order createOrder(OrderRequest request) { // 参数校验 // 扣减库存 // 生成订单 // 记录销售明细 // 返回订单 }rollbackFor Exception.class这个参数非常关键默认情况下Spring事务只在遇到RuntimeException时回滚普通Exception默认不回滚。如果你只写Transactional遇到异常时数据就悬空了。这是新手高频出错点值得重点记。线上订单和线下收银的区别在于支付状态线下收银支付状态直接为已支付线上订单先为待支付模拟一个“到店自提免支付”的逻辑即可。订单状态流转建议设计为待支付 → 已支付/待提货 → 已完成 → 已取消配一个order_status字段管理。3.4 会员与营销不要做得太复杂会员模块做一个简单的等级制就够了储值卡、会员折扣、积分抵扣。核心表就一张member表和一张member_transaction表。会员打折的逻辑要注意是“在商品售价基础上打折”不是“在会员价基础上打折”。有的同学实现会员价直接在product表里加了个member_price字段这样做也没错但要注意和管理员修改售价的联动关系。建议售价统一用sell_price会员折扣率单独一张表配置避免改价时两边不同步。3.5 经营报表用最小的成本做出最大的价值报表模块是拉开普通项目与优秀项目的分水岭。不需要复杂的BI系统用ECharts画几个核心图表就行近7天销售趋势折线图本月品类销售占比饼图商品销售排行柱状图活体损耗率统计表格实现思路就是写SQL聚合查询。比如近7天销售额SELECT DATE(create_time) AS order_date, SUM(total_amount) AS amount FROM orders WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY order_date;注意SQL里的DATE_SUB(CURDATE(), INTERVAL 7 DAY)写法它会自动计算最近7天的数据不需要你在Java代码里手动拼接日期范围。ECharts通过Thymeleaf模板渲染数据用ThymeleafViewResolver把后端计算好的JSON数据置入页面这一块工作量不大但效果非常直观答辩演示时最有说服力。4. 数据库设计一张好的表结构决定了开发顺畅度数据库设计这个环节我见过太多人了拿到题目就开始建表结果建到一半推翻重来。花半小时把表结构理清楚能省下后面几天的返工时间。4.1 核心表清单表名说明核心字段user用户表id, username, password, role, phone, create_timemember会员表id, name, phone, balance, level, discount_rateproduct商品表非活体id, name, category, sku, stock, cost_price, sell_price, statuslive_product活体商品表id, name, category, sell_price, batch_id, statusstock_batch批次表id, product_id, batch_no, quantity, quantity_left, unit_cost, death_lossorders订单主表id, order_no, user_id, member_id, total_amount, discount_amount, pay_status, order_statusorder_item订单明细表id, order_id, product_type, product_id, product_name, price, quantitysupplier供应商表id, name, contact, phone, addresspurchase_order进货单表id, supplier_id, total_amount, create_time, statuspurchase_item进货明细表id, purchase_id, product_type, product_id, quantity, cost_price这些表字段数量不多但关系清晰能满足题目要求的“商品销售与经营管理”的全部能力。顺序上建议先建基础表用户、会员、商品、供应商再建关联表订单、进货单、明细表避免外键引用不存在的表。4.2 金额字段设计的关键用DECIMAL不用FLOAT金额字段一定用DECIMAL(10,2)不要用FLOAT或DOUBLE。原因很简单浮点数在计算机中存储是不精确的0.10.2在二进制下会得到0.30000000000000004这样的值。涉及金额累加时用浮点数计算累计下来可能差出几毛几分财务对账时就是大问题。DECIMAL是定点数在数据库层面就能保证精度。同一张表内进价cost_price、售价sell_price、折扣后价格pay_amount这些字段全部用DECIMAL(10,2)在Java实体类里对应BigDecimal类型。涉及金额运算时也统一用BigDecimal的add、multiply方法不要用double去乘。4.3 逻辑删除和外键使用建议表结构里不建议加物理外键约束原因是物理外键会带来插入顺序、更新删除的强约束成本。比如你要删除一个分类而这个分类下还有商品物理外键会直接拦住你让你先删商品操作上很不灵活。开发阶段用Java代码层面维护数据一致性表设计上保持逻辑关联就够了。对于商品删除这个动作建议用逻辑删除也就是加一个deleted字段标识0/1而不是物理删除。这样历史订单明细里还能引用到商品名称报表统计时不会因为删了商品导致数据缺失。MyBatis-Plus内置了TableLogic注解加了注解后默认的deleteById会自动变成update ... set deleted1不用自己改代码。5. 核心功能实现从Controller到Mapper的完整链路这一节把一条核心链路完整走一遍从Controller到Service到Mapper你会看到每一层到底干什么。就取“创建订单并扣库存”这个最核心的流程来拆。5.1 Controller层参数接收和权限控制RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) public Result createOrder(RequestBody OrderRequest request, HttpSession session) { Long userId (Long) session.getAttribute(userId); Order order orderService.createOrder(request, userId); return Result.success(order); } }Controller层只做三件事接参数、调Service、返回结果。不要在Controller里写业务判断比如“判断库存够不够”应该放在Service层因为Controller可能被多个入口调用PC端、收银端、管理端业务规则保持一致。Result是一个统一返回体包含code、message、data三个字段前端根据code判断业务成功还是失败。定义为泛型类方便复用。5.2 Service层事务、校验、业务规则Service public class OrderServiceImpl implements OrderService { Autowired private ProductMapper productMapper; Autowired private LiveProductMapper liveProductMapper; Autowired private OrderMapper orderMapper; Override Transactional(rollbackFor Exception.class) public Order createOrder(OrderRequest request, Long userId) { BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); String orderNo generateOrderNo(); for (OrderItemRequest itemReq : request.getItems()) { if (itemReq.getProductType().equals(NORMAL)) { Product product productMapper.selectById(itemReq.getProductId()); if (product.getStock() itemReq.getQuantity()) { throw new BusinessException(商品 product.getName() 库存不足); } product.setStock(product.getStock() - itemReq.getQuantity()); productMapper.updateById(product); OrderItem item new OrderItem(); item.setOrderId(orderId); item.setProductType(NORMAL); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setPrice(product.getSellPrice()); item.setQuantity(itemReq.getQuantity()); orderItems.add(item); totalAmount totalAmount.add(product.getSellPrice().multiply(BigDecimal.valueOf(itemReq.getQuantity()))); } } // 生成订单主表记录 // 批量插入订单明细 return order; } }注意几个细节订单号orderNo建议用时间戳随机数生成格式如20250520143012001避免使用数据库自增ID当订单号否则打印出小票容易被猜出一天的交易笔数。校验库存时用的是selectById查出来的对象检查库存后先update再insert。顺序上先更新库存再插入订单明细如果后续插入失败事务回滚库存也会自动回滚。活体商品的扣减逻辑不同不是简单的库存减一而是批次余量减一如果该批次余量耗尽自动从最新批次补货。这个逻辑可以单独写一个方法。5.3 Mapper层核心SQL怎么写简单的单表操作直接用MyBatis-Plus的BaseMapper不用写XML。复杂查询在XML里写SQL比如报表查询select idselectSalesTrend resultTypejava.util.Map SELECT DATE(create_time) AS date, SUM(total_amount) AS amount FROM orders WHERE create_time gt; DATE_SUB(CURDATE(), INTERVAL #{days} DAY) AND order_status ! CANCELLED GROUP BY DATE(create_time) ORDER BY date /select注意XML里小于号和大于号需要转义写成lt;写成gt;否则XML解析直接报错。这个坑几乎每个写MyBatis的人都会踩一次。5.4 给订单加锁防止并发超卖单机部署的毕设项目不需要引入Redis分布式锁但需要在数据库层面做唯一的约束保护。最简单可靠的办法是在product表更新库存时加条件int updateCount productMapper.updateStockById(id, quantity); if (updateCount 0) { throw new BusinessException(商品库存不足); }对应的SQL是UPDATE product SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity};这样是数据库层面的原子操作天然防止并发超卖。如果只是“先查库存在Java里判断再执行更新”两个请求同时查到库存为1都能通过判断最后都执行更新库存就变成-1了。加上stock #{quantity}条件更新能直接影响行数判定更新行数为0就知道库存不够了。6. 项目部署与调试环境搭建、初始化数据、常见启动报错部署这块一定要提前演练尤其是答辩现场用自己电脑演示的结合我平时帮人排查问题的经验这里单拎出来讲。6.1 本地运行完整步骤第一步准备环境。JDK1.8、Maven 3.6、MySQL 5.7、IDEA或Eclipse全部装好。第二步初始化数据库。用Navicat或命令行执行项目里的sql/init.sql文件。初始化脚本里应该包含所有表的建表语句和基础数据比如管理员账号、初始商品分类、几只演示用的活体批次。注意如果初始化脚本里出现中文乱码检查MySQL安装时是否选了utf8mb4字符集连接串里也加上useUnicodetruecharacterEncodingutf8。乱码问题在答辩现场出现过无数回数据全建好了但界面上全是问号现场越着急越修不好。第三步修改配置文件。打开application.yml把数据库URL、用户名、密码改成你自己的spring: datasource: url: jdbc:mysql://localhost:3306/aquarium?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456这里的密码换成你本地MySQL的实际密码不要直接抄文档里的默认值。很多同学第一步启动报连接失败十有八九是用户名密码没对上。第四步启动项目。IDEA里运行AquariumApplication.java的main方法看到Tomcat started on port(s): 8080这样的日志说明启动成功。打开浏览器访问http://localhost:8080默认账号密码在初始化脚本里写死比如admin/123456。6.2 启动报错的快速定位思路启动报错是新手最慌的事但报错是有规律可循的。这里按出现频率排一下端口被占用。报错里出现Port 8080 was already in use说明8080端口被其他程序占用。命令行里输入netstat -ano | findstr 8080找到PID后到任务管理器结束进程或者直接把配置文件的端口改成8081、8082都行。数据库连接失败。报错里出现Access denied for user说明用户名密码不对出现Unknown database说明数据库名不一致出现Communications link failure说明MySQL没启动或端口不对。思路是挨个排查应用配置和MySQL状态。Bean创建失败。报错里出现Error creating bean with name xxxMapper大概率是XML文件位置没放对或Mapper接口没加Mapper注解。MyBatis-Plus的Mapper接口需要在启动类上加MapperScan(com.example.mapper)或者在每个Mapper接口上加Mapper。两个方式选了就别混用。Maven依赖下载失败。如果你看到Cannot resolve symbol SpringBootApplication或下载依赖时超时检查Maven仓库镜像配置。在settings.xml里配置了阿里云镜像速度会快很多。这不是代码问题但恰恰是环境里卡住最多人的环节。6.3 给项目补充演示数据初始化数据不要只建一个空库一定要有演示数据。评委打开系统一片空列表无法直观判断系统功能效果不好。建议手动插入这些数据10个以上商品涵盖活体小鱼、中型鱼、鱼粮、过滤器、造景石5个以上分类3个批次以上位于不同状态1个正常售卖、1个有部分损耗、1个已售罄近30天模拟订单保证报表模块图表有数据可画3~5个会员有些有余额有些有积分记录模拟订单数据可以用SQL脚本批量生成写法很简单用DATE_SUB生成不同日期的数据用FLOOR(RAND()*100)生成随机数量这样图表展示时效果好很多。7. 常见问题与排查技巧实录这一节是实战中积累的教训按类整理一下遇到问题照着查。7.1 前端传值问题问题描述Controller里接收的实体对象属性为null页面提交的数据存不进去。排查思路大部分是前后端字段名对不上。比如实体类是productName前端表单写的nameSpringMVC无法自动绑定。用RequestBody接收JSON时确认前端传的key与后端字段一致。Thymeleaf模板页面直接绑定的确保th:object和th:field的路径正确。7.2 事务失效问题问题描述代码里加了Transactional但操作出错后数据没回滚库存少了订单没生成。排查思路第一步看异常类型是不是普通的Exception而不是RuntimeException如果是Transactional默认不会回滚必须加rollbackFor Exception.class。第二步看调用方式如果事务方法被同类中的其他方法直接调用事务会失效把事务方法抽到独立Service再调用。7.3 分页查不出来数据问题描述列表页调用selectPage返回总数为0但数据库里明明有数据。排查思路多数是没配置MyBatis-Plus的分页插件。MyBatis-Plus的分页功能不是默认开启的需要配置PaginationInnerInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个BeanselectPage会执行不带LIMIT的查询分页结果自然不对。7.4 密码明文存储问题虽然毕设项目做登录但密码明文存储会在答辩时被老师一眼抓住。用Spring Security的BCryptPasswordEncoder做密码加密String encodedPassword new BCryptPasswordEncoder().encode(rawPassword); boolean matches new BCryptPasswordEncoder().matches(rawPassword, encodedPassword);数据库里存的是加盐哈希后的字符串即使数据库泄露也不会直接暴露账号密码。教学项目这一点做不做影响不太大但做了答辩时多一个加分点。7.5 上传图片类文件问题商品需要展示图片的话用文件上传功能。本地保存到upload/目录把路径存数据库前端用img标签的src指向这个路径。注意Windows和Linux路径分隔符不同统一用相对路径存。如果部署到服务器把上传目录配置到应用外的独立路径不要放在jar包内部打包后是只读的运行时无法写入。8. 给准备答辩的你几点掏心窝的建议项目写完后答辩不只是看效果更看你的理解和表达。有几点经验值得单独分享不要把“这是我自己写的”当挡箭牌。老师问你怎么实现的功能答不上来就很被动。每个核心功能的代码请你至少能画出Service层调用链说清楚用到了哪些关键注解。主动引导话题。开场演示时先说“系统有三个特色活体批次管理、经营报表、会员折扣体系”老师大概率沿着你展示的方向提问而这三个方向你都做好了准备。技术栈的每个选型都准备一个理由。为什么用SpringBoot而不是SSM为什么用MyBatis-Plus而不是JPA这些问题不一定要长篇大论但至少要能从“开发效率”和“生态成熟度”两个角度给出回答。提前准备一个生产环境部署记录。老师问“项目部署到哪了”的时候你拿出一段操作记录云服务器部署、域名访问、日常运行会让他觉得你有工程化意识而不只是一个会写Demo的学生。9. 项目扩展从毕设到完整产品的升级路径答辩结束后如果希望这段经历变成能写进简历的项目经验可以从几个方向做真实升级对接真实支付。现在很多第三方支付网关支持个人开发者的模拟支付沙箱不需要企业资质也能完成完整支付流程的调试。引入消息队列。订单创建成功后发送一条延迟消息用于自动关闭超时未支付订单这个逻辑用RabbitMQ或RocketMQ实现产线级功能且面试常被问。库存管理升级。非活体商品接入低库存预警短信通知活体商品增加按月损耗率趋势图这个功能是经营场景的刚需商业价值高。使用Redis做热销排行榜。真实的经营活动里热销排行数据人人都在看用定时任务或缓存提高性能有复杂度也容易在面试中讲出亮点。这个项目的价值在于“业务场景真实技术栈主流”。哪怕你后面不做水族馆行业换成宠物店的活体销售、花店的鲜花损耗管理核心模型都通用。把一个项目吃透比浮光掠影地看十个项目强得多。如果你在调试过程中遇到什么问题把报错信息整理好发过来我看到后会第一时间帮你分析思路。
阅读完成 · 觉得有帮助?
咨询建站