ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于Spring Boot的小型超市管理系统:从数据库到部署的完整实战

基于Spring Boot的小型超市管理系统:从数据库到部署的完整实战 简介基于SpringBoot的小型超市管理系统毕业设计完整资料面向计算机专业学生与Java开发者用于解决小型商超数字化管理中的信息孤岛、采购流程繁琐及销售决策缺乏数据支持等问题。系统采用B/S架构集成商品分类管理、采购流程优化、销售数据分析与经营报表生成等功能前端基于Vue构建后端采用RESTful接口形成清晰前后端分离的项目结构。压缩包共402个文件大小13.18MB核心包括158个Java源码、58个Vue页面、数据库脚本、XML配置及Word设计文档等可直接导入MySQL运行便于二次开发与毕设撰写。目前已有147人学习下载适合作为课题参考或实战练习通过源码与文档可掌握需求分析、数据库设计、接口开发到页面联调的全流程对提升综合开发能力有直接帮助。1. 基于Spring Boot的小型超市管理系统它不是课程作业而是一套可交付的进销存骨架只要在高校计算机专业待过大概率都听过“小型超市管理系统”这个题目。很多人在答辩前一个月才开始动手然后发现它远不是“做个页面、连个数据库”那么简单。真正上了线才明白商品要分类、进货要算成本、卖货要扣库存、库存不足要预警再加上收银员、管理员的角色划分一整套业务下来只有Spring Boot加MySQL这条路最稳。基于Spring Boot的小型超市管理系统本质上是把超市后台的进销存、收银、库存预警和报表统计这四件事整合到一个Web应用里让门店从记Excel变成键对键的线上系统。这篇文章会从数据库设计、后端代码、事务处理和打包上线四个角度把一套能从零跑通的实现方案摊开讲适合需要交毕业设计、答辩露脸或者真想给线下小店搭一套轻量后台的人。2. 技术选型和模块拆分为什么Spring Boot和关系型数据库撑得起小型超市2.1 Spring Boot MyBatis MySQL的组合为什么是首选小型超市管理系统听起来简单但它和普通增删改查最大的区别在于数据量虽不大业务链条却很长。一次销售行为要同时更新订单表、订单明细表和商品库存表任何一个环节失败都会让账目对不上。这种场景恰好是Spring Boot的舒适区──它自带的声明式事务管理让多表联动操作变得极其可控。常见做法是用Spring Boot 2.7.x这一代人配合JDK 8或11。这个组合启动快、依赖兼容性好社区里能搜到的排错方案最多。持久层我一般选MyBatis而不是JPA原因是超市系统的库存扣减和统计报表有大量需要精细控制的SQLMyBatis允许直接在XML里写UPDATE语句通过对条件的严格判断来避免并发问题。数据库方面MySQL 5.7或8.0都行小型门店的数据量用InnoDB引擎完全够。这套组合最直接的好处是Spring Boot负责把HTTP请求、事务、依赖注入这些事情全部兜住MyBatis负责把每一条SQL都握在手里MySQL负责最终的一致性和持久化。还有一个很多人忽略的选型点前端分离。小型超市管理系统通常会用Vue或者简单的HTMLjQuery做前端后端只暴露JSON接口。Spring Boot对跨域、JSON序列化、接口文档Swagger都有现成支持这比写服务端渲染的JSP要省心得多。尤其是到了答辩阶段面试官问“你这个系统的后端架构是怎么分层的”Spring Boot的三层结构Controller、Service、Mapper可以拿来直接讲清楚。2.2 模块边界商品、库存、销售、用户四大核心域拿到需求后第一件事不是写代码而是把系统切成功能域。小型超市管理系统按业务边界一般拆成四个模块用户管理管理员、收银员、仓管员三种角色登录认证和权限控制商品管理商品分类、商品档案、条码、进价、零售价、上下架库存管理入库、出库、库存查询、库存预警销售管理购物车结算、订单生成、订单明细、支付方式记录模块边界决定了表结构的拆分。很多人做这个项目最大的翻车点就是想着“商品和订单放一张表省事”结果月底做统计时SQL写得像天书。四个模块分开后每个模块只对自身的Service层负责模块之间通过接口调用而不是直接操作对方的Mapper。2.3 REST风格接口规划先定接口再写代码模块切完之后紧接着就要把接口清单列出来。这是很多新手容易忽略的一步不做接口设计直接上手撸Controller后期前端对接必然返工。小型超市管理系统的接口一般按资源命名模块接口路径方法作用用户认证/api/auth/loginPOST登录返回token用户认证/api/auth/logoutPOST退出登录商品管理/api/product/pageGET分页查询商品商品管理/api/product/savePOST新增/修改商品商品管理/api/product/delete/{id}DELETE删除商品库存管理/api/stock/entryPOST入库库存管理/api/stock/currentGET查询当前库存销售管理/api/sale/createPOST提交销售订单销售管理/api/sale/statisticsGET销售统计报表统一返回格式也是必须提前定的事。我一般用Result对象包装所有接口返回值结构为{code, msg, data}前端拿到code等于200就进入正常渲染流程否则提示msg里的错误信息。这样做的好处是后端抛业务异常时前端不用每个接口单独判断HTTP状态码全局拦截一遍即可。到这里骨架已经清楚了Spring Boot作为应用框架MySQL作为数据仓库MyBatis作为持久层桥梁四个业务模块通过REST接口对外提供能力。下面开始落地从数据库设计开始把这张网织起来。3. 数据库设计五张核心表撑起超市的进销存数据流3.1 从业务流程推导表结构数据库设计决定了这个系统能跑多稳。小型超市的日常流程可以拆成三条链路商品先分类再入库入库产生库存销售时从库存扣减并生成订单。顺着这个流程最少需要五张表用户表sys_user、商品分类表category、商品表product、销售订单表sale_order、销售订单明细表sale_order_item。如果想要更细的库存追溯再加一张库存流水表stock_log但第一版可以先不做。设计时有几个边界要提前想清楚。商品表里同时存了退货价格和零售价格因为超市的利润核算依赖这两列。库存数stock直接冗余在product表里而不是实时聚合库存流水表算出来这是为了查询商品列表时不用JOIN十张表。但stock冗余的关键前提是所有修改库存的地方都必须走事务否则并发下会出现负数。3.2 建库建表SQL一套可直接执行的DDL以下DDL可以直接在MySQL 5.7及以上版本执行。注意数据库字符集用utf8mb4不然商品名称里的生僻字会变成问号。CREATE DATABASE IF NOT EXISTS supermarket_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE supermarket_db; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密, real_name VARCHAR(30) COMMENT 员工姓名, role VARCHAR(20) NOT NULL DEFAULT CASHIER COMMENT 角色ADMIN/CASHIER/WAREHOUSE, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB COMMENT系统用户表; CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序号 ) ENGINEInnoDB COMMENT商品分类表; CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT 分类ID, barcode VARCHAR(30) COMMENT 条码可用作检索, name VARCHAR(100) NOT NULL COMMENT 商品名称, spec VARCHAR(50) COMMENT 规格如500ml/瓶, unit VARCHAR(10) NOT NULL DEFAULT 件 COMMENT 计量单位, purchase_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 进货价, sale_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 零售价, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, warning_stock INT NOT NULL DEFAULT 10 COMMENT 库存预警阈值, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category_id (category_id), KEY idx_barcode (barcode) ) ENGINEInnoDB COMMENT商品表; CREATE TABLE sale_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL UNIQUE COMMENT 订单号如20240520xxxx, cashier_id BIGINT NOT NULL COMMENT 收银员ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, payment_method TINYINT NOT NULL DEFAULT 1 COMMENT 1现金 2微信 3支付宝, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_create_time (create_time) ) ENGINEInnoDB COMMENT销售订单表; CREATE TABLE sale_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 所属订单ID, product_id BIGINT NOT NULL COMMENT 商品ID, product_name VARCHAR(100) NOT NULL COMMENT 冗余商品名称, price DECIMAL(10,2) NOT NULL COMMENT 成交单价, quantity INT NOT NULL COMMENT 购买数量, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计金额 ) ENGINEInnoDB COMMENT销售订单明细表;这段DDL定义了五张业务表。需要重点说明的是sale_order_item里冗余了product_name和price这是刻意为之。原因很现实商品可能改名、下架甚至删除而订单明细属于历史数据必须保留交易发生那一刻的快照。你如果去冗余靠查询时JOIN商品表拿名称商品一旦被删历史订单里就查不到卖的是什么了。3.3 初始化数据让系统第一次启动就能登录表建好后要初始化一批数据否则系统启动后连登录都进不去。管理员账号是必须的密码要注意不能是明文。常见的做法是启动时插入一条BCrypt加密过的密码如果你不知道怎么生成BCrypt串可以先写一个临时接口启动后调用它生成加密值再回填。这里给一条初始化语句密码默认是123456的加密串不同BCrypt实现生成的结果不同实际使用时任选一种下面这种格式即可INSERT INTO sys_user (username, password, real_name, role, status) VALUES (admin, $2a$10$7JB720yubVSZvUI0rEqK/.VqG0z2bq3eSxE6uX5Ui8YxPzTgTQ1nO, 系统管理员, ADMIN, 1); INSERT INTO category (name, sort_order) VALUES (饮料, 1), (零食, 2), (日用品, 3); INSERT INTO product (category_id, barcode, name, spec, unit, purchase_price, sale_price, stock, warning_stock) VALUES (1, 6901234567890, 可口可乐, 330ml/罐, 罐, 2.00, 3.00, 100, 20);初始化数据在开发阶段主要是为了让列表页有内容可看。到了生产环境商品和库存数据要靠导入功能或者后台手动录入不建议直接改SQL。3.4 数据库连接配置application.yml里的关键参数数据库连接池我直接使用Spring Boot默认的HikariCP它在性能和稳定性上比老牌的Druid或者C3P0更适合并发不高但要求响应快的小型系统。连接配置放在resources/application.yml里server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/supermarket_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: cn.supermarket.entity configuration: map-underscore-to-camel-case: true这里有两个参数值得注意。第一个是serverTimezoneAsia/Shanghai如果你不指定连数据库时会报时区错误尤其在MySQL 8.0下。第二个是map-underscore-to-camel-case它让数据库的category_id自动映射成Java实体里的categoryId省去写大量resultMap的功夫。HikariCP的maximum-pool-size设置10对于单门店系统足够不要一味调大连接池过大反而会增加数据库负担。数据库层的准备到这里就齐了。下一章进入代码实战把实体、Mapper、Service和Controller串起来真正让商品能上架、能卖出、库存能扣减。4. 后端核心代码从实体类到Controller的完整链路4.1 项目骨架与依赖管理用Spring Initializr避开依赖地狱搭建工程最省事的方式是直接用Spring Initializr生成基础项目然后手动补上MyBatis和MySQL依赖。最终pom.xml里核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies版本选择上Spring Boot 2.7.18是2.x系列的最后一个维护版本稳定且兼容性好。MyBatis starter 2.3.2配合它没有任何冲突。如果你的环境是JDK 17以上也可以直接上Spring Boot 3.x但要注意javax包名变成了jakarta很多代码示例会有差异新手容易在import上报错。所以我个人建议第一阶段先用2.7.x跑通业务之后有条件再迁移。工程分包的惯例是controller、service、mapper、entity、config、common六个包。controller放接口service放业务逻辑mapper放持久层接口和XMLentity放数据库实体config放跨域、拦截器配置common放统一返回值和异常处理。4.2 实体类与Mapper映射一条SQL原子扣减库存实体类写法取决于你是否引入MyBatis Plus。如果用的是经典MyBatis实体会长这样package cn.supermarket.entity; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data public class Product { private Long id; private Long categoryId; private String barcode; private String name; private String spec; private String unit; private BigDecimal purchasePrice; private BigDecimal salePrice; private Integer stock; private Integer warningStock; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }使用Lombok的Data注解会自动生成getter和setter实体代码能精简一半。接下来是Mapper接口和对应的XML文件两者通过方法名绑定。package cn.supermarket.mapper; import cn.supermarket.entity.Product; import org.apache.ibatis.annotations.Param; public interface ProductMapper { Product selectById(Param(id) Long id); int deductStock(Param(productId) Long productId, Param(quantity) Integer quantity); }?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecn.supermarket.mapper.ProductMapper select idselectById resultTypecn.supermarket.entity.Product SELECT id, category_id, barcode, name, spec, unit, purchase_price, sale_price, stock, warning_stock, status, create_time, update_time FROM product WHERE id #{id} /select update iddeductStock UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity} AND status 1 /update /mapperdeductStock这条SQL是整个库存扣减的关键所在。它把“检查库存是否足够”和“扣减库存”合并成一条原子UPDATE语句数据库的行锁保证同一时刻只有一个事务能成功修改该商品。如果库存不足或者商品已下架受影响行数为0上层拿到这个结果后直接抛业务异常即可。不要在Service里先SELECT查库存再UPDATE那样从查出来到更新之间另一个请求可能已经把库存改掉了超卖就是这么产生的。4.3 销售Service层事务与业务异常处理销售订单创建是系统里最核心的操作一次购买行为必须保证订单主表、明细表和库存三者同时成功。Spring Boot的Transactional注解在这里体现真正价值package cn.supermarket.service.impl; import cn.supermarket.common.BusinessException; import cn.supermarket.entity.Product; import cn.supermarket.entity.SaleOrder; import cn.supermarket.entity.SaleOrderItem; import cn.supermarket.mapper.ProductMapper; import cn.supermarket.mapper.SaleOrderItemMapper; import cn.supermarket.mapper.SaleOrderMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import java.util.Random; Service RequiredArgsConstructor public class SaleService { private final ProductMapper productMapper; private final SaleOrderMapper saleOrderMapper; private final SaleOrderItemMapper saleOrderItemMapper; Transactional(rollbackFor Exception.class) public MapString, Object createSale(Long cashierId, ListCartItem items) { if (items null || items.isEmpty()) { throw new BusinessException(购物车不能为空); } SaleOrder order new SaleOrder(); order.setOrderNo(generateOrderNo()); order.setCashierId(cashierId); order.setTotalAmount(BigDecimal.ZERO); order.setPaymentMethod(1); saleOrderMapper.insert(order); BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { // 原子扣减库存失败则说明库存不足 int affected productMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected 0) { throw new BusinessException(商品不足或已下架请刷新购物车); } Product product productMapper.selectById(item.getProductId()); BigDecimal subtotal product.getSalePrice() .multiply(BigDecimal.valueOf(item.getQuantity())); total total.add(subtotal); SaleOrderItem itemEntity new SaleOrderItem(); itemEntity.setOrderId(order.getId()); itemEntity.setProductId(product.getId()); itemEntity.setProductName(product.getName()); itemEntity.setPrice(product.getSalePrice()); itemEntity.setQuantity(item.getQuantity()); itemEntity.setSubtotal(subtotal); saleOrderItemMapper.insert(itemEntity); } order.setTotalAmount(total); saleOrderMapper.updateTotalAmount(order.getId(), total); return Map.of( orderNo, order.getOrderNo(), totalAmount, total ); } private String generateOrderNo() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) String.format(%04d, new Random().nextInt(10000)); } }这段代码有三处可以讲给面试官听的重点。一是rollbackFor Exception.class它强制让所有Exception都触发回滚尤其是RunTimeException之外的检查异常如果不指定这个参数事务很可能不生效。二是在循环里先扣库存再回查商品信息把“锁行时间”缩短到最小。如果先查商品再扣库存两个操作之间其他请求会读到过期库存。三是订单号的生成直接用了时间加随机数对于单门店而言足够唯一不需要引入雪花算法。4.4 Controller层统一返回格式和参数校验Controller要足够薄它只负责接收参数、调用Service、包装返回值。以一个商品的保存接口和一个销售创建接口为例package cn.supermarket.controller; import cn.supermarket.common.Result; import cn.supermarket.entity.Product; import cn.supermarket.service.ProductService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/product) RequiredArgsConstructor public class ProductController { private final ProductService productService; PostMapping(/save) public Result? save(RequestBody Product product) { Long id productService.saveProduct(product); return Result.success(id); } GetMapping(/page) public Result? page(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) String keyword) { return Result.success(productService.pageQuery(page, size, keyword)); } }Result包装类的核心是静态工厂方法package cn.supermarket.common; import lombok.Data; Data public class ResultT { private int code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(int code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }再配一个全局异常处理器把业务异常转换成统一的JSON返回前端就不用关心HTTP状态码是400还是500了。这一步能省掉大量联调扯皮的时间属于下单前就必须做好的基础设施。Controller层尽量只暴露“页面需要的数据结构”不要把整个实体类直接返给前端。比如给商品列表的时候前端只需要商品名称、价格、库存和分类名不需要createTime这种字段。常见的做法是建一个ProductVO类只包含前端需要的字段。这样做的另一个好处是实体类里哪怕加了字段也不会意外把内部数据泄露出去。代码链路到这里已经闭环前端调用ControllerController调ServiceService操作MapperMapper通过XML SQL操作MySQL。接下来是很多人在开发到一半时最容易心态爆炸的部分——各种报错和诡异数据。避坑指南这就安排上。5. 避坑指南小型超市管理系统开发中的血泪经验5.1 并发扣库存导致的负数库存一条SQL解决超卖现象两个收银员同时卖同一件商品数据库里的库存变成了负数或者明明显示有货却下单失败。原因Service里先SELECT check库存再UPDATE扣库存两个操作之间存在时间窗口。高并发场景下两个线程同时通过了“库存充足”的判断然后一起执行UPDATE库存被扣了两次。解决把检查库存和扣库存合并到一条UPDATE语句通过WHERE条件里的stock #{quantity}来保证扣减时仍有余量。这条MySQL语句会触发行锁第二个事务必须等第一个提交或回滚后才能执行。它天然就解决了并发问题。更稳妥的方案是给商品表加version字段做乐观锁但对于小型超市系统条件UPDATE已经足够不要引入不必要的复杂度。5.2 Transactional不生效同一个类里调方法翻车现象SaleService里新增了一个方法refund内部先退款再回补库存。代码里加了Transactional结果库存回补了但订单状态没更新或者反过来。原因最典型的原因是同类内部调用。比如createSale方法内调用了另一个本类里带Transactional的方法事务不会生效。因为Spring的事务是基于动态代理实现的只有通过代理对象调用方法注解才会被拦截。同类内直接调用this.xxx()绕过了代理事务自然失效。此外方法被private修饰、方法被try-catch吞掉异常后抛出也会导致事务回滚不了。解决把需要事务的子方法放到另一个Service类里。事务方法必须是public并且不要在方法内部吞异常要让异常往外抛配合rollbackFor Exception.class使用。如果确实需要自己捕获异常做二次处理可以在catch块内手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。5.3 前后端日期时间格式对不上现象后端返回的商品创建时间是2024-05-20 12:30:00前端展示出2024-05-20T12:30或者直接变成时间戳数字。原因Spring Boot默认使用Jackson序列化LocalDateTime输出的是ISO-8601格式。如果你在application.yml里只配置了spring.jackson.date-format它只对java.util.Date生效对LocalDateTime无效。解决在application.yml里配置spring.jackson.time-zone以及date-format之外还需要在实体类的时间字段上加JsonFormat注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;如果你用Spring Boot 2.x加上这个注解后前端拿到的就是人类可读的时间字符串。如果你用Spring Boot 3.x还需要额外引入jackson-datatype-jsr310模块虽然starter里通常已经包含但要注意版本匹配。5.4 前后端分离的跨域问题现象前端页面在localhost:8081端口后端跑在8080端口浏览器直接报跨域错误请求根本发不出。原因浏览器同源策略限制了两个不同端口之间的请求。虽然Postman或Apifox测试后端接口一切正常但浏览器就会拦截。解决后端加一个全局CORS配置类允许指定来源的请求Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8081) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)和allowedOrigins(*)不能同时使用。如果前端需要携带Cookie就必须指定具体来源而不能用通配符。如果你在部署时用Nginx做了反向代理让前后端同源这个配置就可以去掉。5.5 密码明文存储在sys_user表现象某次演示时打开数据库工具发现系统用户表里密码一列全是肉眼可读的123456。原因赶进度直接把用户输入的密码放进数据库了。解决使用Spring Security自带的BCryptPasswordEncoder或者引入spring-security-crypto依赖单独使用。BCrypt是加盐哈希同一个密码每次加密结果不同这是它比MD5更安全的地方。登录校验时调用matches(plainText, hashedPassword)比对不要自己实现加密算法。小型超市系统可以不引入整套Spring Security只引入crypto模块来做密码加密认证部分用JWT或者Session管理即可。不要因为项目小就忽略密码安全这属于底线问题。5.6 逻辑删除和唯一索引的冲突现象商品表里barcode字段有唯一索引删除一个条码为6901234567890的商品后再重新录入同一条码的商品数据库报Duplicate entry。原因系统做了逻辑删除通过status或delete_flag字段标记但为了查询方便依然给barcode加了唯一索引。逻辑删除的记录还在表里唯一索引的限制就依然生效。解决方案有两个。第一是建唯一索引时把逻辑删除字段组合进去比如UNIQUE KEY(barcode, delete_flag)但只能支持一条删除记录二次删除相同的条码依然冲突。第二是删除时真的DELETE物理记录历史订单明细里已经冗余了product_name和price商品记录从product表删除后不影响历史订单展示。对小型超市系统而言第二种方案更简单直接。避坑章节是关于项目的一个真实反思。后半段尤其关键逻辑删除这个问题几乎每个做过进销存开发的人都会在血泪里学到。这里列出的每一条都可能成为答辩时被追问的点能把原理讲清楚项目加分不少。6. 打包部署与冒烟验证系统交付前的最后一步开发阶段一切正常到了部署环节仍有不少人翻车。Spring Boot的部署比传统Java Web项目要简单太多核心就两步打包成可执行JAR然后通过java -jar启动。cd /Users/supermarket mvn clean package -DskipTests # 启动并指定外部配置文件 java -jar target/supermarket-0.0.1-SNAPSHOT.jar \ --spring.config.locationfile:/opt/supermarket/application-prod.yml这里使用外部配置文件的方式是我个人的习惯。把数据库密码、端口这些环境相关的配置放到JAR包外面换服务器时不用重新打包只改配置即可。project里多环境配置的用法是把application.yml作为通用配置application-prod.yml放生产环境的数据库和日志配置启动时通过--spring.profiles.activeprod指定环境。关于JAR包打包后的资源文件加载需要说明的是Spring Boot对classpath下的mapper/*.xml会被打进JAR里这一点MyBatis的mapper-locations配置在打包后依然生效。启动完成后别急着关终端先看日志里有没有这两行Tomcat started on port(s): 8080 (http) Started Application in 3.456 seconds服务起来之后用curl做一轮冒烟验证。对小型超市系统来说核心链路就是“登录拿token → 查商品列表 → 创建销售订单 → 查库存是否减少”。下面这一套curl命令可以直接复用# 1. 登录拿token curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} # 2. 商品分页查询 curl http://localhost:8080/api/product/page?page1size10 \ -H Authorization: Bearer token # 3. 创建一个销售订单商品id为1数量为2 curl -X POST http://localhost:8080/api/sale/create \ -H Content-Type: application/json \ -H Authorization: Bearer token \ -d {items:[{productId:1,quantity:2}]} # 4. 查商品库存看有没有减少 curl http://localhost:8080/api/product/list?keyword可 \ -H Authorization: Bearer token如果第3步返回了订单号和金额第4步查询到库存已经减掉那这条业务主链路就通起来了。还有个容易被忽略的地方JAR包启动的日志默认输出到控制台关了窗口服务就没了。部署到测试环境时用nohup命令放后台并把标准输出和错误日志重定向到文件nohup java -jar supermarket-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod \ logs/console.log 21 关于验证方法我再补一个自己常用的习惯拿一份真实超市的Excel台账做数据对账。这条经验来自第一次给自己人做的门店系统。当时把商品导入功能上线后直接拿Excel里期末库存和各商品账面库存做差异比对结果发现有好几个商品对不上。排查下来才知道是退货流程里直接改了商品表库存而没有走带日志的库存变更接口。从此之后我给自己定了一条规矩任何库存变动都必须经过Service层报表统计只认订单和库存流水不认商品表的实时值。这种类似的坑其实还有很多比如商品表没有记录最后一次操作人、库存流水没有操作类型、订单表没存支付单号导致对账困难……第一版系统踩过之后复用同一套代码时这些教训就会变成内置的习惯。小型超市管理系统作为一个经典的Spring Boot落地项目它的价值不在于技术有多新而在于它迫使你把事务、并发、数据一致性这些基础功打扎实。如果你正在做这个方向或者正准备答辩希望这篇笔记能帮你把该走的坑提前踩平也希望你做出一个真正敢拿到门店去跑的系统那就比我当年交出去的作业强多了。本文还有配套的精品资源点击获取
返回列表