☰
Spring Boot美容美发管理系统:从需求到部署的完整拆解
2026/10/1 12:17:00 网站建设 项目流程

做了不少Java课设和真实项目带人,发现一个现象:很多同学一拿到“基于Spring Boot的美容美发管理系统”这种题目,第一反应就是上GitHub搜开源项目,搜到后直接下载、启动、截图,交差了事。但真到了答辩或者面试官问你“为什么会员余额表要单独拆出来”的时候,立马卡壳。这篇就来拆解一个编号14898的美容美发管理系统设计与实现案例——我按“需求分析→技术选型→数据库设计→核心代码→安全权限→部署排坑”的完整链路过一遍,最后交代清楚源码怎么“白嫖”到手。这篇适合两类人:一是准备做课程设计、毕设的学生;二是真打算给线下门店做一套管理系统的开发者。读的时候我建议你带着两个问题看:这套系统到底解决了什么痛点、换你写,哪几个点容易翻车。

1. 需求调研就像给美容院“问诊”——先把业务场景摸透

很多学生拿到课设题目直接打开IDE建项目,我拦一下。你连店里谁用系统、一天要处理哪些事都没搞清楚,写出来的无非是个“带会员表的CRUD”空壳。所以先说业务,再谈代码。

1.1 美容美发门店的三类核心角色和十项日常事务

我实地蹲过好几家门店,也跟店长、前台、发型师聊过。美容美发店的日常绕不开三类人:

  • 老板/店长:看营收、看会员储值情况、给员工发提成、查哪个项目卖得好。
  • 前台/接待员:开卡、充值、预约排班、消费记账、提醒会员到期。
  • 技师/发型师:查看自己的预约排班、登记服务完成、可能还需要核销项目卡。

对应这三类人,系统要用到的功能清单很自然地浮出来:

  1. 员工账号管理(登录、密码找回、角色区分)
  2. 员工排班与预约管理(谁几点有空、谁来约)
  3. 会员信息管理(姓名、手机号、等级、积分)
  4. 会员储值卡管理(充值、扣费、余额记录)
  5. 服务项目管理(美发、美容、spa分类)
  6. 消费记录/订单管理(做了什么项目、花了多少)
  7. 商品零售管理(洗发水、护发素等门店商品)
  8. 营销工具(优惠券、次卡、生日提醒)
  9. 营收统计报表(日营收、月营收、项目消耗排行)
  10. 系统设置(角色权限、基础参数)

如果全部功能做齐,够你写一个学期。但课设往往不需要那么满,所以还要再砍掉一部分或做成简化版。这个案例我看到的实现里,保留了核心的“会员管理+预约管理+消费流水+营收统计+权限登录”,零售和营销只是附带了一个简单实现。这是聪明的取舍——项目完整度够了,工作量也能在几周内拿下。

1.2 需求优先级排序:先做能保命的功能,再做增光添彩的功能

不是所有的需求都值得先做。我给自己带项目定过一条不成文规则:没有“数据闭环”的功能先放一边。

什么叫数据闭环?举个例子。会员充值是十项需求里最核心的一环,因为“充1000送100”这种动作直接改变会员余额和门店的预收款负债,余额变动必须能追溯。预约管理也需要闭环:客户约了周五下午两点做护理,时间到了前台要能查到预约记录、技师要能标记“已服务完成”,之后这条记录才能变成一条消费流水。如果预约完了就完了,后面不跟着消费和扣款,这个预约功能就是空的。

所以我会按这个顺序切蛋糕:

  1. 第一优先级(骨架):登录与角色权限、会员档案、服务项目管理。
  2. 第二优先级(业务闭环):预约、消费收银、储值充值、积分变动。
  3. 第三优先级(数据价值):营收报表、会员消费历史查询。
  4. 第四优先级(锦上添花):数据字典、操作日志、优惠券。

你拿到源码之后,先按这个优先级关系读代码,会比从首页模块点到模块更高效。因为这套系统的很多表结构、接口设计,都是围绕“数据能串起来”这个目标来的。

2. 技术选型不跟风——Spring Boot 凭什么成为这个项目的主心骨

