刚开始接这个课题的时候,很多同学都会下意识觉得“宠物咖啡馆”不过是把普通咖啡馆系统换个皮,加几个宠物字段而已。真正动手之后才发现,这里面的业务细节远比想象中复杂:座位预约要处理时间冲突、寄养服务要记录宠物状态、点单要联动商品库存,还有会员积分、订单退款、员工排班……如果一开始没有把整体架构想清楚,开发到一半很容易陷入“改需求改到吐”的泥潭。
这篇文章我就以自己实际做完这个基于Spring Boot的宠物咖啡馆平台管理系统的经验,把从需求拆解、技术选型、数据库设计到核心代码实现、文档撰写、答辩准备的整个链路完整讲一遍。无论你是用来做课程设计还是毕业设计,这篇内容基本可以当成一份“从零到交付”的实操地图来用。
1. 项目整体设计与思路拆解
1.1 宠物咖啡馆到底在“管”什么
宠物咖啡馆和普通咖啡馆最大的区别,在于它的服务对象有两个:人和宠物。这意味着系统的业务模型需要覆盖两套主线的数据。
顾客这一侧,核心流程是:注册登录 → 浏览门店信息 → 选择座位/包间并预约(带宠物) → 到店后点单(咖啡+宠物零食) → 享受服务(可选宠物寄养或洗护) → 结算 → 评价。这套流程里牵扯到的核心对象有用户、宠物档案、座位、商品、订单、评价,每个对象之间还有明确的关联关系。
管理员这一侧,核心流程是:维护商品和分类 → 审核和处理预约 → 管理寄养记录 → 查看订单和营业数据 → 发布公告 → 管理员工账号。说得直白一点,这个系统要能同时撑起“顾客自助使用”和“门店运营管理”两个场景,缺了任何一边,做出来的东西都只是半成品。
我当时给这个项目定的功能边界是这样的:
- 用户端:注册登录、宠物档案管理、座位/包间预约、在线点单、订单查询、评价、个人信息维护。
- 管理端:数据看板、商品管理、订单管理、预约管理、寄养管理、公告管理、员工管理。
没有做会员充值、优惠券、营销活动这些锦上添花的功能。原因很简单:课程设计和毕业设计最重要的是把核心业务链路讲清楚、把技术点展示到位,功能堆得太多反而容易顾此失彼,最后每个模块都做得不深。
1.2 技术选型:为什么是Spring Boot + MySQL + MyBatis-Plus
技术选型是这个项目里第一个需要“讲得出道理”的点。很多同学答辩被问住,往往就是在这里只说了一句“因为大家都在用”,这显然不够。
选Spring Boot的理由其实很扎实。首先它解决了传统SSM项目里大量繁琐的XML配置问题,内嵌Tomcat,打好jar包就能跑,这对课程设计来说意味着部署成本极低,不用折腾外部容器。其次是它的自动装配机制让开发效率大幅提升,配合Spring生态里成熟的组件,一个人在一两周内把核心功能写完是完全可以实现的。
持久层我选了MyBatis-Plus而不是原生MyBatis,省掉了很多琐碎的CRUD代码。BaseMapper提供通用方法,分页插件一个配置就能用,LambdaQueryWrapper写条件查询非常顺手。更关键的是,它对课程设计这种“需要快速出活”的场景极度友好,同时又保留了手写SQL的能力,预约冲突检测这种复杂查询依然可以自己控制。
数据库用MySQL没什么悬念。订单、预约这类数据天然是强事务场景,MySQL的InnoDB引擎提供的行级锁和事务隔离机制足够可靠。至于为什么不用Redis?这个项目里确实没有特别高并发的场景,硬加Redis反而会让系统复杂化,答辩时如果被问到“Redis在你的项目里到底解决了什么问题”,答不上来就是给自己挖坑。
我在实际开发中建议的项目结构是这样的:Spring Boot单体应用 + MyBatis-Plus + MySQL + Thymeleaf,如果前端基础比较好,可以改成前后端分离的Vue + Axios。但我个人更推荐Thymeleaf方案,因为服务端渲染模式下,你不需要额外处理跨域、Token存储这些问题,整体开发量会小一圈,对赶进度的同学更友好。
1.3 架构分层与包结构设计
说完选型,再来看看项目代码的组织方式。Spring Boot项目通常采用经典的分层架构,我用的包结构如下:
com.pet.cafe ├── controller // 控制器层,接收请求、参数校验、返回结果 ├── service // 业务逻辑层,核心业务处理 │ └── impl // Service实现类 ├── mapper // 数据访问层,MyBatis-Plus的Mapper接口 ├── entity // 实体类,与数据库表字段对应 ├── dto // 数据传输对象,接收前端请求参数 ├── vo // 视图对象,返回给前端的数据结构 ├── config // 配置类,如MyBatis-Plus分页插件、拦截器 ├── common // 公共类,如统一返回结果、异常处理 └── utils // 工具类这套分层结构对应的是一种职责清晰的单向依赖链:Controller依赖Service,Service依赖Mapper,实体类被各层共用。这样做最大的好处是,每一层只做自己该做的事——Controller不写SQL,Service不处理HTTP请求,代码可读性和可维护性都会好很多。答辩的时候,你可以很清楚地给老师讲“我层与层之间是通过接口交互的,具体实现都封装在Service里”,这本身就是架构思维的体现。
1.4 部分核心功能、预计开发工作量与顺序
做项目最忌讳的就是一上来就写代码。我强烈建议先把功能模块拆出来,列一张开发优先级表,然后按依赖关系一件件完成。
| 优先级 | 模块 | 工作量估算 | 说明 |
|---|---|---|---|
| P0 | 数据库设计与项目搭建 | 1-2天 | 所有功能的基础,必须最先完成 |
| P0 | 用户注册登录 | 1天 | 用拦截器控制访问权限,涉及Cookie/Session |
| P1 | 宠物档案管理 | 1天 | 用户端新增、修改、删除宠物信息 |
| P1 | 座位/包间预约 | 2天 | 核心亮点,涉及时间冲突检测 |
| P1 | 商品管理与点单 | 2-3天 | 购物车、订单事务、库存扣减 |
| P2 | 订单管理与寄养记录 | 2天 | 管理员处理订单、记录寄养 |
| P2 | 公告与评价 | 1天 | 简单CRUD |
| P3 | 数据看板与统计 | 1-2天 | 用ECharts或简单图表展示营业额等数据 |
| P3 | 项目文档撰写 | 2-3天 | 可以随着开发同步进行 |
按照这个顺序走,基本两周左右可以把一个能完整演示的项目做出来。这里提醒一句:不要把文档留到最后统一写,开发过程中随手记录遇到的问题和解决方案,后面整理文档能省一半时间。
2. 核心模块解析与数据库设计
2.1 数据库表设计:从业务场景到ER模型
数据库设计是整个项目的地基。表结构设计得好不好,直接决定后面写业务代码时是顺风顺水还是各种别扭。我在设计表的时候,是从业务对象出发一步步推导的。
先梳理核心实体:用户、宠物、座位、商品、订单、预约、寄养、评价、公告、员工。再分析实体间的关系:一个用户拥有多只宠物,一个订单包含多个商品,一次预约关联一个用户和一把座位,一次寄养关联一只宠物……
最终我设计了11张表,下面挑核心的几张说一下设计思路。
用户表(sys_user)是系统的根基,除了常规的账号、密码、手机号、昵称、头像字段外,我还加了会员等级和积分两个字段。这是为了后续扩展会员权益、积分抵扣等功能留的余地,同时也让用户表在演示时有更多可讲的内容。
宠物表(pet)是宠物咖啡馆区别于普通咖啡馆的关键表。字段包括宠物名称、品种、年龄、性别、体重、是否接种疫苗、性格描述。特别要注意“疫苗状态”这个字段,在寄养场景里这是管理员审核的重要依据,也体现出了业务的专业度。
座位表(seat)我设计成了“座位+包间”的统一模型,加了一个type字段区分普通座位和独立包间,price字段表示每小时价格,status字段标识当前状态(空闲/占用/暂停使用)。这样做的好处是,后续如果需要支持“包间预约”功能,不需要额外建表。
订单表(orders)和订单明细表(order_item)是典型的父子表结构。订单表记录订单编号、用户ID、总金额、状态、支付方式、备注;订单明细表记录订单下的每个商品、数量、单价、小计。之所以要拆成两张表,是因为一个订单可能包含多个商品,如果把所有信息塞进一张表里,冗余严重,而且统计订单金额时很容易出错。
至于预约表(reservation),字段包括用户ID、座位ID、预约日期、开始时间、结束时间、状态、备注。这是系统里业务逻辑最复杂的一张表,后面专门展开讲。
为了控制体量,我在这里先给出几张核心表的建表SQL示例,完整的脚本在项目源码里已经附上。
CREATE TABLE `sys_user` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(50) NOT NULL COMMENT '登录账号', `password` VARCHAR(100) NOT NULL COMMENT '密码(MD5加密)', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像地址', `role` TINYINT NOT NULL DEFAULT 1 COMMENT '角色:0管理员,1用户', `level` INT DEFAULT 1 COMMENT '会员等级', `points` INT DEFAULT 0 COMMENT '积分', `status` TINYINT DEFAULT 1 COMMENT '状态:1正常,0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';CREATE TABLE `reservation` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '预约ID', `user_id` INT NOT NULL COMMENT '用户ID', `seat_id` INT NOT NULL COMMENT '座位ID', `reserve_date` DATE NOT NULL COMMENT '预约日期', `start_time` TIME NOT NULL COMMENT '开始时间', `end_time` TIME NOT NULL COMMENT '结束时间', `status` TINYINT DEFAULT 0 COMMENT '状态:0待确认,1已确认,2已取消,3已完成', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_seat_id` (`seat_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='座位预约表';从上面的SQL可以看到几个设计细节:用户名加了唯一索引,防止重复注册;预约表对user_id和seat_id都建了索引,因为查询时基本都会按这两个字段筛选。对于课程设计来说,适当增加索引能体现你懂数据库优化,但不要过度设计——每个表都搞五六个索引,反而会被老师质疑需求分析能力。
再强调两个容易踩的坑。第一,order是MySQL的保留字,所以订单表名我用的是orders而不是order,否则所有涉及该表的SQL都要加反引号,很麻烦。第二,金额字段一定不要用double或float,用decimal类型,否则计算订单总价时可能出现0.1+0.2不等于0.3这类精度的诡异问题。
2.2 座位预约的核心逻辑:时间冲突检测
这个模块算是整个系统里最值得拿出来讲“含金量”的地方。预约的逻辑看起来简单——用户选一个时间段提交就行——但这里面有一个隐蔽的问题:怎么保证同一个座位在同一时间段没有被别人预约。
我最初写的是遍历已有预约记录,然后比较时间是否重叠。代码倒是写出来了,测试的时候却发现存在漏洞。后来想明白了,两条预约是否冲突,可以用一组区间判断条件来表达:
一条已有预约的时间区间为[oldStart, oldEnd],用户想预约的是[newStart, newEnd]。只要这两个区间存在交集,就说明冲突。判断不冲突的条件有两个:
- 新预约的开始时间不小于已有预约的结束时间:newStart >= oldEnd
- 新预约的结束时间不大于已有预约的开始时间:newEnd <= oldStart
只要上述两个条件一个都不满足,就判断为冲突。翻译成SQL是下面这样:
SELECT COUNT(*) FROM reservation WHERE seat_id = #{seatId} AND reserve_date = #{reserveDate} AND status IN (0, 1) AND ( (start_time < #{newEnd} AND end_time > #{newStart}) )这里的核心是AND (start_time < #{newEnd} AND end_time > #{newStart})这个条件,它将“区间重叠”的判定压缩成了一个简单的关系表达式,避免了遍历比较的复杂逻辑。只要这个查询返回的数量大于0,就说明当前座位在该时间段已被占用,直接提示用户换时间段。
实际开发中还有两个细节要注意。第一,只能把状态为待确认和已确认的记录纳入冲突检测范围,已取消和已完成的预约不应该参与判断。第二,这个判断逻辑要放在数据库事务中执行,配合行锁或唯一索引来防止并发场景下的重复预约。
2.3 点单与订单状态流转
点单流程是另一个核心功能。用户在前端选择商品加入购物车,提交后生成订单。这里的业务逻辑比预约简单,但同样要设计清楚。
订单状态我设计了四个:待支付(0)、制作中(1)、已完成(2)、已取消(3)。实际开发中,我做了一个简化处理——订单创建后直接进入“制作中”状态,跳过真实的支付环节。这样做的好处是把支付对接的复杂度去掉了,但保留了订单核心状态流转。
创建订单时有一个关键操作:扣减库存。更准确地说,是在创建订单的同时检查并扣减商品库存,这两个操作必须在同一个事务中完成。如果先创建订单再扣库存,或者两条SQL中间出了异常没有回滚,就会出现“订单有了但库存没扣”的数据不一致问题。解决方式很简单:
@Transactional(rollbackFor = Exception.class) public Order createOrder(CreateOrderDTO dto) { // 1. 生成订单主记录 // 2. 遍历商品明细,检查库存并扣减 // 3. 计算总金额 // 4. 清空购物车 // 5. 返回订单信息 }@Transactional注解保证了这一系列操作要么全部成功,要么全部回滚。我在实际测试时故意在扣库存后抛了一个异常,发现订单数据确实没有残留,Spring事务管理在这一块是非常可靠的。
这里再提醒一个容易被忽略的点:库存字段在扣减时一定要加条件判断。对应SQL是UPDATE product SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num},而不是先SELECT查一遍库存再判断。后一种写法在并发环境下会出现超卖,因为两个请求可能同时读到相同的库存值,然后同时减库存。而把条件直接写进UPDATE语句里,数据库的行锁会在执行时自然解决并发问题。
3. 实操过程与核心环节实现
3.1 项目初始化与配置文件
这一节我把自己实际搭建项目的完整步骤写出来,每一步都是验证过的。
第一步,用Spring Initializr创建项目。通过IDEA的New Project可以直接生成,关键是选好依赖:Spring Web、Thymeleaf(如果做前后端分离就选Spring Web + 手动引入Vue)、MyBatis-Plus(注意这里不能直接用Spring Initializr选,要手动加Starter坐标)、MySQL Driver、Lombok。
MyBatis-Plus的Starter坐标比较特殊,这里给出关键的依赖配置:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>选择3.5.x版本是因为它的API稳定,和Spring Boot 2.7.x兼容很好。如果你的Spring Boot用的是3.0以上版本,那要额外注意,MyBatis-Plus也有对应的新版本适配,别直接拿3.5.3.1硬上,启动时大概率会报错。
第二步,编写application.yml配置文件。这是项目所有环境相关参数的集中地,我给出的模板如下:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_cafe?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 thymeleaf: cache: false mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两行配置我要重点说明。第一个是serverTimezone=Asia/Shanghai,这个不加的话,数据库连接时会报时区错误,这是新手最常遇到的启动问题。第二个是map-underscore-to-camel-case,它的作用是把数据库里的下划线字段名(如create_time)自动映射为Java实体类的驼峰属性名(createTime),有了这个配置,你就不需要为每个字段写@TableField注解了。
MyBatis-Plus配置里我还开启了SQL日志输出(StdOutImpl)。开发阶段强烈建议打开,这样控制台能直接看到每次执行的SQL语句,排查问题效率能提高很多。上线或交付时再关掉就行。
第三步,配置MyBatis-Plus分页插件和统一结果封装。
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "操作成功"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.code = 500; result.message = message; return result; } }统一返回结果类的作用是让前后端交互有固定的数据格式。所有接口返回的都是这样一个Result对象,前端拿到code后判断业务是否成功,message用于展示错误信息,data是真正的业务数据。这个小工具虽然代码量不大,但它体现的规范意识在课程设计评分里很加分。
3.2 核心代码实现:预约冲突检测与订单创建
这一部分我贴两个核心方法,都是可以直接抄作业的。
第一个是预约Service层的冲突检测实现。流程是:先查这个座位在该日期是否已被预约,然后判断状态,如果冲突直接抛出业务异常。
@Service public class ReservationServiceImpl implements ReservationService { @Autowired private ReservationMapper reservationMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean createReservation(ReservationDTO dto) { // 校验时间合法性 if (dto.getStartTime().isAfter(dto.getEndTime())) { throw new BusinessException("开始时间不能晚于结束时间"); } // 核心:查询是否存在时间冲突的预约 Integer count = reservationMapper.selectConflictCount( dto.getSeatId(), dto.getReserveDate(), dto.getStartTime(), dto.getEndTime() ); if (count != null && count > 0) { throw new BusinessException("该座位在所选时间段已被预约,请更换时间"); } // 无冲突则插入预约记录 Reservation reservation = new Reservation(); BeanUtils.copyProperties(dto, reservation); reservation.setStatus(0); // 待确认 return reservationMapper.insert(reservation) > 0; } }对应的Mapper方法:
public interface ReservationMapper extends BaseMapper<Reservation> { Integer selectConflictCount(@Param("seatId") Integer seatId, @Param("reserveDate") LocalDate reserveDate, @Param("startTime") LocalTime startTime, @Param("endTime") LocalTime endTime); }对应的SQL写在Mapper XML文件里:
<select id="selectConflictCount" resultType="java.lang.Integer"> SELECT COUNT(*) FROM reservation WHERE seat_id = #{seatId} AND reserve_date = #{reserveDate} AND status IN (0, 1) AND ( (start_time < #{endTime} AND end_time > #{startTime}) ) </select>注意XML里小于号和大于号要用<和>来转义,否则XML解析会报错。这个坑我当年踩过,特别提一下。
第二个是订单创建的Service实现:
@Override @Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateDTO dto) { // 1. 创建订单主记录 Order order = new Order(); order.setOrderNo(generateOrderNo()); // 生成订单编号 order.setUserId(dto.getUserId()); order.setTotalAmount(new BigDecimal("0")); order.setStatus(0); // 待支付 orderMapper.insert(order); // 2. 遍历商品明细,逐个扣减库存 BigDecimal totalAmount = BigDecimal.ZERO; List<OrderItem> items = new ArrayList<>(); for (OrderItemDTO itemDTO : dto.getItems()) { Product product = productMapper.selectById(itemDTO.getProductId()); if (product == null) { throw new BusinessException("商品不存在"); } // 关键:条件更新库存,防止超卖 int rows = productMapper.deductStock(product.getId(), itemDTO.getQuantity()); if (rows == 0) { throw new BusinessException("商品【" + product.getName() + "】库存不足"); } // 3. 累加总金额 BigDecimal subtotal = product.getPrice().multiply(new BigDecimal(itemDTO.getQuantity())); totalAmount = totalAmount.add(subtotal); // 4. 组装订单明细 OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setPrice(product.getPrice()); item.setQuantity(itemDTO.getQuantity()); item.setSubtotal(subtotal); items.add(item); } // 5. 批量插入订单明细 orderItemMapper.batchInsert(items); // 6. 更新订单总金额 order.setTotalAmount(totalAmount); orderMapper.updateById(order); return order.getId(); }这段代码有几个值得在答辩时展开讲的点:事务的ACID特性如何在业务中体现、乐观锁/条件更新如何防止超卖、为什么需要把订单和明细拆表存储。每个问题展开都能讲两三分钟,比背概念有说服力得多。
3.3 前端页面设计与接口对接
前端部分我用的是Thymeleaf模板加上Bootstrap框架,没有写复杂的Vue组件。这么做的好处是学习成本低,出页面快。我建议页面结构按照用户端和管理端分开组织:
用户端的核心页面包括:首页(门店展示+公告)、用户登录/注册页、宠物档案列表页(增删改查)、座位预约页(展示座位状态+时间选择)、自助点单页、我的订单页。管理端的核心页面包括:控制台首页(营业数据面板)、商品列表与编辑页、订单处理页、预约审核页、寄养记录页。
前后端对接时,我总结了一个经验:约定永远比配置重要。所谓约定,就是提前确定好URL的命名规则、请求参数的字段名、返回结果的数据结构。我自己定了一套简单好记的规则:用户端接口以/api/user/*开头,管理端接口以/api/admin/*开头,查询用GET,新增和修改用POST,删除用GET(不推荐,但课程设计里图省事可以用)。前端通过Ajax请求这些接口,拿到Result对象后按code判断业务是否成功,再做页面跳转或渲染。
另外,要注意管理端页面的访问权限控制。如果管理员页面没有任何拦截,任何人都能直接通过URL进入后台,这在答辩演示时被老师点出来是很尴尬的。我用的方案是Session拦截器——登录成功后把用户信息存入Session,管理端的请求都经过拦截器校验,判断当前Session里的用户角色是否为管理员。
public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } if (user.getRole() != 0) { response.sendRedirect("/index"); return false; } return true; } }这种基于角色的简单访问控制,对于单体应用已经足够。我在配置类里注册了拦截器,并设置了排除路径——静态资源和登录接口不拦截,管理端路径全部拦截。
3.4 数据库脚本与示例数据
这个部分特别容易忽视,但对最终交付至关重要。数据库脚本不只是把表结构建出来,还要包含一份完整的示例数据。我强烈建议至少准备以下几类数据:
- 一个管理员账号和一个普通测试用户账号(密码记得用MD5加密后的字符串)。
- 8-10种商品(咖啡品类+宠物零食品类),价格覆盖不同档位。
- 5-6个座位和2个包间,状态各不相同。
- 几条状态不同的预约记录和订单记录,方便演示时切换查看。
- 2-3条公告、若干条寄养记录。
有这些示例数据,你演示时打开页面才不会是一片空白。更重要的是,管理端的统计图表需要用真实数据进行展示,如果数据库空荡荡的,那个数据看板页面基本没法看。关于MD5加密,这里多说一句:真正的生产环境一般会用BCrypt这类更安全的加密算法,但课程设计用MD5主要还是因为实现简单、演示直观,同时你可以在文档里说明“实际生产环境会升级为BCrypt”,这反而是个加分项。
3.5 万字文档的写法与答辩准备
很多同学拿到文档要求后第一反应是凑字数。但如果你在开发过程中有意识地记录,文档反而不难写。我的文档结构是这样安排的:
第一章是绪论/引言,讲背景和意义。这部分虽然看起来像套话,但对课程设计来说是必要的,重点是引出“宠物经济兴起,线下宠物咖啡馆管理需求上升”这条主线。
第二章是需求分析。把前面功能清单里的每个模块,分别用功能描述+用例的方式写出来。这一章是文档的核心之一,建议写详细,最好配合用例图。
第三章是系统设计。包括总体架构图(可以画分层图)、功能模块划分、数据库ER图和详细表结构说明。表结构用表格列字段,比贴SQL更清晰。
第四章是核心功能实现。对应代码里的亮点模块,比如预约冲突检测、订单事务处理,每一块写清楚“问题描述→解决方案→核心代码→实现效果”,配上截图效果更佳。
第五章是系统测试。列出测试用例表格,写正常流程、异常流程、边界情况,再附上测试结果截图。
第六章是总结与展望。
这里有一个非常关键的建议:文档里的架构图、流程图、用例图,不要用文字硬凑。用Visio、ProcessOn或者Draw.io画出来,哪怕画得简单,都比一大段纯文字描述效果强好几倍。绘图本身也花不了多少时间,但给老师的印象完全不一样。
4. 常见问题与排查技巧实录
4.1 项目启动与环境问题
这一节我把实际开发中遇到过频率最高的几个启动问题整理出来,每一个都是真实案例。
端口被占用是最常见的。启动Spring Boot项目时如果提示Port 8080 was already in use,说明8080端口被其他进程占了。解决方式有两种:一是杀掉占用进程,二是在配置里改端口。我一般用第二种,把server.port改成8081,开发阶段最省事。
数据库连接失败也很常见。报Communications link failure或者Access denied for user,前者一般是URL写错、数据库没启动、或者IP连不上;后者就是用户名密码不对。这里有个经验之谈:先把MySQL用命令行登录一遍,确认账号密码没问题,再检查代码里的配置,别一上来就怀疑代码。
时区报错是MySQL 8.0版本的高频问题。报错信息里会出现The server time zone value,解决方式就是我前面列过的,在JDBC URL末尾加上serverTimezone=Asia/Shanghai。
还有一个是MyBatis-Plus版本不兼容问题。如果你的Spring Boot版本是3.x,旧版MP Starter会在启动时报错:Failed to configure a DataSource: 'url' attribute is not specified。这种情况要换用适配Spring Boot 3的mp版本。查表最快。
4.2 业务逻辑与数据问题
库存扣减失败。这类问题的表现是:订单能创建,但查看商品库存时发现没有减少,或者库存减成了负数。原因基本就是没有用条件更新,先查库存再减导致的并发问题。解决方案我在前面已经给出,用一条UPDATE语句带上stock >= #{num}条件即可。
预约时间明明没冲突却提示冲突。这个问题我遇到过,最后发现是数据库存的时间字段类型不一致。代码里用LocalTime,数据库时间是TIME类型,本来应该没问题,但前端传过来的时间字符串格式不对,比如传了“2024-06-01 10:30:00”而不是“10:30”。解决方式是对前端传入参数做格式校验,在Controller层就把它转换成正确的类型,而不是让问题流到Service层再处理。
还有表名错误问题。前面我提醒过,Order在MySQL里是保留字,如果你把表名建成了Order,执行SQL时MySQL会报语法错误。我就是因为这个问题花了差不多一个小时排查。好在提前规划表名时就发现了,后来用了orders表名才绕过去。如果你已经建了Order表又不想改表名,那所有的SQL都要写成order,每一条,非常痛苦。
4.3 前后端联调与调试技巧
联调阶段最让人头大的就是404和405。404意味着URL路径不对,要么是Controller里没写对应请求地址,要么是前端请求的地址多了或少了一层。404还有一种情况是静态资源找不着,通常是因为请求路径没有排除拦截器的拦截。405则是请求方式不对,比如前端用POST请求一个只允许GET的接口。
排查联调问题,我习惯用浏览器F12打开开发者工具看Network面板。前端请求发出去后,状态码是多少、返回体是什么,一目了然。比在代码里加日志调试效率高得多。此外,Swagger或在线API调试工具也是好选择,但对于课程设计这种规模,直接看Network面板就足够了。
如果在Controller里返回了对象,但前端页面拿不到数据,多数情况是没加@ResponseBody注解,或者用的是@Controller而不是@RestController。Spring Boot把这两者区分得很清楚——@Controller返回的是视图名称,@RestController返回的是JSON数据。在前后端分离的架构下,控制层统一用@RestController是省心的做法。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 项目启动端口占用 | 8080被占用 | 改server.port或杀进程 |
| 启动报时区错误 | JDBC URL缺少serverTimezone参数 | 添加serverTimezone=Asia/Shanghai |
| 数据库连接被拒 | 账号密码错误或数据库未启动 | 命令行先试连MySQL |
| MyBatis-Plus启动失败 | Spring Boot版本和MP版本不兼容 | 按版本对应表更换依赖 |
| 库存扣成负数 | 先查后减,并发超卖 | 改用条件更新SQL |
| 预约冲突检测失效 | 状态未参与判断 | 在SQL中加入status条件 |
| 前端请求404 | URL路径不符或静态资源被拦截 | 检查Controller映射和拦截器配置 |
| 返回JSON但页面空白 | Controller未加@ResponseBody | 改用@RestController |
| 数据中文乱码 | 字符集配置缺失 | URL加characterEncoding=utf-8,检查字段编码 |
排查思路的核心是:先分清是环境问题还是业务问题,再从报错信息的第一行开始读。很多同学看了报错信息几十行,其实关键信息永远在前几行,英文不好也没关系,把关键单词复制到搜索框里查,基本都能定位。
5. 后续扩展与个人体会
这个项目的整体开发周期,从我规划需求到完成文档,大概用了两周。每天大概是三到四个小时的投入。如果你是第一次做这种完整项目,时间可能要翻倍,所以课程设计一定留足提前量。
我个人做完这个项目最大的体会是:课程设计和公司项目最大的区别在于,课程设计要的是“完整地走一遍流程”——从需求出发,经历过设计、开发、测试、文档,每个环节都能说个一二三,而不是做出来一个能跑的界面。所以我不建议在这个阶段执着于微服务、分布式这些炫酷架构,把Spring Boot单体应用吃透,把数据库设计想明白,把核心业务逻辑做对,已经足够支撑你拿一个很好看的成绩。
如果你学有余力,后续可以尝试这样扩展。把模拟支付替换成支付宝沙箱或微信支付沙箱,这会成为答辩的加分亮点。用ECharts替换简单的统计页,做营业趋势、商品销量排行等可视化图表。加一个简单的Redis缓存,把商品热门信息缓存起来,并说明缓存和数据库的一致性问题如何解决。把项目拆成前后端分离版本,后端提供RESTful API,前端用Vue实现。这些扩展方向每个都能成为你在答辩时自信展开的谈资,而且它们都是基于这个系统自然生长出来的,不属于生搬硬套。
最后再分享一个小技巧:演示之前,一定把示例数据准备到位。预约记录要有待确认的、已确认的、已完成的,订单要有不同状态的,商品要有库存充足和库存不足的。老师现场演示时习惯性地点击各种按钮,如果每次点都有可看的数据展示,他对这个项目的完成度印象分会高很多。反之,点开一个列表页面显示“暂无数据”,体验就大打折扣了。