简介:这是一套完整的宠物管理系统项目包,涵盖可运行的源码、高保真原型与配套数据库脚本,主要面向需要完成课程设计、毕业设计或快速搭建业务演示的开发者,也可作为Java/Web方向初学者的练习参考。压缩包为RAR格式,共4个文件,两个ZIP压缩包分别封装源码与原型设计,PDF文档提供系统说明或操作指引,SQL脚本包含建表语句与初始示例数据;整体仅1.54MB,轻量易部署。数据库脚本导入后可与源码联动运行,两个ZIP包分开存放便于按需提取设计稿或修改代码,原型文件则可直观展示页面布局与交互逻辑,方便需求评审和方案汇报。目前已有2539人学习下载,直接复用可省去从零搭建的时间,适合用于项目启动、二次开发或教学展示。对课程答辩或项目实训而言,这套包能快速提供可演示的完整流程。
1. 拿到“宠物管理系统”三件套,先跑通源码再画原型
很多接触“宠物管理系统”的人——不管是做课程设计、毕业设计,还是接了一个宠物店的管理后台私活——看到“源码+原型+数据库”三件套,第一反应都是先翻源码。源码是黑匣子、原型是需求清单、数据库是地基。我一般会先把源码里的启动入口跑起来,把登录、新增宠物、寄养下单这几条主流程点一遍,知道它到底能做什么;然后对照原型页面,一个个字段去核对数据库表。这样你拿到的不只是一堆文件,而是一套能说清楚“这个系统为什么长这样”的完整方案。这套东西适合想在短时间内部署交付、又怕被“改需求”反复拖垮的人。
2. 从原型锁定功能边界:宠物档案、寄养订单、库存三张核心页面先画出来
2.1 原型不是画图,是一份可评审的需求清单
做过宠物管理系统的人都有这个体验:需求方说“做个简单的管理后台”,结果做到一半突然问“能不能加个疫苗到期提醒”“能不能在寄养订单里看到宠物主人”。这不是需求方难缠,是需求一开始就没锁死。原型在这里的作用不是画几个好看的框,而是把每个页面、每个按钮、每种状态变成能被逐条确认的清单。
我一般会先列一张页面清单,再决定要不要动笔。清单里只写三列:页面归属哪个模块、这个页面做什么、操作后落到哪些数据。给宠物店看的时候,鼠标一页页点过去,对方只要回答“有”“没有”“不对”三个字,需求边界就清楚了。常见的宠物管理系统至少包含五块:宠物档案、客户(宠物主人)管理、寄养订单、商品库存、统计看板。如果标题里还有疫苗和洗澡预约,就把预约也加进寄养模块,不要单独建页面。
| 页面模块 | 核心页面 | 主要操作 | 涉及数据 |
|---|---|---|---|
| 宠物档案 | 宠物列表 / 新增宠物 | 搜索、筛选、查看详情、编辑 | 宠物基本档案、主人关联 |
| 寄养管理 | 寄养订单列表 / 新建订单 | 下单、续期、结算、取消 | 寄养订单、费用记录 |
| 商品管理 | 商品列表 / 上下架 | 新增商品、改价、停售 | 商品表、库存表 |
| 库存管理 | 库存流水 / 预警 | 入库、出库、盘点调整 | 库存流水、预警阀值 |
| 系统管理 | 员工账号 / 角色 | 增删账号、分配权限 | 系统用户、角色表 |
这张表本身比原型文件更值钱。因为评审时大家盯的是“这个系统要管哪些事”,而不是“这个蓝色好不好看”。原型工具用 Axure RP、墨刀或者即时设计都可以,选你上手最快的,重点是把它当工具用,不要沉迷在交互动画里。
2.2 用低保真线框锁字段:宠物档案页的字段取舍表
很多原型翻车都翻在字段数量上:先画了个特别全的表单,开发时发现一半字段用不上;或者反过来,漏了“体重”“是否绝育”这种日常必填项,上线后再加,又要改数据库表、又要改列表页。我建议在低保真阶段就把宠物档案页的字段定死,像素和配色后面再说。
宠物档案页我一般保留这些字段,取舍逻辑也写在表里。给宠物店演示的时候,他们往往盯着“毛色”争论是“橘白”还是“橘猫白”,这时候要意识到:字段是给查询和统计用的,不是给百科写词条。能做成下拉的不要手填,能存字典的不要直接塞字符。
| 字段 | 类型参考 | 是否必填 | 取舍理由 |
|---|---|---|---|
| 宠物名 | varchar(50) | 必填 | 列表和详情页都会展示,搜索高频 |
| 宠物品种 | 字典表外键 | 必填 | 品种要支持后续扩展,不能写死文本 |
| 出生日期 | date | 选填 | 能算出年龄,比直接存“年龄”字段靠谱 |
| 体重(kg) | decimal(5,2) | 选填 | 洗澡、寄养、用药都要参考,浮点类型会失真 |
| 毛色 | varchar(30) | 选填 | 视觉识别用,文本够用,不做枚举 |
| 是否绝育 | tinyint(1) | 必填 | 寄养和医疗场景需要它做条件判断 |
| 疫苗状态 | tinyint(1) | 必填 | 寄养接待时第一问题就是疫苗是否齐全 |
这个表一出来,数据库基本已经写完一半。原型评审时不要去抠菜单动画是左滑还是右滑,只确认字段和状态。字段确认后,把表发给开发看,开发如果还在问“宠物主人在哪呀”,说明原型里少了客户管理入口,立刻补上,这时候补一张页面只需要半小时,等到建完表再补就要动数据库了。
2.3 把异常流程画进原型:寄养超期、疫苗过期提醒
原型不止画正常路径。多数宠物管理系统翻车都在异常流程上:寄养订单到期没人处理、宠物主人忘了接、疫苗过期还进了寄养间。这些流程如果不在原型里出现,开发阶段就会默认“不做”,等客户验收时才发现少了一块。
我习惯在原型里给每个状态加一行备注。比如寄养订单至少要有四个状态:待入住、寄养中、待结算、已完成,另外再加一个“超期”作为特殊状态。原型页面上只需要有一个状态标签,但标签背后的判断逻辑必须在原型说明里写清:订单超过预计离店日期时,系统把它标记成“超期”,并把宠物主人的联系方式醒目展示。疫苗状态同理,档案里疫苗过期时,列表页置灰,寄养下单时直接拦截。
评审时多问三个问题:谁录入这些数据?这个按钮误点了会发生什么?订单取消后库存怎么回来?这三个问题如果能在原型阶段答上来,后面源码和数据库的改动就会少很多。
3. 把原型落成 MySQL 物理模型:5 张核心表字段边界
3.1 从原型到 ER 模型:三范式在宠物管理系统里怎么让步
数据库设计没有标准答案,我对于这种中小型管理系统有一条经验:范式不要硬套,但核心关系必须清晰。宠物管理系统的业务主线是“宠物主人把宠物送来寄养,顺便买点猫粮狗粮”,围绕这条主线,最少只需要 5 张表:客户表 customer、宠物档案表 pet、寄养订单表 boarding_order、商品表 product、订单明细表 order_item。如果你把管理后台的账号也做进来,再加一张 sys_user,第六张,但不要一上来就设计十几张表。
三范式里有一条“消除冗余”,我在这类系统里会刻意违反它。寄养订单表里我会冗余 customer_name 和 pet_name,而不是只存外键,因为订单是一次历史记录,宠物以后改名字了,订单里显示的名字不应该跟着变。这就是“快照”字段,做报表时少关联两张表,查询速度也快。库存不从订单倒推,单独开“库存流水”的思路也是反范式,但换来了可追溯性。小系统不怕多存几个字的冗余,怕的是多表关联撑不起并发。
选型上,MySQL 就是最稳的选择,InnoDB 引擎、utf8mb4 字符集。不要用 MyISAM,它不支持事务,寄养订单的扣库存操作没有事务兜底就是埋雷。字符集这一点在后面的乱码坑里会再提一次,建库语句里直接写明,免得后面返工。
3.2 核心建表 SQL:宠物档案、寄养订单、商品库存的完整 DDL
下面这套建表脚本是简化过但可以直接运行的最小集,包含前面原型章节里锁定的主要字段。我在关键字段上都写了注释,建立表的时候把注释一起建进去,后面写文档时能少查很多次。
-- 客户表:宠物主人 CREATE TABLE `customer` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(50) NOT NULL COMMENT '主人姓名', `phone` varchar(20) NOT NULL COMMENT '手机号', `address` varchar(200) DEFAULT NULL COMMENT '联系地址', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户(宠物主人)表'; -- 宠物档案表 CREATE TABLE `pet` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `customer_id` bigint unsigned NOT NULL COMMENT '主人ID', `name` varchar(50) NOT NULL COMMENT '宠物名', `species` tinyint NOT NULL COMMENT '1=猫 2=狗 3=其他', `breed` varchar(50) DEFAULT NULL COMMENT '品种文本,原型阶段保留自由录入', `birth_date` date DEFAULT NULL COMMENT '出生日期', `weight_kg` decimal(5,2) DEFAULT NULL COMMENT '体重(kg),最多支持999.99', `hair_color` varchar(30) DEFAULT NULL COMMENT '毛色', `is_neutered` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否绝育:0=否 1=是', `vaccine_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '疫苗是否在有效期:0=过期 1=有效', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建档时间', PRIMARY KEY (`id`), KEY `idx_customer_id` (`customer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='宠物档案表'; -- 寄养订单表 CREATE TABLE `boarding_order` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` varchar(32) NOT NULL COMMENT '业务单号,手动生成,必须唯一', `customer_id` bigint unsigned NOT NULL COMMENT '客户ID', `pet_id` bigint unsigned NOT NULL COMMENT '宠物ID', `customer_name` varchar(50) NOT NULL COMMENT '客户姓名快照', `pet_name` varchar(50) NOT NULL COMMENT '宠物名快照', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0=待入住 1=寄养中 2=待结算 3=已完成 4=超期', `checkin_date` datetime NOT NULL COMMENT '预计入住时间', `checkout_date` datetime NOT NULL COMMENT '预计离店时间', `actual_checkout_date` datetime DEFAULT NULL COMMENT '实际离店时间', `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '应收总额', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_customer_id` (`customer_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='寄养订单表'; -- 商品表 CREATE TABLE `product` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(100) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '销售单价', `stock_qty` int NOT NULL DEFAULT '0' COMMENT '当前库存', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1=上架 0=下架', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表'; -- 商品订单明细表 CREATE TABLE `order_item` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `order_id` bigint unsigned NOT NULL COMMENT '寄养订单ID,这里复用为销售订单ID', `product_id` bigint unsigned NOT NULL COMMENT '商品ID', `product_name` varchar(100) NOT NULL COMMENT '商品名快照', `price` decimal(10,2) NOT NULL COMMENT '成交单价快照', `qty` int NOT NULL COMMENT '购买数量', `subtotal` decimal(10,2) NOT NULL COMMENT '小计金额', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单商品明细表';这段 DDL 里有几个点值得细说。第一是 order_no 专门建了唯一索引,这是防止重复下单的最后一道物理防线,业务代码写错了它也能拦住。第二是 boarding_order 里的 customer_name 和 pet_name 两个冗余字段,前面已经解释过是快照,查询列表页时可以少关联两张表。第三是 decimal 类型的精度,把体重和金额都按实际业务上限来设定,宠物体重最多 999.99kg,金额 99999999.99 对宠物店完全够用,不要随手写个 decimal(255,0) 出来。
外键我在这套表里没有建物理外键,只建了普通索引,比如 idx_customer_id。中小型系统为了性能和解耦,这个做法很常见,删除数据时用业务代码控制。如果你做的是毕业设计,想体现完整性,把 FOREIGN KEY 加回去也可以,但生产环境我建议保留逻辑外键,避免删除客户时被订单表挡住。
3.3 典型查询源码:增删改查与列表分页的 SQL 和 Mapper
表建好只是第一步,项目里真正跑起来的是查询和写入。最常见的两个场景是“宠物档案列表分页”和“寄养下单扣库存”。列表分页我在后端用的是 MyBatis 的 XML,前端传 pageNo 和 pageSize,SQL 里用 limit 加偏移量。注意一点:不要用 PageHelper 一把梭,它的总结数查询在大数据量下会明显变慢,中小系统没事,但理解底层 SQL 会让你排查问题时更有底。
<!-- 宠物档案分页查询 --> <select id="selectPetPage" resultType="com.example.pet.entity.PetVO"> SELECT p.id, p.name, p.species, p.breed, p.weight_kg, p.is_neutered, p.vaccine_status, c.name AS customer_name, c.phone AS customer_phone FROM pet p LEFT JOIN customer c ON c.id = p.customer_id <where> <if test="keyword != null and keyword != ''"> AND (p.name LIKE CONCAT('%', #{keyword}, '%') OR c.name LIKE CONCAT('%', #{keyword}, '%') OR c.phone LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="species != null"> AND p.species = #{species} </if> <if test="vaccineStatus != null"> AND p.vaccine_status = #{vaccineStatus} </if> </where> ORDER BY p.id DESC LIMIT #{offset}, #{pageSize} </select>这段 XML 里的关键点是动态条件全部拼在 where 里面,查询走的是主键排序和覆盖索引;如果加了 vaccine_status 筛选,要确保索引里有这个字段,否则会变成全表扫描。LIKE 查询用在了宠物名和主人手机号上,手机号字段建了索引,但在 LIKE '%keyword%' 这种写法里索引会失效,数据量小的时候无所谓,数据过万后就要考虑分词或者前后缀匹配优化。
下单扣库存是另一个必考场景,直接用事务和行锁来处理。刚才建的 product 表里 stock_qty 就在 product 行上,扣库存和创建订单必须放进同一个事务,否则会出现“订单建了,库存没扣”或者反过来。我一般会在 Service 层写成这样:
@Transactional(rollbackFor = Exception.class) public Long createBoardingOrder(BoardingOrderDTO dto) { // 1. 生成业务单号,带日期和随机数,保证唯一 String orderNo = "BO" + System.currentTimeMillis() + RandomUtil.randomNumbers(4); // 2. 从商品表锁行读取当前库存 Product product = productMapper.selectByIdForUpdate(dto.getProductId()); if (product.getStockQty() < dto.getQty()) { throw new BusinessException("库存不足,当前剩余:" + product.getStockQty()); } // 3. 扣减库存 productMapper.decreaseStock(dto.getProductId(), dto.getQty()); // 4. 创建寄养订单和商品明细 BoardingOrder order = new BoardingOrder(); // 这里省略 order 字段赋值 boardingOrderMapper.insert(order); return order.getId(); }selectByIdForUpdate 是在事务里对商品行加了悲观锁,两个用户同时下单同一件商品时,第二个请求会阻塞到前一个事务提交。对宠物管理系统这种低并发场景,悲观锁简单直接,不会出现库存超卖。参数上要注意的是事务在同一个类里自己调自己时容易失效,我遇到过一次,排查了半天发现是没有走 Spring 代理,这个坑下面的避坑章里细说。
4. 落地避坑:5 个让你返工的数据库与源码问题
4.1 体重和金额字段用错类型,宠物档案数据直接“失真”
现象:数据库里宠物体重显示 5.199999809265137,商品价格 9.9 变成了 9.899999999999999。前端页面小数点后面跟着一长串数字,看起来像系统坏了。
原因:建表时把体重、金额这类精确数值定成了 float 或 double。这两个类型在 MySQL 里是浮点数,底层是近似存储,算出来的结果天然带误差。做订单和计价,哪怕差一分钱都会被客户发现。
解决:已经建好的表,只能改字段类型。先去业务代码里确认有没有依赖这个字段的计算逻辑,再执行 ALTER TABLE:
ALTER TABLE pet MODIFY COLUMN weight_kg DECIMAL(5,2) NOT NULL DEFAULT NULL COMMENT '体重(kg)'; ALTER TABLE product MODIFY COLUMN price DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '销售单价';改完字段后历史数据里的小数尾巴会被四舍五入,此时要立刻抽查几条记录,并且把代码里所有写入该字段的地方都改成 BigDecimal 而不是 Double。这里有个细节,Java 的 BigDecimal 构造时要用字符串,写成 new BigDecimal("19.9"),直接 new BigDecimal(19.9) 等于又把浮点误差带回来了,这是同一条坑的两个入口。
4.2 订单重复提交导致重复扣库存:少了唯一索引和事务
现象:用户快速点了两次“提交订单”,页面第一次没跳转,又点了一次。后台出现两条相同内容的寄养订单,商品库存被扣了两遍。客户当场发火,开发一脸无辜地说“前端背锅”。
原因:前端按钮没有做防重复点击是一层,但后端也有责任——下单接口没有做幂等控制。单靠前端 disabled 按钮拦不住网络重发和客户端工具重放,后端必须有一道物理防线。
解决:两道防线都补上。第一道是数据库层面,boarding_order 表的 order_no 字段加唯一索引,之前 DDL 里已经写进去了。第二道是代码层面,下单前先按 order_no 查一次,存在就直接返回原订单,不再扣库存,同时把创建订单和扣库存包进同一个事务。如果还担心极端并发,就把查重也放进事务里。我这里吃过亏,单独一个 insert 没有异常处理,唯一索引冲突抛出来的 DuplicateKeyException 没接住,导致接口直接 500,前端拿到的是“系统错误”,用户完全看不懂。后来我在事务边界上统一 catch 一下,转成“订单已提交,请勿重复操作”的提示,这事才算闭环。
4.3 列表分页翻到第 100 页变慢:深分页让索引失效
现象:寄养订单列表前 10 页飞快,翻到第 80 页明显卡顿,第 100 页请求耗时超过 5 秒。查看慢查询日志,发现都是同一个分页 SQL。
原因:LIMIT 10000, 20 这样的写法,MySQL 会先扫描 10000 行,然后把前 10000 行丢掉,只返回最后 20 行。数据量到几万条时,这个偏移量带来的扫描成本会线性上升。索引建了也没用,因为 InnoDB 的二级索引和主键排序不一致,回表成本高。
解决:不要用深偏移量,改成延迟关联。常见做法是先查主键,再用主键去关联明细行:
-- 优化前 SELECT * FROM order_item ORDER BY id DESC LIMIT 10000, 20; -- 优化后 SELECT oi.* FROM order_item oi INNER JOIN ( SELECT id FROM order_item ORDER BY id DESC LIMIT 10000, 20 ) tmp ON tmp.id = oi.id;子查询里只查主键,MySQL 扫描索引比扫描全行快得多,拿到主键列表后再回表取完整数据。另一种常用做法是用 last_id 游标代替 offset:前端传“上一次看到的最后一条记录的 id”,后端直接 WHERE id < #{lastId} ORDER BY id DESC LIMIT 20。游标分页翻到第 200 页也不会变慢,只是不能随便跳页。宠物管理系统的订单列表按时间倒序看,游标完全够用。这个优化做完,慢查询直接就消失了。
4.4 中文乱码和连接池耗尽:两个最容易“玄学”的后台故障
中文乱码的现象很典型:宠物名字“橘白”入库后变成“???”,或者读出偶数个问号。我排查过很多次,根源通常不在代码,而在数据库连接串和建库字符集。建库时用了 utf8,MySQL 的 utf8 是 utf8mb3,最多只能存 3 字节,像一些生僻字、emoji 字符直接变问号。正确做法是建库建表统一用 utf8mb4,连接串里显式带上 characterEncoding:
CREATE DATABASE pet_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;jdbc:mysql://localhost:3306/pet_system?useUnicode=true&characterEncoding=utf8mb4&useSSL=false连接池耗尽是另一种“玄学”故障:系统跑几天后突然报“Too many connections”,重启就好了,过几天又出现。这种情况多数是连接池参数没调,默认的连接池连接数不够用,同时慢 SQL 把连接一个个占住不释放。排查时先看 MySQL 的 max_connections 配置,再看连接池空闲超时。调整 HikariCP 参数时我一般这样设置:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000maximum-pool-size 不是越大越好,20 对小项目足够,太大反而消耗数据库资源。idle-timeout 和 max-lifetime 保证连接不会长期占用。配合前面深分页的优化,慢 SQL 少了,连接池自然就稳了。遇到这类问题不要急着去改代码逻辑,先看监控:是连接创建多了,还是连接释放不了,对症下药。
5. 三件套对齐:用字段清单反向校验源码和数据库
项目交付前最后一步,我习惯做一次“三件套对账”:把原型里每个页面的字段,和数据库里每个表的字段,以及源码中增删改查涉及的所有字段,列成一张核对表。这个过程手工做太容易漏,我写过一个简单的 Python 校验脚本,连接数据库后就地扫描 information_schema,把表和字段导出来,再跟手工维护的字段清单做差集。
import pymysql conn = pymysql.connect( host="localhost", user="root", password="your_password", database="pet_system", charset="utf8mb4" ) expected_fields = { "pet": ["id", "customer_id", "name", "species", "breed", "birth_date", "weight_kg", "is_neutered", "vaccine_status"], "boarding_order": ["id", "order_no", "customer_id", "pet_id", "status", "checkin_date", "checkout_date"], } with conn.cursor() as cursor: for table, expected in expected_fields.items(): cursor.execute( "SELECT COLUMN_NAME FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = 'pet_system' AND TABLE_NAME = %s", (table,) ) actual = [row[0] for row in cursor.fetchall()] missing = set(expected) - set(actual) extra = set(actual) - set(expected) print(f"[{table}] missing={missing or '无'} extra={extra or '无'}") conn.close()这个脚本不复杂,但它把校验从人肉对表变成了一行命令。跑完脚本后,我会把所有表字段导到一个 Excel 里,在原型的每个页面下方标注“字段来源:pet 表第 3、5、7 列”,这样客户做验收测试时,指着页面上的任何一个输入框,我都知道数据存在哪张表哪个字段。这个操作看着土,但对交付结算和后面维护都特别有用。
我自己的教训是:做宠物管理系统这类中小型项目,顺序永远是“原型锁字段 → 数据库锁结构 → 源码填逻辑”,不要倒过来。有一回我接手的项目源码先写好,数据库也建好了,最后补的原型,结果原型的“线上预约”功能和源码里的“到店登记”流程对不上,只能返工改一边。后来凡是带“源码+原型+数据库”字样的项目,我都把对账这一步当作验收必经流程,客户提的任何修改也能快速判断出——这个改动要动表结构还是只动页面。希望这套流程和方法能帮到你,少踩一个坑,就少加一次班。
本文还有配套的精品资源,点击获取