不是拿新潮的技术堆出来就代表系统牛,很多时候选型是在跟场景对答案。这套系统用的技术栈很常规:Spring Boot 2.x + MyBatis-Plus + MySQL + Thymeleaf + Bootstrap + Shiro(或拦截器方案)。我逐个说为什么这样选。

2.1 前后端分离 vs 服务端渲染:案例为何选后者

现在培训班最爱吹前后端分离,Vue+Spring Boot一摆,好像不分离就不专业。但回到美容美发系统这个场景:一个几十平米的门店,前台电脑打开浏览器用系统,主要操作是填表单、查会员、打小票,并发量低到可以忽略。引入Vue后,你至少多维护一套Node构建、跨域配置、Token刷新逻辑,对课设来说纯属拉高翻车概率。

这个项目用的是Thymeleaf模板引擎做服务端渲染。也就是说,页面数据在后端通过Model渲染好,直接把HTML返回给浏览器。好处是:

  • 不用处理跨域,开发调试一条龙
  • 会话状态天然使用HttpSession,登录权限控制做起来简单
  • 代码量明显少,课设答辩时你能把每一行都讲清楚

我在推荐学生做Java课设时,除非题目明确要求前后端分离,否则一律建议Spring Boot + Thymeleaf + Bootstrap。你要点开这个案例的源码会发现,页面都是Controller返回视图名,配合一个公共布局模板,写起来非常快。

2.2 数据持久层用 MyBatis-Plus,省下百分之三十的代码

传统MyBatis要写一堆XML里的select、insert、update,单表查询根本不需要。管理系统的普通查询大多就是“查一个会员是否存在”“按手机号查余额”“分页找预约记录”,这些用MyBatis-Plus的BaseMapper直接继承,方法名拼接查询条件即可。

举例,写一个会员分页查询,MyBatis-Plus下核心代码就是:

LambdaQueryWrapper<Member> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Member::getPhone, keyword) .or() .like(StringUtils.hasText(keyword), Member::getName, keyword); wrapper.orderByDesc(Member::getCreateTime); Page<Member> page = memberMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);

你对比一下原生MyBatis,要写实体类、Mapper接口、Mapper.xml、resultMap,这套配置足够劝退新手。项目最终目的不是让你表演“我手写了多少XML”,而是快速产出能用的东西。代码量少了,逻辑才有地方展开,也方便你二次开发。

2.3 目录结构按“功能分包”还是“技术分层”

拿到源码先看包结构。这个案例包结构是:

com.example.beauty ├── controller ├── service │ └── impl ├── mapper ├── entity ├── config ├── common └── utils

这是经典的技术分层包名,几乎所有课设项目都长这样。好处是层次清晰,controller层只做参数接收和结果封装,service层写业务逻辑,mapper层访问数据库。但它的坑在于,业务上“会员充值”这一件事,要跨到MemberController、RechargeRecordService、MemberMapper好几个地方看,阅读起来不连贯。

我个人现在更推荐在service层按“业务域”包一层,比如把“会员相关的开户、充值、扣款”都收敛到MemberDomainServiceImpl里,controller只做一个薄薄的转发。但课设源码为了照顾评审习惯多半还是传统分层,你理解它的设计意图就行,不要过度重构,否则上游调用关系会乱。你只要看到Service接口加Impl实现类的组合,就知道这是为了以后接RPC或者做AOP预留地方,属于常规操作。

3. 数据库表设计是系统成功的一半——会员、预约、工单如何串起来

数据库设计我见过太多翻车案例:有的同学把所有东西塞到一张会员表里,搞出一副“大宽表帝国”;有的同学拆了二十多张表,连自己都理不清外键关系。这个项目的表结构在六张核心表之间走了一个比较标准的范式,同时保留了一点冗余换性能。我挑重点说明。

3.1 六张核心表的建立与字段理由

第一张是员工表。它和用户登录表通常合并,字段包括id、username、password、real_name、role、phone、status。密码用BCrypt加密存储,这个后面专门讲,别在这存明文。role字段我用int,0代表老板、1代表前台、2代表技师,后续权限判断直接比对数字。

第二张是会员表。核心字段:id、member_no、name、phone、level、balance、points、birthday、create_time、remark。注意member_no是业务流水号,我建议用“年份+自增序号”生成,比如202506001;这样门店员工报会员号时只需报后三位。balance和points单独存在这张表里,是为了前台查询快,但余额变动不能直接改这张表,必须通过流水表落账,防止随意改数。

