最近帮朋友的美发连锁店梳理门店管理流程,顺手拿了一套基于SpringBoot+Vue的美发门店管理系统源码,后端是SpringBoot + MyBatis + MySQL,前端用Vue,整体是一个标准的前后端分离项目。这套源码在毕设、课设和小型商业项目里都非常常见,但很多人下载下来后卡在跑不起来、看不懂业务逻辑、不知道怎么改成自己能用的版本。这篇文章我把整个实操过程完整记录下来,从环境搭建、数据库初始化、前后端联调,到核心业务模块拆解和二次开发方向,都给梳理一遍,如果你手里正好也有类似的源码,或者正准备自己写一套门店管理系统,可以直接照着走。
先说结论:这类系统并不复杂,本质上就是围绕“会员、预约、消费、统计”四条主线做增删改查,再加上一点状态流转和报表聚合逻辑。技术栈选得也很务实,SpringBoot负责接口和业务,MyBatis控制SQL,MySQL存数据,Vue管页面交互,没有炫技的成分,每一步都落到业务上。下面我按实操流程来拆解。
1. 这个管理系统到底解决了什么问题
1.1 美发门店的日常管理痛点
我去朋友店里坐了半小时,就看明白为什么他们需要一套系统了。前台小姐姐手里一本厚厚的纸质登记本,会员充值时靠手写记录余额,客人打电话预约只能翻本子看技师排班,月底算提成的时候要把所有小票翻出来手动汇总。这还只是一家单店,如果开第二家店,数据对不上,员工绩效算不清,会员卡在两家店通用更是一笔糊涂账。
这些问题落到系统层面,其实可以拆成几个很具体的需求点:
- 会员信息要能查、能改、能分级,余额和积分必须实时准确;
- 充值、办卡、消费要留痕,每次扣费都要能追溯到具体订单;
- 预约要有时间维度,技师当天能接多少个客人,系统里一眼就能看到;
- 月底要能自动汇总营收、消耗、提成,不用人工翻本子;
- 不同门店之间会员能通用,或者至少数据能汇总看。
这套SpringBoot+Vue的管理系统,核心就是围绕这几个需求去设计数据模型和业务流程。理解了业务痛点,再去看代码,你才会明白为什么会有那么多张表,以及为什么很多字段要那么冗余设计。
1.2 系统核心价值与适合人群
从源码结构来看,这套系统覆盖了美发门店最常用的几个场景:会员开卡、预约登记、套餐购买、消费扣次、业绩统计。它不追求大而全,但把核心链路做完整了,对于学习和二次开发来说,反而是个很好的起点。
适合谁去折腾这套东西?首先是Java后端初学者,尤其是准备毕设或课设的同学,这种项目结构清晰、业务不复杂、技术栈通用,拿来学习SpringBoot的接口编写、MyBatis的SQL映射、Vue的页面交互非常合适。其次是真正有门店管理需求的经营者或开发者,哪怕业务不完全一致,在这个基础上改一改会员规则、加一个次卡类型,成本比从零写要低得多。
我拿到这套源码后,第一件事不是急着跑起来,而是先把数据库脚本和项目目录结构过了一遍,搞清楚每个模块大概对应哪些表和接口。这一步非常重要,直接决定了后面改代码的时候是“无头苍蝇”还是“按图索骥”。
2. 技术选型:为什么是SpringBoot+Vue+MyBatis+MySQL
2.1 后端组合的成熟度分析
SpringBoot在Java后端领域已经算是事实标准了,它的自动配置把以前SpringMVC那一大堆XML配置压缩到极简,一个application.yml文件搞定数据源、端口、日志这些基础项。MyBatis作为持久层框架,和SpringBoot的整合非常成熟,核心价值在于SQL由你自己写,多表联查、条件统计、复杂报表都能精确控制,不会像JPA那样在复杂查询时让SQL变得不可控。
这个选择放在门店管理系统里特别合理。因为这种系统的数据查询大多是多表关联加条件过滤的,比如查一个会员的消费记录,要同时关联会员表、订单表、服务项目表,还可能要根据时间范围、支付方式做筛选。用MyBatis写XML映射,一条SQL就能搞定,而且结果可以直接映射成Map或VO对象返回给前端。
SpringBoot + MyBatis还有一个好处,就是社区资料极其丰富。你随便遇到一个报错,搜索引擎一翻基本都能找到解决方案。对于初学者来说这真的很重要,不然卡在某个配置问题上一整天,心态直接崩掉。
2.2 前后端分离的工程优势
前端用Vue,这是当下最主流的选择之一。Vue的组件化开发思路很适合门店管理系统这种后台界面,一个会员列表组件、一个预约日历组件、一个统计图表组件,可以拆开各自维护,互不干扰。
这套源码用的前后端分离架构,前端开发时通过代理把请求转发到后端接口,部署时前端打包成静态文件扔到Nginx或直接扔到SpringBoot的静态资源目录里,非常灵活。我在实际跑通后发现,前端npm run dev启动的开发服务器默认端口和后端SpringBoot的8080端口做了代理转发,也就是说前端页面上发的请求,统一走/api开头,然后代理到后端的localhost:8080,这个设计在联调阶段非常方便。
有人可能会问,为什么不直接用JSP或者Thymeleaf把前后端揉在一起?答案很简单,门店系统未来大概率要扩展多个端,比如小程序端、移动H5端,前后端分离后,后端接口可以同时被不同端复用,前端各写各的互不影响。这套源码选择Vue跑道,算是给后续扩展留了空间。
2.3 核心表结构设计思路
看一套系统的源码,先看数据库脚本是最快的路径。这套系统的主要表大概有这些:
| 表名 | 核心字段 | 作用说明 |
|---|---|---|
| member | id, name, phone, balance, points, level | 会员基础信息,余额和积分冗余存放 |
| card_type | id, name, type, price, total_times, discount | 卡项类型定义,区分次卡、期限卡、折扣卡 |
| member_card | id, member_id, card_type_id, remain_times, expire_time | 会员实际持有的卡,记录剩余次数和有效期 |
| service_item | id, name, price, duration, category | 服务项目,比如剪发、染发、烫发 |
| employee | id, name, phone, post, status | 技师信息,排班和提成的依据 |
| appointment | id, member_id, employee_id, service_item_id, appointment_time, status | 预约记录,状态区分已预约、已消费、已取消 |
| consume_record | id, member_id, appointment_id, amount, pay_type | 消费流水,存储每笔实际扣款 |
这个表结构有几个值得学习的设计点。第一,member表里直接冗余了balance和points,为什么?因为门店高频操作就是查余额、扣余额,如果每次都要去几十万条流水里SUM一下,性能和复杂度都不划算,用冗余字段加事务控制来保证一致性,是实际项目里非常常见的取舍。第二,member_card作为会员和卡项之间的关联表,单独存remain_times和expire_time,这样可以支持一个会员同时持有多种卡,场景上完全说得通。第三,consume_record不直接存服务名称,而是存service_item_id,通过关联查询拿到名称,避免冗余更新困难。
我在导数据的时候特别注意了表之间的外键关系,虽然是逻辑外键(代码层保证),而不是物理外键,但业务上的关联非常清晰。这种设计在中小型项目中很常见,性能好、解耦灵活,缺点是必须在业务代码里注意事务和一致性。
3. 核心功能模块拆解:从会员到报表
3.1 会员与储值卡管理
会员模块是整套系统的地基,因为美发门店最大的价值就是会员储值带来的现金流。这个模块的代码量不算大,但逻辑链路长:开卡要生成会员档案、创建储值卡、写入开卡流水;充值要更新余额、写充值记录;消费时要校验余额、扣款、积分、更新会员等级。
我在读代码时最关注的,是余额扣减是怎么保证不超扣的。源码里用了事务加条件更新的方式,核心SQL类似这样:
UPDATE member SET balance = balance - #{amount}, points = points + #{points} WHERE id = #{memberId} AND balance >= #{amount}这个写法的妙处在于,数据库层面就判定余额是否充足,如果不满足条件,受影响行数为0,业务层再抛出异常回滚事务,从根上避免了并发扣款把余额扣成负数的问题。比起“先查余额再判断再更新”的做法,这种条件更新写法在高并发场景下安全得多。我建议你拿到源码后重点看看这部分的Service层和Mapper层,事务隔离级别和回滚策略都值得学。
开卡业务稍微复杂一点,因为要同时操作member表、member_card表、consume_record表,如果其中一步失败,前面已执行的插入就必须回滚。源码里用了@Transactional注解来完成这个多表一致性保证,这也是SpringBoot声明式事务最常见的用法。我在测试时特意手动造了一个卡项ID不存在的场景,观察事务是否能正确回滚,实测下来没有问题。
3.2 预约与技师排班
预约模块的核心难点是时间冲突判断。一个技师在某个时间段只能接待一个客人,如果两个预约在系统里撞车,前台在录入时就应该给提示,而不是等当天才发现。
源码里的预约表设计得比较直接,存了appointment_time和employee_id,判断冲突时用一条条件查询:查同一个技师、同一个时间段、且状态不是“已取消”的记录是否存在,存在就提示换人或者换时间。我粗略估算了一下,这个查询在单店几万条预约量级下性能完全没问题,配合employee_id和appointment_time的联合索引,毫秒级返回。
排班这块其实在系统里比较轻量,没有做复杂的周排班表,而是通过列表展示技师在某天的预约时段来体现。如果你要扩展得更细致,可以考虑增加一张schedule表,提前定义技师的每日可预约时段,预约下单时只能在可预约时段里选。这是一个很典型的二次开发方向,我后面在第五节会展开说。
3.3 套餐与消费记录
卡项类型设计上分成了次卡、期限卡、折扣卡,这基本覆盖了美发店的主流玩法。次卡按次数扣减,适合剪发卡、烫染卡这类高频低价项目;期限卡按有效期限制,比如三百元三个月内无限次洗吹;折扣卡则是充一笔钱,后续消费按折扣实时结算。
消费记录的设计我比较喜欢,因为它没有把所有扣费逻辑固化在代码里,而是把“卡类型不同导致扣费方式不同”作为策略去处理。次卡走次数扣减,余额卡走金额扣减,要是未来再新增一种“次数+金额混合卡”,只需要在卡类型判断处加一个分支,扩展成本很低。
扣费记录统一写到consume_record里,每条记录都带pay_type字段标明本次消费用的是余额、次卡次数还是现金。这样一来,财务对账的时候就不会乱,月底统计营收时按支付类型分组求和就行。我在测试时特意验证了“次卡次数为0且余额也不足”的场景,系统能正确提示“请先充值”,没有出现扣费成功但实际没扣到的脏数据。
3.4 门店经营统计报表
报表模块虽然代码量不多,但却是店长最关注的模块。统计维度主要是三个:每日营收、消费项目排名、技师业绩。
每日营收的SQL思路其实很简单,对consume_record表按日期分组求和,再关联到支付方式维度:
SELECT DATE(create_time) AS date, pay_type, SUM(amount) AS total FROM consume_record WHERE create_time BETWEEN #{startTime} AND #{endTime} GROUP BY DATE(create_time), pay_type ORDER BY date DESC这类SQL在MyBatis的XML里写起来很清楚,读源码时建议顺着这条线去看StatisticMapper.xml里各个统计SQL的写法。技师业绩排名则是按consume_record关联appointment表,再用employee_id分组汇总,逻辑上跟营收统计类似。
报表模块在Vue前端通常配合图表库来做可视化展示。这套源码里用的是ECharts,折线图展示每日营收趋势,柱状图展示项目销量排名,饼图展示支付方式占比。如果你在二次开发时要加新的报表,比如“会员复购率”、“客单价趋势”,后端加SQL、前端加图表组件的套路是完全一样的,复制现有模式就能扩展。
4. 源码快速跑通:环境搭建与启动实录
4.1 环境准备清单
在开始之前先把环境准备好,这一步别省。我之前给朋友调试时发现,很多人项目跑不起来,根本不是代码问题,而是环境版本不对造成的。
我这次用的是这套组合:
| 依赖项 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 | SpringBoot 2.x 项目标配,太高的Java版本反而容易出兼容问题 |
| Maven | 3.6+ | 用IDEA自带或单独安装都行 |
| MySQL | 5.7 或 8.0 | 8.0时需要改驱动和时区配置 |
| Node.js | 14~16 | Vue2项目建议装16,申请npm包时兼容性最好 |
| IDE | IntelliJ IDEA | 后端用IDEA,前端可以继续用IDEA或VS Code |
这里有个容易踩坑的地方:如果你的机器装的是Java 17甚至更高版本,SpringBoot 2.x的老项目跑起来可能报一些奇怪的反射或字节码错误。解决方法有两个,要么在本机多装一个JDK 1.8并切过去,要么在工程pom.xml里把java.version改成1.8并结合IDE指定SDK。我实测用JDK 1.8 + SpringBoot 2.x + MySQL 5.7这套组合是最稳的。
4.2 数据库初始化步骤
拿到源码后,先找SQL脚本。通常放在项目的sql目录或docs目录下,文件名类似hairstyle.sql或init.sql。执行导入前我建议先创建一个独立的数据库,避免跟现有库混淆。
mysql -uroot -p CREATE DATABASE hairstyle DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hairstyle; SOURCE /path/to/hairstyle.sql;导入完成后,用SHOW TABLES;确认表是否齐全,再随机抽查一行数据,比如SELECT * FROM member LIMIT 5;,确保数据真的导进去了。很多人在导入时报错,大部分原因是SQL脚本里有特殊字符或版本不兼容,但也有小概率是脚本本身编码不对,这时用文本编辑器另存为UTF-8编码再导入,基本能解决。
4.3 后端启动细节与常见报错
后端工程用IDEA打开后,Maven会自动开始下载依赖,等右下角进度条走完,再去看源码结构。默认包名通常和项目名一致,重点关注主启动类上的@SpringBootApplication注解,以及application.yml文件里的配置。
数据源配置是最关键的一步,需要根据自己的MySQL账号密码做调整:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hairstyle?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hairstyle.entity configuration: map-underscore-to-camel-case: true如果你用的是MySQL 5.7,驱动类用com.mysql.jdbc.Driver也还是可以的,但如果你用MySQL 8.0,必须改成com.mysql.cj.jdbc.Driver,而且URL里一定要加serverTimezone=Asia/Shanghai和useSSL=false,否则启动或首次查询时会抛时区错误和SSL连接错误。这个配置我反复确认过,是新手最容易出错的地方。
配置改好后,直接启动主类,看到Started Application in x.xxx seconds就说明后端起来了。访问http://localhost:8080能看到一个“未找到页面提示”,也别慌,这只是因为后端没有根路径的Controller,接口都放在/api/...下面,接口正常返回JSON才是关键。
4.4 前端安装依赖与联调
前端工程通常是一个单独的子目录,比如frontend或web,打开后先安装依赖,这一步耗时可能比较长:
cd frontend npm install如果你遇到node-sass安装失败或编译报错,大概率是Node版本和sass-loader不匹配。项目如果是Vue2 + webpack的老组合,推荐直接用Node 16,然后把node_modules整个删掉重新npm install。我实测用Node 16一次就过了,用Node 18反而在编译时卡住。
依赖装完后启动开发服务器:
npm run dev看到App running at Local: http://localhost:8081/就说明前端起来了。打开页面后如果接口请求报404或跨域错误,先检查前端代理配置。Vue2项目的代理在vue.config.js里(Vite项目则在vite.config.js里),核心配置类似这样:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }确保/api开头的请求都被转发到了后端8080端口,前端的登录请求、列表请求就能正常拿到了。如果代理配置没问题但依然加载不出数据,就用浏览器的开发者工具看Network面板里具体哪个请求失败了,一般问题出在后端没启动、CORS配置、请求路径不对这三个方向。
完整跑通后,你应该能用测试账号登录系统,看到左侧菜单的会员管理、预约管理、卡项管理、统计报表这些页面,并且能在页面上做新增、修改、查询这些基本操作。
5. 深入源码:关键代码片段与二次开发方向
5.1 代码结构与请求链路梳理
先把整体结构看明白。后端典型的分层结构是:Controller接收请求、Service处理业务逻辑、Mapper操作数据库、Entity对应数据表。前端则是页面组件加API请求模块。
一次完整请求的链路是这样的:Vue页面里调用api模块封装的axios方法,发出HTTP请求到/api/member/list;后端MemberController收到请求,调用MemberService的list方法;MemberService里组织业务参数,调用MemberMapper接口;MemberMapper的SQL映射在MemberMapper.xml里找到对应SQL执行,结果反射成实体对象一路返回给前端渲染。
我在阅读时最推荐从MemberMapper.xml入手,因为里面集中了最常用的SQL写法。比如分页查询会员列表的SQL,通常用LIMIT配合PageHelper插件或手动计算offset;模糊搜索一般用LIKE CONCAT('%', #{keyword}, '%'),这里用CONCAT而不是直接%${keyword}%,是为了避免SQL注入,这个点面试里也经常被问到。
后端Controller层还有一个值得注意的设计,就是统一返回体的封装。这类管理系统一般会定义一个Result类,里面包含code、message、data三个字段,所有接口都返回这个结构,前端拿到后先判断code是否为200再取数据。这样一旦后端出现业务异常,前端就能统一弹错误提示,而不是每个接口各写一套判断逻辑。二次开发时如果新增接口,记得也按这个格式返回,能少踩很多坑。
5.2 MyBatis映射与SQL实战示例
我摘一段典型的Mapper代码来说明。拿会员储值卡变更来说,除了要更新余额,还要插入一条流水记录。MyBatis的useGeneratedKeys和事务配合,在这种场景下很重要:
public interface MemberMapper { @Transactional int payByBalance(@Param("memberId") Integer memberId, @Param("amount") BigDecimal amount, @Param("points") Integer points); }对应的XML映射:
<update id="payByBalance"> UPDATE member SET balance = balance - #{amount}, points = points + #{points} WHERE id = #{memberId} AND balance >= #{amount} </update>不得不说,MyBatis这种把SQL和Java代码分离的做法,在业务逻辑变动频繁的管理系统里确实灵活。不用像JPA那样为了一个多表查询去维护实体关系,直接在XML里写好SQL就行。改查询条件时,只需要在XML里加一个<if>标签做动态SQL,调接口时的各种组合搜索就都覆盖了。比如会员列表的筛选条件,就是通过<if test="phone != null">这种写法来拼SQL的:
<select id="selectMemberList" resultType="com.hairstyle.entity.Member"> SELECT * FROM member <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="phone != null and phone != ''"> AND phone LIKE CONCAT('%', #{phone}, '%') </if> </where> ORDER BY id DESC </select>静态SQL拼起来容易出错,但MyBatis的动态SQL标签把这块做了很好的封装,这也是为什么实际项目里MyBatis用得更顺手的原因。你在读这套源码时,把每个Mapper.xml里的<where>、<if>、<foreach>标签过一遍,基本就掌握了动态SQL的八成用法。
5.3 二次开发可以怎么玩
源码最大的价值是可扩展。我根据门店实际运营需求,整理了三个最实用的二次开发方向:
第一个方向是做技师提成规则扩展。现在源码里提成大概率是走固定比例或人工计算,如果店里不同项目提成比例不同,比如染发提成8%、剪发提成5%,可以在service_item表加一个commission_rate字段,然后在统计报表SQL里按比例实时计算,或者在做消费记录时同步计算出提成金额存到订单字段里。加字段后,所有关联处的查询都要带上这个字段,改动量不大,但很实用。
第二个方向是做预约提醒。源码里目前应该只是简单的预约记录,没有到期提醒能力。如果门店想降低爽约率,可以加一个定时任务框架(比如Spring自带的@Scheduled)每天扫描次日的预约记录,把未取消的预约通过短信或微信模板消息发出去。短信服务可以接阿里云短信或腾讯云短信,微信模板消息则要研究一下服务号接口,扩展的思路很清晰。
第三个方向是做一个移动端查看统计报表的页面。前端Vue项目加一个H5页面,复用后端的统计接口,只展示报表数据,不需要登录写操作。这个功能对老板特别友好,人在外面也能实时看到当日营收。因为你用的是前后端分离架构,后端接口已经具备,前端新建几个页面组件就够了,我算了下工作量,纯新手一到两周内也能做完。
6. 实测踩坑与问题排查实录
6.1 高频问题速查表
我把实操过程中遇到的典型问题整理了一张速查表,按启动顺序排列,方便你对症状找药方。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
后端启动报java.sql.SQLException: The server time zone value | MySQL版本和JDBC驱动时区不一致 | URL加serverTimezone=Asia/Shanghai |
启动报Failed to bind properties under 'spring.datasource' | 配置缩进或数据库账号密码错误 | 检查YAML缩进,确认密码正确且MySQL服务已启动 |
请求接口返回404 | Controller路径与Mapper路径不一致,或Mapper.xml没扫到 | 确认@RequestMapping路径、mapper-locations配置路径 |
请求接口报Failed to parse HTTP message | 前端JSON格式错误,或实体类缺少无参构造器 | 用Postman直接调后端接口定位问题在端到端哪一层 |
启动报BindingException: Invalid bound statement | Mapper接口和XML文件不匹配 | 检查Mapper方法ID与XML的id一致、XML路径在配置范围内 |
前端npm install报node-sass编译失败 | Node版本与sass-loader兼容性问题 | 降Node到16,删掉node_modules重新安装 |
| 页面请求跨域报错 | 后端CORS没配置或前端代理配置错误 | 前后端联调优先使用devServer代理,绕过跨域 |
| 数据中文乱码 | 数据库字符集不是utf8mb4 | 建库时指定utf8mb4,连接URL配置characterEncoding=utf8 |
排查的顺序有个笨但有效的方法:先确认后端接口用Postman能通,再排查前端代理和渲染逻辑。因为前端报错往往只是现象,根因多在后端接口没返回预期数据。我在调预约列表时碰到过一次,页面一直报接口500,结果自己一看是MyBatis的XML里用了DATEDIFF函数,数据库版本不支持,换了个函数就好了。
6.2 几个值得注意的细节和小建议
再说几个我在实际使用中发现的细节,这些如果没人提醒,你可能要折腾很久才能意识到。
第一,application.yml里map-underscore-to-camel-case: true这个配置非常重要。因为MySQL字段习惯用下划线命名(比如create_time),Java实体类习惯用驼峰命名(比如createTime),打开这个配置后MyBatis会自动完成映射,不需要手写大量resultMap。我遇到过有人没开这个配置,实体类属性全是null,排查了大半天。
第二,事务不要扩大也不要遗漏。比如发卡这个操作,如果只给member_card插入卡记录,但忘了写consume_record流水,那后续对账就对不上;反过来,如果连发送短信的远程调用也放在事务里,短信服务响应慢时会长时间占用数据库连接。正确做法是把数据库更新放在事务里,远程调用放事务外并且做好失败补偿。
第三,前端组件里注意表单校验的时机。管理系统的表单不少,比如会员手机号校验,应该在失焦时提醒还是提交时统一校验?我建议前端的失焦校验用async-validator这类库做规则校验,后端Controller参数上再用@Validated做兜底校验。两边都做,体验和安全性才有保障。
第四,关于密码安全。这类管理系统的登录模块往往是最薄弱的,很多源码直接用MD5存密码,甚至明文存放。建议二次开发时至少升级为BCrypt加盐哈希,Spring Security自带BCryptPasswordEncoder,集成成本很低。这虽然不是业务功能,但上线前不做这一项,风险很大。
第五,报表数据要用索引支撑。统计模块的SQL一般会GROUP BY日期或员工ID,如果consume_record表数据量大,一定要在create_time、employee_id这些字段上建索引。我朋友这家店一个月大概产生五六千条流水,目前不加索引也能跑,但数据积累到十万级以上,报表查询就会明显变慢。提前用EXPLAIN看下执行计划,加上索引,是成本最低的优化手段。
从我的实操体验来看,这套SpringBoot+Vue的美发门店管理系统源码,技术层面并不高深,都是Java Web开发里最常见的那套东西,但它的业务链路很完整,从会员开卡到消费统计能串成一个闭环。拿它来做毕设、课设,或者作为门店系统的开发底座,都是一个性价比很高的选择。如果你正在研究这套源码,建议每看完一个模块,就自己动手加一个小功能,比如给会员列表加一个“本月消费”列,或者给预约模块加一个取消原因备注,通过改代码来验证你的理解,比单纯看代码要扎实得多。