流浪动物救助网站管理系统,SpringBoot + Vue + MyBatis + MySQL,这四个关键词凑在一起,就是一套典型的Java全栈Web项目。我拿这个项目做了完整版的源码,还带数据库脚本和操作文档,目的很直接:给正在找毕设题目、想练手全栈开发、或者真的在帮救助组织做信息化管理的朋友,提供一个开箱即用、能直接跑起来的方案。系统本身不复杂,但业务链路很完整,从救助登记、动物上架到领养申请、后台审核,一条龙全都有。把它吃透之后,你再去面试讲项目、或者做二次开发,底气都会不一样。
这套系统面向的人群很宽,我大概分三类:一是计算机相关专业的在校生,拿它做毕业设计,业务场景比单纯的商城或管理系统更有话题性;二是刚工作不久、想熟悉SpringBoot+Vue前后端分离协作流程的初级开发者;三是本身参与流浪动物救助的志愿者组织,想低成本上一套管理系统,把领养信息从朋友圈、群聊里搬到网站上来。接下来我就把这套系统的设计思路、表结构、后端实现、前端联调、部署跑通和排坑过程完整拆开讲,所有核心代码都是项目里实际在用的,你照着做就能复现。
1. 项目整体设计与架构拆解
1.1 先盘清楚:救助网站到底管什么业务
很多同学拿到题目就开始写代码,结果写着写着发现逻辑绕了、角色混了。所以我先带你理业务。流浪动物救助的核心链路是:发现流浪动物 → 临时安置/救助 → 信息发布 → 爱心用户浏览 → 提交领养申请 → 管理员审核 → 线下交接 → 信息归档。围绕这条链路,系统至少要包含几个关键角色。
普通访客可以浏览动物列表、查看救助故事、看公告,不需要登录;注册用户登录后多两个核心动作,一是发布救助信息,二是提交领养申请;管理员则是全系统的“守门人”,负责审核动物信息是否真实、审核领养人资格、管理用户、发布公告。这三个角色的权限边界必须清晰,否则后面做接口权限控制会很难受。
所以我把系统拆成前台展示和后台管理两个大端,前台是普通用户能看到的各类页面,后台是管理员操作的表格页和审核页。这种拆法不只是前端页面层面的拆分,后端接口也要按这个逻辑规划,比如/api/animal/list是公开接口,/api/admin/animal/audit就必须限定管理员权限。
1.2 技术选型背后的理由
后端用SpringBoot 2.x,这是目前国内中小型项目最稳的选择。它内嵌Tomcat,不需要单独部署容器,自动配置能省掉大量XML配置,生态也成熟,哪怕你后续要接Redis、消息队列,社区资料一搜一大把。为什么不选Spring Cloud微服务?因为这个项目规模就是单体应用,强行上微服务只会增加部署成本和学习负担,没有任何业务收益。
前端用Vue 2.x + Element UI,Vue响应式上手快,Element UI的表单、表格、弹窗组件特别适合做管理后台这类页面密集型的系统。MyBatis是半自动ORM,SQL由自己掌控,像动物列表这种多条件组合查询,动态SQL写在XML里比JPA那种自动生成SQL更好排查、更好优化。
数据库用MySQL社区版,免费、稳定、资源多。前端和后端通过RESTful API通信,JSON作为数据载体,前后端完全分离开发,前端mock数据调试、后端Postman测接口,互不阻塞。
这套组合的性价比很高,对毕设和真实小型项目都适用。如果是几十万用户量的大平台,当然要考虑更多中间件,但作为一套企业级入门架构,它已经覆盖了从规范开发到部署上线的完整路径。
2. 数据库设计:表结构和数据流怎么排
2.1 核心表结构拆解
我先讲数据库,因为后端代码本质上是围绕表结构在写增删改查。这套系统我设计了8张核心表,我把最重要的4张展开说明,其余的表其实都是同套思路。
用户表user是最基础的,字段有 id、username、password、phone、email、role、status、create_time。这里我要强调两点:密码绝不能明文存,项目里用的是BCrypt加密,注册时加密写入,登录时用匹配方法校验;role字段用TINYINT区分普通用户和管理员,0是普通用户,1是管理员,不要用字符串,后面判断效率高、写起来也简单。
动物信息表animal是整个系统的核心,字段包括 id、animal_name、category、breed、gender、age、health_status、description、images、status、publisher_id、create_time。category存的是猫还是狗,比如0猫1狗;health_status存健康状况,比如已驱虫、已绝育、待体检;images用varchar存图片路径,多张图用逗号分隔;status是关键状态位,0待审核、1已上架、2已领养、3已下架。
领养申请表adoption_application承载领养审核流程,字段有 id、animal_id、user_id、real_name、phone、address、reason、status、create_time。reason字段用于说明领养理由和喂养条件,让管理员判断是否靠谱。status状态是0待处理、1已通过、2已拒绝。
救助记录表rescue_record用于前端用户登记发现的流浪动物,字段有 id、location、description、images、reporter_id、status、create_time。这张表相当于“原始线索”,管理员看到后可以决定是否同步录入到动物信息表进行正式发布。
另外还有公告表notice、评论表comment、寻宠信息表lost_info、管理员操作日志表operation_log,它们结构都更简单,核心就是做一个带分页的列表查询即可。
2.2 表关系和状态流转设计
表关系上主要是三个一对多:用户到动物信息是一对多,一个用户能发布多只动物;用户到领养申请是一对多;动物信息到领养申请也是一对多,同一只动物可能收到多个申请,但状态变成已领养后,其他待处理的申请要同步处理掉,避免一个宠物被多个人“领养”了。
状态流转一定要提前设计好,不然后端写业务逻辑时容易漏。动物信息的正常流转是:发布时status为0待审核 → 管理员审核通过后变为1上架 → 有人领养申请通过后变为2已领养。如果信息不通过或者被下架,则变为3下架状态。领养申请的状态是:用户提交时为0 → 管理员审核通过为1,拒绝为2。
这里需要特别说明一个细节:为什么领养通过时要把其他同动物的待处理申请也一并拒绝?因为在真实场景里,同一只猫一旦被领养走,其他排队申请就自动失效了。这个逻辑我放在Service层的事务方法里处理,后面讲后端时会细说。
字符集方面,我建议建库时直接用utf8mb4而不是utf8,因为前台评论和救助描述里完全可能出现emoji表情,utf8存不了四字节字符,会导致插入报错。排序规则用utf8mb4_general_ci就够用。
下面给一段核心建表脚本片段,完整的SQL文件在源码包的sql/rescue_animal.sql里,直接导入即用。
CREATE TABLE IF NOT EXISTS `animal` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `animal_name` VARCHAR(50) NOT NULL COMMENT '动物名称', `category` TINYINT NOT NULL COMMENT '类别:0猫 1狗', `breed` VARCHAR(50) DEFAULT NULL COMMENT '品种', `gender` TINYINT DEFAULT NULL COMMENT '性别:0公 1母', `age` VARCHAR(20) DEFAULT NULL COMMENT '年龄描述', `health_status` VARCHAR(200) DEFAULT NULL COMMENT '健康状况', `description` TEXT COMMENT '详细描述', `images` VARCHAR(500) DEFAULT NULL COMMENT '图片路径,逗号分隔', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审核 1已上架 2已领养 3已下架', `publisher_id` INT NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`publisher_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='动物信息表';3. 后端核心实现:SpringBoot 的业务代码怎么组织
3.1 项目结构与分层设计
后端我按最常见的三层架构来组织:Controller层负责接口接收和参数校验,Service层写业务逻辑,Mapper层操作数据库。包结构是这样的:
com.example.rescue ├── controller // 接口层,只做参数接收和返回 ├── service // 业务逻辑,接口+实现类 │ └── impl ├── mapper // MyBatis Mapper接口 ├── entity // 数据库实体类 ├── common // 统一返回、全局异常、常量 └── config // 配置类,比如拦截器、跨域为什么要强制分层?最直接的好处是可替换性和可测试性。Mapper层和Service层都是接口,你以后想加缓存、换数据源,只要在Service里加逻辑或者换实现类就行,Controller不用动。而且分层后出Bug的定位范围会小很多,前端传参报错先查Controller,SQL报错直接看Mapper.xml,业务状态不对就往Service里找。
3.2 统一返回结果与全局异常处理
接口返回值如果不统一,前端处理起来会非常痛苦。有的接口返回对象,有的返回数组,有的报错时返回一个错误字符串,前端就得分情况写一堆判断。我的做法是定义统一的 Result 泛型类:
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.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }所有Controller方法都返回这个结构,前端只需要在Axios响应拦截器里判断code === 200就走成功分支,否则统一提示错误message。登录失效返回401,前端直接跳到登录页。
同时配一个全局异常处理器,用@RestControllerAdvice捕获业务异常、参数校验异常兜底返回统一的错误信息。这样即使代码里漏写了try-catch,也不会把长长的一堆堆栈抛给前端,既安全又友好。
3.3 JWT 登录认证与权限控制
登录认证我用的是JWT方案。流程是这样的:用户提交用户名密码 → 后端用BCrypt校验密码 → 校验通过后生成JWT令牌返回给前端 → 前端存到localStorage或者sessionStorage→ 每次请求在Header里带Authorization: Bearer <token>。
后端有一个专门的拦截器来解析这个token,把它放入ThreadLocal里存当前登录用户的id和角色。ThreadLocal的好处是请求线程内全局可以取到当前用户,请求结束自动释放,避免在业务方法里层层传参数。
管理员权限接口的处理更简单,拦截器解析token时顺便把角色取出来,如果是/api/admin/**的路径并且角色不是1,直接返回无权限。这块代码如下所示,我加了一些注释方便你阅读理解:
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); UserContext.set(claims.get("userId", Integer.class), claims.get("role", Integer.class)); return true; } }有一点容易被新手忽略:跨域预检请求OPTIONS是没有Authorization头的,所以拦截器里要先放行OPTIONS,否则前后端联调时浏览器控制台满屏报跨域错误,还找不到原因。
3.4 动物信息发布与审核流转
动物信息发布这块业务是最典型的:注册用户发布一条“待审核”的流浪猫信息,管理员审核通过后,它才会出现在前台的动物列表页。
发布接口的处理流程是:接收表单数据(包含animalName、category、breed、healthStatus、description等字段)→ 后端校验必填参数 → 图片已经在前端通过上传接口存到了服务器 → 把status置为0、publisherId置为当前登录用户id → 插入数据库。
管理员审核的方法在Service层要加事务注解@Transactional。审核通过就是把status改成1,审核拒绝就是把status改成3并填上审核备注。这里有一个业务细节:如果一只动物状态已经是2已领养,就不能再审核了,接口要加前置判断。
领养申请的处理是整个系统里逻辑最值得研究的地方。用户提交申请时,Service要做三步:校验这只动物状态是否为1已上架;校验当前用户是否已经申请过,如果有过一条状态为0的申请,直接提示“请等待审核完成”;然后插入申请记录。管理员审核通过时,流程是:把该申请状态改为1 → 把对应动物的status改为2 → 把所有同一动物其他状态为0的申请改为2已拒绝,三步在一个事务里执行,任何一步失败都整体回滚。
3.5 MyBatis 动态SQL和多条件分页查询
动物列表页有三个筛选条件:类别Category、状态Status、关键字keyword,另外还有分页参数。用MyBatis的XML动态SQL写这个多条件查询非常舒服:
<select id="selectAnimalPage" resultType="com.example.rescue.entity.Animal"> SELECT * FROM animal <where> <if test="query.category != null"> AND category = #{query.category} </if> <if test="query.status != null"> AND status = #{query.status} </if> <if test="query.keyword != null and query.keyword != ''"> AND (animal_name LIKE CONCAT('%', #{query.keyword}, '%') OR breed LIKE CONCAT('%', #{query.keyword}, '%')) </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签会自动处理掉第一个条件前面的AND,动态拼接更安全。分页我直接用了LIMIT #{offset}, #{pageSize},offset在Service层计算为(pageNum - 1) * pageSize,这样比用PageHelper插件更直观,也少一个依赖。只要数据量没到百万级,这种基础分页完全够用。
4. 前端实现:Vue 页面和接口对接
4.1 前端工程结构与路由设计
前端工程我用Vue CLI 2.x+ 版本建的。目录结构的关键部分长这样:
src/ ├── api/ // 按模块封装的接口请求 │ ├── animal.js │ ├── user.js │ └── admin.js ├── utils/ │ └── request.js // axios 实例,统一处理token和响应 ├── views/ // 页面视图 │ ├── home/ // 首页、列表、详情 │ ├── user/ // 登录注册、个人中心 │ ├── adopt/ // 领养申请 │ └── admin/ // 后台管理 ├── router/ │ └── index.js // 路由配置 └── components/ // 通用组件路由设计上,前台和后台我分开配了路由,后台页面统一放在/admin前缀下,在路由守卫beforeEach里判断用户角色,非管理员访问/admin/xx时直接强制跳转首页。这里注意一点:路由守卫只做页面跳转层面的拦截,真正安全校验还是要靠后端的JWT拦截器,前端只是优化体验,不能当安全边界。
登录页、首页等无状态页面用懒加载没有问题,但是像首页这种首屏要展示的内容,懒加载反而会降低体验。我这里首页用静态import,后台管理页面和详情页用懒加载,兼顾首屏速度和分包体积。
4.2 核心页面与交互流程拆解
前台首页是访客第一眼看到的东西。信息架构分三块:顶部导航栏、轮播公告区、最新待领养动物卡片区。导航栏根据用户登录状态动态显示“登录/注册”还是“用户名+退出”;轮播图的数据从公告接口读取,管理员在后台能改;动物卡片展示动物名称、一张主图、健康状况标签,点击卡片跳到详情页。
动物详情页是交互最重的页面。上半部分图片轮播,下半部分展示基本信息和一个“我要领养”按钮。如果动物状态不是1已上架,按钮要置灰并显示“已领养”或“待审核”;如果当前用户没登录,点击“我要领养”时弹窗提示去登录。登录后进入领养申请表单页,Element UI的Form表单校验很方便:
<el-form :model="form" :rules="rules" ref="adoptForm" label-width="100px"> <el-form-item label="真实姓名" prop="realName"> <el-input v-model="form.realName" placeholder="请输入真实姓名"></el-input> </el-form-item> <el-form-item label="联系电话" prop="phone"> <el-input v-model="form.phone" placeholder="请输入联系电话"></el-input> </el-form-item> <el-form-item label="领养理由" prop="reason"> <el-input type="textarea" :rows="4" v-model="form.reason" placeholder="请说明你的领养理由、家庭环境、喂养计划等"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitAdopt">提交申请</el-button> </el-form-item> </el-form>rules里面我用了required校验和自定义手机号正则,这里不能偷懒,后端虽然在Service层也校验了参数,但前端做好校验能明显减少无效请求,节省服务器资源。
后台管理页面我全部用Element UI的el-table+el-dialog组合。动物审核页有一个表格列显示图片缩略图,状态是待审核的记录操作列是“通过”和“拒绝”按钮;领养审核页展示领养人信息和申请理由,操作列是“通过”和“拒绝”。表格数据用走马灯式接口请求,每页10条,配合pagination组件切换。
4.3 前后端联调:跨域、Token与请求封装
整个联调过程里,最容易出问题的就是跨域和token。开发环境下我用Vue CLI的devServer代理来解决跨域,配置如下:
// vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }配置了这个代理后,前端代码里请求地址直接写/api/xxx,不存在跨域问题,也没必要在后端加@CrossOrigin。生产环境部署时,前端dist目录由Nginx托管,Nginx配置反向代理/api转发到后端服务,思路一模一样。
请求封装这一步很关键。我在utils/request.js里创建axios实例,统一设置了baseURL和超时时间,然后用两个拦截器:请求拦截器从localStorage取token塞进Header;响应拦截器统一处理返回结果,code不是200时弹错误提示,code是401时清掉token并跳登录页。这样页面组件里调用接口的代码就能写得很干净。
另外要特别提醒图片上传的注意点:上传接口要求请求头带token,之前我写过一版图片上传组件没带token,发布图片时报401,排查了半天才想起来上传请求走的就是公共axios实例,但那个实例没加请求拦截器。项目里上传组件务必要复用带拦截器的request实例,不要另外裸用axios。
5. 完整实操:从空环境到跑通全流程
5.1 环境准备与数据库初始化
要跑这套系统,先把环境列齐:JDK 1.8+、Maven 3.6+、MySQL 5.7或8.0、Node.js 14+、npm或yarn。我建议用系统的包管理器装全,Windows用户装MySQL时记得选utf8mb4字符集,Linux用户要用systemctl start mysqld确认服务启动。
数据库初始化不需要手动建表,源码包里已经有sql/rescue_animal.sql文件,在命令行执行:
mysql -u root -p < rescue_animal.sql执行成功后,库里有8张表,还带了一条管理员账号和一条测试用户数据。管理员默认账号密码在文档里写明了,第一次登录后建议立刻改密码。这里有个容易踩的坑:如果你用的MySQL是8.0以上,项目里默认配置了对应的新版驱动,如果换成5.7版数据库,配置文件里的driver要改回老驱动。
5.2 修改配置文件并启动后端
后端配置文件在src/main/resources/application.yml,主要改三处:数据库账号密码、上传图片的存储路径、服务端口号。我提供一个最小化配置示例:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/rescue_animal?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver file: upload-dir: /data/upload # 换成你本机的路径 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.rescue.entity启动方式我平时用命令行更顺手:
mvn spring-boot:run第一次启动会比较慢,因为Maven要把依赖全部下载下来,建议在~/.m2/settings.xml里配好阿里云镜像,不然有些依赖在国外仓库下载能看到进度条走一整天。看到 “Started Application” 日志就说明后端起来了,可以用Postman调一个公开接口验证一下连通性。
5.3 安装依赖并启动前端
前端启动流程是固定的:
npm install npm run serve如果npm install出问题,大概率是node版本不对。项目建议用Node 14或16,太高的版本(比如18+)可能会遇到某些依赖编译不过的问题。推荐用nvm管理node版本,切到对应版本后重新装依赖即可。
启动成功后浏览器访问http://localhost:8081,首页能看到数据说明前后端已经连通。注意如果后端还没启动,前端页面会加载出来但所有接口都报错,界面上是空的列表和不断弹窗的“网络错误”。
5.4 把一条完整领养流程演示一遍
跑通全流程是最好的验收方式,我建议你跟着这个顺序走一遍:
第一步,用测试用户账号登录前台。如果没有账号,先走注册流程,密码会用BCrypt加密后存进数据库,这一步顺便能验证密码加密逻辑。
第二步,以注册用户身份发布一条动物救助信息。填写动物名称、类别、救助地点、健康描述,上传一到三张图片。发布成功后,前台列表页是看不到这条记录的,因为它的status还是0待审核。
第三步,用管理员账号登录后台,进入动物审核页面。能看到刚才这条待审核记录,点击“通过”,然后打开前台列表页刷新,这条动物就能看到了。这里能直观体验到前后台状态同步。
第四步,回到普通用户身份,在动物详情页点“我要领养”,填好领养申请表提交。然后去后台领养审核页,能看到这条申请记录,点击“通过”。
第五步,再回到前台看这只动物的详情页,状态已经变成“已领养”,领养按钮置灰。此时如果有其他人也想领养这只动物,提交时会收到提示“该动物已被领养”。
这套流程跑通,说明整个系统的核心业务链路没有问题了,前后端交互、状态流转全部正常。
6. 常见问题与排查技巧实录
6.1 启动阶段的高频问题速查表
把我在调试时遇到的高频问题和解决方案整理成一张表,你遇到同类的可以直接按方抓药:
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 前端接口报跨域错误 | devServer代理没配或后端没起 | 检查vue.config.js代理配置,确认后端8080端口存活 |
| npm install 报错 | node版本不对,或依赖源太慢 | 用nvm切换16版本,换淘宝镜像源 |
| 后端启动就报数据库连接失败 | 账号密码不对、MySQL未启动 | 检查application.yml配置,先测数据库能连上 |
| Maven依赖卡住不动 | 没配镜像源 | 全局settings.xml加阿里云镜像 |
| 图片上传后访问404 | 上传目录没有读写权限 | Linux下给上传目录chmod,或换到用户目录 |
| 登录后接口全返回401 | token没生效,怕是请求头漏了 | 检查axios拦截器是否加上Authorization头 |
6.2 我实际调试中最花时间的三个坑
第一个坑是MyBatis的驼峰映射问题。animal表里字段叫animal_name,实体类属性叫animalName,如果MyBatis没有开驼峰映射,默认查出来animalName就是null。解决方法是在application.yml里加一行map-underscore-to-camel-case: true,或者在XML里显式给每个字段起别名。这个坑特别隐蔽,因为列表页不报错,只有点详情时才看到名字空着,排查思路容易偏。
第二个坑是SpringBoot拦截器的OPTIONS放行。写完JWT拦截器后,前端一调带token的接口,浏览器控制台就报CORS错误,其实不是跨域配置的问题,是拦截器把预检请求拦住了。给拦截器加上OPTIONS方法直接放行的逻辑后,问题立刻消失。这个排查过程让我意识到,前后端联调时浏览器报的错误不一定就是浏览器层面的问题,有时是后端把本该放行的请求拦掉了。
第三个坑是图片上传后的在线访问路径。开发时图片传到 localhost:8080/upload/xx.jpg,页面里能直接拼服务器地址访问;一旦部署到Linux服务器,图片传到绝对路径(比如/root/app/upload),如果配置里没有把/upload/**映射到文件路径,前端照样404。所以要写一个WebMvc配置,把静态资源路径映射好:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath = "..."; // 对应你配置的file.upload-dir registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }这个配置本地开发时不起眼,部署到服务器才会暴露,但最好一开始就加上,省得后面二次排查。
6.3 调试过程快进的经验方法
后端在排查问题时,可以用knife4j或Swagger界面把接口列出来直接调试。尤其是跨系统状态流转的逻辑,比如领养通过后动物状态能不能正确变更,用接口文档直观地调用一遍,比在页面里点击更高效。项目里我已经把knife4j依赖配好了,启动后访问/doc.html就能打开接口文档页,写接口描述也能顺带当开发文档用。
另外建议在后端日志里把关键业务操作打出来。我习惯用Lombok的@Slf4j,在Service层每个核心方法入口和出口加上日志,记录参数和结果。比如领养审核通过这个方法,日志里能看到哪只动物、哪个用户、审核结果是什么,排起错来一目了然。
写在最后
这套项目从设计到跑通我花了差不多一周,最深的体会是:业务状态设计得越清晰,代码写起来越省心。动物信息和领养申请的状态机全部在数据库层设计阶段定好,后面Controller和Service的编码就变成了翻译这些字段的机械工作,几乎不用返工。
最后分享一个小技巧:如果你打算拿这个项目去答辩,不要只顺着功能演示,“这只动物从发布到领养成功,经历了哪些状态流转、数据库里哪些字段参与了流转、我这套前端页面在每一步给了用户什么不同的反馈”这条线理顺了,比单纯讲技术栈有用得多。这套系统的细节我还在持续完善,后面有新的业务扩展和优化点,我再接着写出来和大家交流。