第三张是服务项目表。字段包括id、cate_id、name、duration、price、status。美容美发项目指定时长很重要,因为预约排班需要根据时长算冲突。status字段用于下架项目,而不是物理删除,物理删除会导致历史订单关联失败。

第四张是预约表。字段:id、member_id、employee_id、item_id、reserve_date、start_time、end_time、status、remark。status我用枚举数字维护:0待确认、1已确认、2已完成、3已取消。这就是预约的状态机,后面代码部分会详细讲。

第五张是消费订单表。字段:id、order_no、member_id、employee_id、item_id、amount、pay_method、status、create_time。注意这里金额存的是实际应收金额,如果用积分抵扣、优惠券抵扣,再用单独的折扣记录表去记录差值,否则对账会对不上。

第六张是充值流水表。字段:id、member_id、amount、give_amount、balance_before、balance_after、operator_id、create_time、remark。核心是balance_before和balance_after这两个字段,它把每一次余额变化的前后快照钉死,出现纠纷时一查就清楚。

3.2 预约时间冲突怎么通过数据库约束解决

一个预约场景:顾客要做90分钟的染发,如果技师在13:00已被别的人约了,前台必须约不进去。如果只靠Java代码判断,两个请求同时提交时会存在并发窗口——比如两单都查到13:00空,然后都插入成功,这就脏数据了。

数据库层面最直接的兜底是加唯一约束。但因为预约区间不是唯一的(同一员工同一开始时间理论上只允许一个),可以建唯一索引:

ALTER TABLE appointment ADD UNIQUE INDEX uk_emp_start (employee_id, start_time);

这样即使代码逻辑漏了一手,数据库也会拦住重复。当然,更严谨的做法是往appointment表里加一个“当天几点结束”的反查逻辑,查询结束时需要校验:

boolean conflict = appointmentMapper.checkTimeConflict(employeeId, reserveDate, startTime, endTime) > 0;

对应的SQL大概是:

SELECT COUNT(*) FROM appointment WHERE employee_id = #{employeeId} AND reserve_date = #{reserveDate} AND status IN (0, 1) AND start_time < #{endTime} AND end_time > #{startTime}

这个开区间判断已经能覆盖绝大多数“部分重叠”的场景。你写的时候千万别只判断“等于”,因为预约常常是前后脚,一个14:00结束,另一个14:00开始,它们不应冲突,开区间完美放过。

3.3 充值与消耗流水:一张流水表打天下

这个案例有意思的点是它把充值流水和消费流水分开建表了。很多设计会把所有资金变动塞进一张交易流水表,用type字段区分,我也这么干过。分开的好处是简单粗暴,解耦,充值记录由财务模块管,消费流水由收银模块管;坏处是统计资金总流水时要做两次查询再合并。

我的建议是:如果只是课设级别,你就照这个案例分两张表;但如果想加点设计感,可以改成一张“会员资金流水表”,加type字段(1充值、2消费、3退款、4赠送),再用balance_before/balance_after记录每次变动。这样会员余额、账单、报表都可以基于同一个流水表派生,会显得更有闭环感。

你写数据库的时候还有一个容易忽略的点:所有金额字段要用DECIMAL(10,2),不要用FLOAT/DOUBLE。float的精度问题在涉及钱的场景是绝对不允许出现的。0.1+0.2等于0.30000000000000004这种事,Java端也会翻车,用BigDecimal,数据库用DECIMAL,两端同时守住。

4. 核心功能实现:从控制器到前端的完整链路

源码的核心逻辑集中在这几个模块。我带你把重要的链路串一遍。看源码的时候别漫无目的点,就按“会员开户→充值→预约→消费→报表”这条主链路走。

4.1 会员开卡与余额充值的 Service 层到底要处理哪些事

会员开卡不算复杂,但有很多“边界情况”要cover。前台输入手机号时,第一件事是去会员表查这个手机号是否已经存在。美容院经常有老顾客的手机号已经被登记,如果直接插入就会造成同一人两张卡。我做这套逻辑时,在Service开头加了一步:

@Override @Transactional(rollbackFor = Exception.class) public Member createMember(MemberDTO dto) { Member exist = memberMapper.selectByPhone(dto.getPhone()); if (exist != null) { throw new BusinessException("该手机号已注册会员,请直接搜索手机号进行开卡"); } Member member = new Member(); BeanUtils.copyProperties(dto, member); member.setMemberNo(generateMemberNo()); member.setLevel(1); member.setBalance(new BigDecimal("0")); member.setPoints(0); memberMapper.insert(member); return member; }

注意我用了@Transactional,因为生成会员号和初始化积分需要在一个事务里保证一致性。你要是把事务忘了,万一插入会员号成功后初始化积分失败了,你会得到一张没有余额的半截卡。

充值逻辑是另一个关键点。前台充值1000元,门店活动“充1000送100”,那么写到账户余额的是1100,而不是实收金额。同时必须往充值流水表里记录实收和赠送分布,否则月中回看业绩时,管理员看到全是1100一笔,但对不上实际收款,财务会疯。

public void recharge(Long memberId, BigDecimal amount, BigDecimal giveAmount) { Member member = memberMapper.selectById(memberId); if (member == null) { throw new BusinessException("会员不存在"); } BigDecimal oldBalance = member.getBalance(); BigDecimal newBalance = oldBalance.add(amount).add(giveAmount); member.setBalance(newBalance); memberMapper.updateById(member); RechargeRecord record = new RechargeRecord(); record.setMemberId(memberId); record.setAmount(amount); record.setGiveAmount(giveAmount); record.setBalanceBefore(oldBalance); record.setBalanceAfter(newBalance); record.setOperatorId(LoginUtil.getCurrentUserId()); record.setCreateTime(new Date()); rechargeRecordMapper.insert(record); }

这里有个细节:更新余额时如果把oldBalance和newBalance都存进流水,回滚查证就很容易。另外操作员ID必须从当前登录上下文取,不要从前端传,防止员工手工代充给老板自己账户。这也是为什么系统权限那块不能省。

4.2 预约模块:状态机设计,拒绝“状态满天飞”

预约状态我之前提了用“待确认/已确认/已完成/已取消”四种,但真正写的时候很多人喜欢在每个Controller里随便set一个int,最后状态管理乱成一锅粥。正确的做法是先定义一个枚举,把状态流转规则收口在一个方法里。

public enum AppointmentStatus { PENDING(0, "待确认"), CONFIRMED(1, "已确认"), COMPLETED(2, "已完成"), CANCELED(3, "已取消"); private final int code; private final String desc; // 构造函数与getter省略 }

所谓状态机,就是定义哪些转换是合法的:

  • 待确认 → 已确认/已取消
  • 已确认 → 已完成/已取消
  • 已完成、已取消是终态

你的Service里每次改状态,都调用一个普通方法去校验,比如把已取消的订单直接改成已完成,这是不可能发生的业务动作,代码层就直接拒绝,而不是等到数据库报错。

预约完成之后,系统会自动生成消费订单并扣余额。这一步如果和预约状态更新分开两个事务,就会出现“预约已完成但账单没生成”的脏数据,所以记得在同一个事务里操作:

@Transactional(rollbackFor = Exception.class) public void completeAppointment(Long appointmentId) { Appointment app = appointmentMapper.selectById(appointmentId); app.setStatus(AppointmentStatus.COMPLETED.getCode()); appointmentMapper.updateById(app); Order order = new Order(); order.setMemberId(app.getMemberId()); order.setItemId(app.getItemId()); order.setAmount(serviceItemMapper.selectById(app.getItemId()).getPrice()); order.setCreateTime(new Date()); orderMapper.insert(order); // 扣减会员余额 memberService.deductBalance(app.getMemberId(), order.getAmount()); }

这里也牵出一个业务决策:做一个项目是直接扣储值卡余额,还是要走“订单支付”流程?真实门店一般允许储值、现金、微信支付混合。这个案例为了简化,把预约完成的消费默认从余额扣,前台再手动去订单里补登实付方式。你如果拿来用,可以再扩展一个支付方式字段。

4.3 首页统计报表:一个聚合查询搞定昨日营收

报表模块是答辩时的加分项,也是很多同学一拖再拖最后随便放个“总会员数”“总营业额”就完事的部分。我的习惯是先把“昨天营收”这种含金量高的指标做出来,因为店长每天早上必须看的数据就是昨天赚了多少、今天有多少预约。

源码里用了一个简单的Mapper聚合查询:

@Select("SELECT COALESCE(SUM(amount), 0) FROM `order` " + "WHERE status = 1 AND create_time BETWEEN #{start} AND #{end}") BigDecimal sumOrderAmount(@Param("start") Date start, @Param("end") Date end);

COALESCE是为了防止没有订单时返回null,进而导致前端NPE。这个细节值得记下来。

然后首页Controller可以一次查出四个指标:今日预约数、昨日营收、会员总数、即将到期的会员提醒。这些指标分别来自不同的Mapper查询,Service聚合返回一个HomeVO即可。

《美容院管理系统》这类系统的报表千万不用做复杂。因为它本质是ToB工具,不是BI系统。给店长看数字,给老板看趋势,就够了。你甚至可以只用ECharts做一个近一周营收折线图,答辩时展示起来非常醒目。

5. 权限与安全:员工偷看老板账单这种尴尬必须杜绝

管理系统没有权限控制就相当于店里的账本放前台抽屉不上锁。员工登录后既能看会员列表,又能点进充值流水看老板昨天给谁充了多少,这在真实门店管理里是要命的。所以这一章是保证系统“能落地”的关键。

5.1 三种角色三种权限,用拦截器还是 Spring Security

很多教程一上来就扔Spring Security加JWT加Redis,配置一大坨。这个案例我看了它的实现,它没有用Spring Security,而是用了自定义拦截器+HandlerInterceptor。这是完全务实的选择:管理系统是单体应用,页面用Session,根本没有跨端Token需求,引入Spring Security反而给自己加大防御成本。

实现思路很简单:写一个LoginInterceptor,在Handler执行前检查Session里的user对象是否存在,不存在就跳到登录页;存在再根据请求的URL前缀判断角色。

比如约定好访问路径:

  • /admin/**:仅管理员(老板)
  • /employee/**:员工角色
  • /api/**:登录用户均可

拦截器里做角色校验:

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; } String uri = request.getRequestURI(); if (uri.startsWith("/admin/") && !Integer.valueOf(0).equals(user.getRole())) { response.sendError(403); return false; } return true; }

写到这就够了。防没防住?防住了90%的场景。剩下的10%是越权访问,比如员工A想通过改URL里的预约ID查看别的员工负责的会员资料。要进阶防越权,就得在查询SQL里带上“当前员工ID”或者“门店ID”的过滤条件,这个需要结合具体业务,慢慢补。

5.2 密码加密与登录鉴权的最小实现

登录这块我见过最离谱的是密码直接用明文存在数据库,然后答辩时还振振有词“我们没来得及加密”。密码加密是一个管理系统最低限度的职业操守。Spring Security里自带的BCryptPasswordEncoder或者shiro里也有加密工具,但如果你用拦截器方案,可以直接用spring-security-crypto这一个jar包里的BCrypt加密,不必引入整套Security。

注册或初始化员工账号时:

String rawPassword = "123456"; String encoded = new BCryptPasswordEncoder().encode(rawPassword);

登录时:

if (!encoder.matches(rawPassword, dbPassword)) { throw new BusinessException("用户名或密码错误"); }

注意BCrypt每次生成的hash都带盐,所以哪怕两个人密码都是123456,数据库里存的字符串也不同。这一点可以在答辩时着重讲,显得你有安全意识。

还有一个常见坑是“登录要防爆破”。这里不要求全,但可以加一个简单逻辑:同一IP连续错误5次,锁定10分钟。用什么存计数器?缓存或Session都行。你可以用Spring自带的ConcurrentHashMap写个简单的防抖,但要注意线程安全。课设做到这一步,答辩已经可以从“能用”提到“可用”了。

6. 部署上线遇见的那些坑,希望你一次也不要踩

写了代码,能跑不等于会部署。这个项目我实际跑起来,在教学环境里见过至少五种在本地没毛病、一换电脑就炸的情况。最后一个章节专门列坑,全部是真实教训。

6.1 本地运行环境变量问题

很多人的数据库连接串写死在application.yml里,数据库密码也写死,换台电脑跑就报“Access denied for user”。这个案例我看改成了通过application-dev.yml和application-prod.yml两个配置区分环境。基础配置里留占位符,比如:

spring: datasource: url: ${DB_URL:jdbc:mysql://localhost:3306/beauty?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai} username: ${DB_USERNAME:root} password: ${DB_PASSWORD:123456}

这样你部署到云服务器时,只需在启动命令里传环境变量,不需要改代码:

java -jar beauty-system.jar --DB_URL=jdbc:mysql://192.168.1.10:3306/beauty --DB_PASSWORD=prod密码

顺带提数据库版本:项目用MySQL 5.7及以上,字符集默认utf8mb4,如果你建库时忘记指定,库表默认latin1会导致中文乱码。建库SQL我喜欢写成:

CREATE DATABASE beauty DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

6.2 打包后静态资源404、数据库连接、时区

Spring Boot打包成jar后,Thymeleaf页面和静态资源都在jar内部。如果你用了static目录或templates目录自定义路径,注意不要用src/main/webapp放页面,因为打成jar后webapp不会自动打包。这是老生常谈,但真的有人踩。正确的目录是src/main/resources/static和src/main/resources/templates。

数据库连接报错还有一个常见原因:驱动版本和MySQL版本不匹配。比如MySQL Connector/J 8.x连MySQL 5.7没问题,但你用8.0.11之前的老驱动连MySQL 8,就会报Public Key Retrieval is not allowed。这个案例pom里用的是mysql-connector-java 8.0.28,你也按这个版本往上走,别随便用老版本。

关于时区,连接串里务必加serverTimezone=Asia/Shanghai。不加的话,你插入的create_time会跟本地时间相差8小时,报表统计全错。这个Bug排查起来很隐蔽,因为它不报错,只是数字对不上。

6.3 端口被占用、内存不够、MySQL服务没启动

这三个是新手复现项目时的高频错误。端口被占用时,你用netstat -ano | findstr 8080或者Linux下lsof -i:8080看哪个进程占了,要么杀进程,要么在application.yml里改成8081。内存不够一般是IDEA启动和MySQL同时跑内存爆了,启动参数加一个-Xmx256m能缓解。MySQL服务没启动的情况在自己电脑上最常见,尤其是Windows下安装的MySQL,开机不自动启动,第一次运行项目就报连接拒绝。先Win+R运行services.msc找到MySQL服务手动启动,再跑项目,能少折腾半小时。

6.4 源码白嫖的正确姿势:先环境、再跑通、后改造

回到标题的“可白嫖源码”。这套项目源码我建议你白嫖之后按三部曲使用,不然白嫖了也是浪费。

第一步,先按项目说明把数据库脚本导入,改环境变量,启动成功。这步验证环境是否OK。

第二步,从头到尾按照主链路点一遍功能:登录、开会员、充值、预约、消费、看报表。哪一步断了,看后台日志,定位是Controller、Service还是SQL的问题。这比漫无目的读代码有效十倍。

第三步,也是白嫖源码最终的意义所在:改一个功能。比如把会员等级从“普通/黄金/铂金”改成1到5级;或者给预约加一个短信提醒(哪怕模拟的也行)。你只要改成功一次,这套源码就真正变成你自己的东西了。

我带的几个学生,有人评论区一堆源码链接但从不落地,有人只看最后的“答辩版”巨细,结果被问两句就露馅。你要是认真跑一遍并改了哪怕一个功能点,答辩通过率成倍提高。

最后再分享一点做这类系统的经验

说句掏心窝的,管理系统项目在Java课设里占了半边天,你说它有没有技术难度?没有。它的真正难点在于业务边界清晰、状态流转严密、数据能闭环。你做这个项目最大的收获不是学会了Spring Boot注解怎么用,而是“把一个门店的日常工作抽象成数据结构”这种建模能力。

这个14898案例的代码逻辑不算复杂,但它的表和接口组织方式适合作为第一套完整源码来拆。如果你决定跑这套源码,建议第一件事先给所有表补上注释字段,梳理一遍它们之间的关系。等你能不看文档说出“哪张表通过哪个外键关联到哪张表”时,源码就吃透了,后面做任何管理系统都能举一反三。祝你也白嫖得心安理得、改得明明白白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询