Java数据库课设:小区物业管理系统表结构设计与JDBC实战
发布时间:2026/9/27 4:27:30来源:尧图网络
简介这是一套基于Java与MySQL搭建的小区物业管理系统课程设计资料包面向计算机专业学生、数据库课程设计者及需要快速完成课设的开发者。系统完整覆盖员工信息维护、业主信息维护、停车信息维护、业主投诉信息维护、设备维修信息维护和值班员工信息维护六大功能模块并包含员工基本信息表、业主基本信息表、车位基本信息表、值班信息表等核心数据表可同时用于理解数据库建模与JavaWeb业务开发。压缩包共31个文件含Java源码、class编译文件、依赖jar包、可直接导入MySQL的sql建表脚本、课程设计说明书PDF和附录docx文档整体仅1.81MB。已有1443人浏览学习资源目录与文档配套清晰适合作为课程设计参考、毕业设计子模块或数据库实操练习。借助完整源码、建表脚本和说明书可以快速梳理物业系统的业务逻辑与表关系节省开发与文档撰写时间。1. 课程设计为什么一直有人选小区物业管理系统Java 数据库课设的经典不等于容易每到期末计算机专业的课设目录里一定躺着“小区物业管理系统”。选这个题的人多不是因为它简单而是因为它把 Java 和 MySQL 课设里该展示的东西全凑齐了主外键关系、跨表查询、状态流转、数据库连接配置还有答辩老师最爱追问的增删改查和事务边界。刚拿到题目时最容易慌的不是 Java 语法而是不知道表怎么建、字段怎么定、代码又该从哪里落手。这套系统真正的价值在于业务闭环完整业主、房屋、缴费、报修、车位每条线都有数据进出天然适合拿 MySQL 练手。它适合正在做 Java 数据库课程设计、想找一份能跑通又能讲明白的源码做对照的人。2. 表结构设计九张表把业主、缴费、报修、车位之间的关系钉死在 MySQL 里2.1 从业务闭环反推实体先画数据流再碰建表语句拿到题目之后别急着开 IDE。我习惯先把业务闭环在纸上画一遍业主入住时登记姓名、电话、证件号物业把房屋分配到人名下房子有面积和楼栋门牌物业费按面积生成账单业主缴费后产生流水房子漏水或电梯故障业主发起报修物业派维修工处理完工后回访车位跟着业主走车辆进出留下记录。这条链路走完实体和关系基本就定稿了。业主对房屋是一对多一张房屋下可以有多个报修工单一个业主可以绑多个车位。建表时把外键挂在“多”的那一端主表在“一”的那端。这个步骤很多人会跳过导致建出来的表要么字段冗余要么查询时发现缺关系字段。比如报修工单表里如果只放报修内容不放 house_id后面想查出“哪个业主报修”就得靠房号去猜这种设计在答辩现场很容易被一眼看穿。2.2 建表脚本物业系统的六张核心表可以直接抄下面这套脚本取自核心五张表外加一张支付流水表按课设难度调整过没有存储过程没有视图方便你对着老师一个问题一个问题讲。payment_log 会在缴费模块里用到先把表建出来对应代码在第 4 章。CREATE DATABASE IF NOT EXISTS property_mysql DEFAULT CHARACTER SET utf8mb4; USE property_mysql; DROP TABLE IF EXISTS payment_log; DROP TABLE IF EXISTS fee_record; DROP TABLE IF EXISTS repair_order; DROP TABLE IF EXISTS house; DROP TABLE IF EXISTS owner; DROP TABLE IF EXISTS admin; CREATE TABLE admin ( admin_id INT AUTO_INCREMENT PRIMARY KEY, admin_name VARCHAR(50) NOT NULL COMMENT 管理员账号, password VARCHAR(64) NOT NULL COMMENT MD5或加密后的密码 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员账号表; CREATE TABLE owner ( owner_id INT AUTO_INCREMENT PRIMARY KEY, owner_name VARCHAR(50) NOT NULL COMMENT 业主姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, id_card VARCHAR(30) DEFAULT NULL COMMENT 证件号, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0已注销, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 建档时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT业主表; CREATE TABLE house ( house_id INT AUTO_INCREMENT PRIMARY KEY, owner_id INT NOT NULL, unit_no VARCHAR(20) NOT NULL COMMENT 楼栋单元门牌如3-2-1201, area DECIMAL(10,2) NOT NULL COMMENT 建筑面积, CONSTRAINT fk_house_owner FOREIGN KEY (owner_id) REFERENCES owner(owner_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房屋表; CREATE TABLE fee_record ( fee_id INT AUTO_INCREMENT PRIMARY KEY, house_id INT NOT NULL, fee_type VARCHAR(20) NOT NULL COMMENT 物业费/水费/停车费, amount DECIMAL(10,2) NOT NULL COMMENT 金额, fee_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未缴 1已缴, due_time DATE NOT NULL COMMENT 缴费截止日, pay_time DATETIME DEFAULT NULL COMMENT 实际缴费时间, CONSTRAINT fk_fee_house FOREIGN KEY (house_id) REFERENCES house(house_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT缴费记录表; CREATE TABLE repair_order ( order_id INT AUTO_INCREMENT PRIMARY KEY, house_id INT NOT NULL, content VARCHAR(255) NOT NULL COMMENT 报修内容, report_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待派工 1处理中 2已完成 3已回访, assignee VARCHAR(50) DEFAULT NULL COMMENT 维修工姓名, CONSTRAINT fk_repair_house FOREIGN KEY (house_id) REFERENCES house(house_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报修工单表; CREATE TABLE payment_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, fee_id INT NOT NULL, pay_amount DECIMAL(10,2) NOT NULL, pay_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_way VARCHAR(20) DEFAULT 现金 COMMENT 现金/微信/支付宝, CONSTRAINT fk_pay_fee FOREIGN KEY (fee_id) REFERENCES fee_record(fee_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT支付流水表;建表顺序有讲究。DROP 语句必须从子表往父表删也就是先删 payment_log、fee_record、repair_order再删 house最后删 owner。顺序反了 MySQL 会因为外键依赖直接报错。CREATE 则相反先建被引用的父表。全部表统一用 InnoDBMyISAM 不支持外键和事务课设里用 MyISAM 等于给自己埋雷。外键字段上 MySQL 会自动建索引这在 EXPLAIN 里能看到。答辩时你主动说一句“外键关联的列走了索引”比被问到时再去看强得多。2.3 字段类型与长度选择金额、状态位、日期这三类最容易踩雷缴费记录里 amount 用 DECIMAL(10,2)10 是总位数2 是小数位最多能存 99999999.99物业费完全够用。别用 DOUBLE二进制浮点会在计算 0.1 0.2 这类场景里产生误差答辩现场打印出来很难看。日期字段分开处理due_time 是账单截止日用 DATEpay_time 是缴费瞬间用 DATETIME。状态位用 TINYINT 加 COMMENT代码里再定义常量或枚举去映射别让 0、1、2 散落在 Java 代码里。比如 repair_order.status 的数字含义写在 COMMENT 里别人看表结构就能懂。密码字段的长度我建议直接给 VARCHAR(64)为 MD5 或 SHA-256 预留空间避免存明文。2.4 演示数据造数让联表查询一眼就能看到效果课设系统最怕空表。答辩现场打开缴费记录里面什么都没有JOIN 的教学意义都展示不出来。我一般会在脚本末尾插入几条经过设计的演示数据两户业主、每户两套房、每套房两条账单、一条已完成报修和一条待派工报修。数量不用多但要保证每条 JOIN 都能命中。INSERT INTO owner(owner_name, phone, id_card) VALUES (张伟, 13800001111, 110101199001011234), (李娜, 13900002222, 110101199212120011); INSERT INTO house(owner_id, unit_no, area) VALUES (1, 1-1-101, 89.50), (1, 1-1-102, 89.50), (2, 2-1-301, 120.30); -- 未缴账单截止日期设为当前日期加30天 INSERT INTO fee_record(house_id, fee_type, amount, due_time) VALUES (1, 物业费, 895.00, DATE_ADD(CURDATE(), INTERVAL 30 DAY)), (2, 物业费, 895.00, DATE_ADD(CURDATE(), INTERVAL 30 DAY)), (3, 物业费, 1203.00, DATE_ADD(CURDATE(), INTERVAL 30 DAY)), (3, 停车费, 300.00, DATE_ADD(CURDATE(), INTERVAL 30 DAY));物业费按面积算89.5 平按 10 元一平就是 895 元这个口径在缴费模块生成账单时要保持一致。INSERT 里不要指定自增 ID交给 AUTO_INCREMENT否则后面 owner_id 和 house_id 的关系全乱了。3. 数据库连接与增删改查连接池、PreparedStatement 和事务3.1 为什么课设阶段我会用连接池而不是裸 JDBC初学 JDBC 时都写过这种代码每次请求来了 Class.forName 驱动DriverManager.getConnection用完 conn.close()。单机课设这么写不是跑不起来但有两个问题会让答辩难受一是连接建立开销高页面多刷几次MySQL 端出现大量 TIME_WAIT二是代码里到处散落连接参数改一次密码要翻所有 DAO。课设阶段我会用连接池常见选型是 Druid 或 HikariCP核心配置都差不多。Druid 自带监控页答辩时现场打开连接池监控说服力很强。配置放在 classpath 下的 db.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/property_mysql?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue usernameroot password你的密码 initialSize3 maxActive10 minIdle1 maxWait3000URL 里几个参数逐一说清楚useSSLfalse 是本地开发关掉 SSL 加密serverTimezoneAsia/Shanghai 解决 JDBC 连 MySQL 8.x 的时区报错characterEncodingutf8 管住中文乱码allowPublicKeyRetrievaltrue 是针对 MySQL 8 默认认证插件的第 5 章还会单独讲。连接池初始化放在一个 DbUtil 单例里public class DbUtil { private static final DruidDataSource ds new DruidDataSource(); static { try (InputStream in DbUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties p new Properties(); p.load(in); ds.setDriverClassName(p.getProperty(driverClassName)); ds.setUrl(p.getProperty(url)); ds.setUsername(p.getProperty(username)); ds.setPassword(p.getProperty(password)); ds.setInitialSize(Integer.parseInt(p.getProperty(initialSize))); ds.setMaxActive(Integer.parseInt(p.getProperty(maxActive))); ds.setMinIdle(Integer.parseInt(p.getProperty(minIdle))); ds.setMaxWait(Long.parseLong(p.getProperty(maxWait))); } catch (IOException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }关键点是整个应用只有一个 DataSource所有 DAO 拿连接都走 DbUtil.getConnection()。千万别在 DAO 里 new DruidDataSource否则又退回裸连接的老路。initialSize 是启动时预创建的连接数maxActive 是最大活跃连接数课设并发量很小3 和 10 足够。3.2 owner 的完整 CRUD查、增、改、删一份代码写清楚数据库增删改查是课设验收的硬指标。OwnerDao 这一段我建议直接作为所有 DAO 的模板其它模块复制后改表名和字段就行。public class OwnerDao { // 查询全部业主按主键排序 public ListOwner listAll() throws SQLException { String sql SELECT owner_id, owner_name, phone, id_card, status FROM owner ORDER BY owner_id; ListOwner list new ArrayList(); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Owner o new Owner(); o.setOwnerId(rs.getInt(owner_id)); o.setOwnerName(rs.getString(owner_name)); o.setPhone(rs.getString(phone)); o.setIdCard(rs.getString(id_card)); o.setStatus(rs.getInt(status)); list.add(o); } } return list; } // 新增业主 public int insert(Owner o) throws SQLException { String sql INSERT INTO owner(owner_name, phone, id_card) VALUES(?, ?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, o.getOwnerName()); ps.setString(2, o.getPhone()); ps.setString(3, o.getIdCard()); return ps.executeUpdate(); } } // 更新业主手机号 public int updatePhone(int ownerId, String phone) throws SQLException { String sql UPDATE owner SET phone ? WHERE owner_id ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, phone); ps.setInt(2, ownerId); return ps.executeUpdate(); } } }这段代码的套路是 try-with-resourcesConnection、PreparedStatement、ResultSet 都实现了 AutoCloseableJava 7 之后可以自动关闭不用手写 finally。三个资源写在一个 try 里关闭顺序按照创建逆序自动处理代码干净不少。返回的 int 是受影响行数insert 时如果返回 0 说明没插进去前端可以据此提示用户。这里想提醒一点owner 表里我建了 status 字段代表“正常/注销”所以真正的删除操作并不直接 DELETE而是 UPDATE status0。这个设计到第 5 章讲外键删除时还会再展开。3.3 SQL 注入防护与级联删除的事务边界数据库课设里最容易翻车的就是 SQL 注入。下面这种写法在很多参考代码里还有String sql SELECT * FROM owner WHERE phone phone ;如果 phone 传进来一段13900002222 OR 11整条 SQL 就变成了恒真条件登录接口直接绕过。你不需要真的去黑自己的系统只要在答辩现场把这段代码递给老师分数基本就悬了。正确做法是参数绑定String sql SELECT * FROM owner WHERE phone ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, phone);PreparedStatement 会先把 SQL 交给 MySQL 预编译再把参数按类型传进去用户输入只被当成字符串值不会参与 SQL 语法解析。所以只要不手贱做字符串拼接注入这条路基本就堵死了。再来看一个需要事务支撑的完整操作删除业主。因为 owner 被 house 引用house 又被 fee_record 和 repair_order 引用直接 DELETE 会撞外键。正确流程是在事务里从子表往父表依次删public void deleteOwner(int ownerId) throws SQLException { String[] delSqls { DELETE FROM fee_record WHERE house_id IN (SELECT house_id FROM house WHERE owner_id ?), DELETE FROM repair_order WHERE house_id IN (SELECT house_id FROM house WHERE owner_id ?), DELETE FROM house WHERE owner_id ?, DELETE FROM owner WHERE owner_id ? }; try (Connection conn DbUtil.getConnection()) { conn.setAutoCommit(false); for (String sql : delSqls) { try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, ownerId); ps.executeUpdate(); } } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } }四条 DELETE 必须放在同一个事务里任何一步失败都要回滚否则可能出现报修单删了、房屋没删掉这种半截数据。外键约束不是摆设它保证了“脏数据进不来”但代价就是删除操作必须有序。这里用到了子查询fee_record 通过 house_id 找到属于该业主的所有账单SQL 逻辑上是允许的注意别在同一条语句里既删目标表又查目标表就行。4. 三个拿得出手的模块登录拦截、缴费状态流转、报修工单联表4.1 登录鉴权与 Session 过滤器绕开登录页的请求全拦住物业管理系统最前面是管理员登录。如果只在前端判断“是否登录”那用户直接访问后台页面 URL 就能绕过。课设里我用一个过滤器把全局请求拦一道WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String uri request.getRequestURI(); // 登录页、登录接口和静态资源直接放行 if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png)) { chain.doFilter(req, resp); return; } // 未登录的请求全部打回登录页 if (session null || session.getAttribute(admin) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这里有个细节request.getSession(false) 如果传 true会强制创建一个新 Session未登录用户每次访问都会多一个垃圾 Session。传 false 表示“有就取没有就返回 null”配合后面的空判断更严谨。如果课设用的是 Spring Boot把 Filter 换成 HandlerInterceptor 的 preHandle判断逻辑完全一样。4.2 缴费模块账单生成、支付确认与状态机的落地缴费模块是一个小状态机账单创建时 fee_status0支付成功后变成 1。重复支付必须在 SQL 里挡掉不能只靠前端按钮置灰。支付确认方法里同时更新 fee_record 和写入 payment_log天然需要事务public void confirmPay(int feeId, BigDecimal amount, String payWay) throws SQLException { String updateFee UPDATE fee_record SET fee_status 1, pay_time NOW() WHERE fee_id ? AND fee_status 0; String insertLog INSERT INTO payment_log(fee_id, pay_amount, pay_way) VALUES(?, ?, ?); try (Connection conn DbUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(updateFee); PreparedStatement ps2 conn.prepareStatement(insertLog)) { ps1.setInt(1, feeId); int rows ps1.executeUpdate(); if (rows 0) { throw new SQLException(账单不存在或已支付); } ps2.setInt(1, feeId); ps2.setBigDecimal(2, amount); ps2.setString(3, payWay); ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } }UPDATE 里带fee_status 0是幂等判断的关键两条请求同时进来只有第一条能更新成功第二条的 rows 为 0直接抛异常回滚。课设里如果只查到 feeId 就去更新不做状态判断连点两次缴费按钮就会生成两条流水这个坑特别隐蔽。账单生成逻辑反向对应第 2 章的演示数据按房屋面积乘单价插入 fee_record 时 fee_status 默认 0。公式是 amount area * unitPrice面积从 house 表取单价按 fee_type 从配置项读。答辩时能把这个口径讲清楚比单纯展示 CRUD 加分得多。4.3 报修工单的联表查询一张 SQL 讲清楚多表 JOIN报修列表页是展示 JOIN 的最佳场地。页面既要显示报修内容还要带出业主姓名和房屋门牌号数据分散在 repair_order、house、owner 三张表里。查询语句这样写SELECT r.order_id, o.owner_name, h.unit_no, r.content, r.status, CASE r.status WHEN 0 THEN 待派工 WHEN 1 THEN 处理中 WHEN 2 THEN 已完成 WHEN 3 THEN 已回访 END AS status_name FROM repair_order r JOIN house h ON r.house_id h.house_id JOIN owner o ON h.owner_id o.owner_id WHERE r.report_time ? ORDER BY r.report_time DESC LIMIT ? OFFSET ?;JOIN 的顺序是从业务主表 repair_order 出发先搭上 house再搭上 owner。这里用 INNER JOIN因为每张报修工单必然关联一个房屋和业主不需要保留无匹配的行。如果哪天要统计“从未报修过的业主”才需要反过来用 LEFT JOIN 从 owner 出发。LIMIT 和 OFFSET 是分页参数LIMIT 是每页条数OFFSET 是跳过的行数第二页就是 OFFSET 10。CASE 表达式把数字状态翻译成中文展示层就不需要再做一次循环转换。这套查询配合前端的分页组件就是完整的报修列表页。还可以加一条统计 SQL 放在首页仪表盘按状态分组展示工单数量SELECT status, COUNT(*) AS cnt FROM repair_order GROUP BY status;这条语句短但能展示 GROUP BY 的掌握程度首页放四个数字卡片比空荡荡的欢迎页好看得多。5. 避坑清单mysql 连接失败、中文乱码、SQL 注入等五个课设现场5.1 mysql 连接失败 2002多半不是密码错而是服务没起来现象命令行执行mysql -uroot -p报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock (2)。原因这个错误和账号密码无关是客户端根本找不到 MySQL 服务。常见场景是刚装完 MySQL、服务没启动或者 Linux 下解压版 MySQL 没有注册成 systemd 服务。网上很多 mysql 安装配置教程讲到 root 能登录就停了导致你换一台机器或重启一次之后第一步就卡在这里。解决Linux 上先看服务状态systemctl status mysqld没启动就systemctl start mysqld。Windows 上打开服务管理器找 MySQL 服务手动启动。另一个办法是绕过 socket 强制走 TCPmysql -h127.0.0.1 -P3306 -uroot -p。Java 连接串同样建议写 127.0.0.1 而不是 localhost因为某些平台会把 localhost 解析成 socket 连接JDBC 找不到 socket 文件就报错。5.2 MySQL 8.0 认证方式导致连接被拒Public Key Retrieval 报错现象程序启动时报Public Key Retrieval is not allowed有时还伴随Access denied for user rootlocalhost。密码明明是对的Workbench 能连上Java 程序连不上。原因MySQL 8 默认认证插件换成了 caching_sha2_password客户端第一次连接时需要用 RSA 公钥做密码传输JDBC 驱动出于安全考虑默认不主动去取公钥于是一上来就被拒。解决两条路任选。一是在连接串加参数allowPublicKeyRetrievaltrue同时保留useSSLfalse本地开发没问题二是在 MySQL 端把 root 改回旧认证方式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;。课设环境我一般改连接串因为不影响数据库全局配置换台机器跑也不用重新设密码。5.3 中文乱码一个地方统一不够三层都得统一现象页面录入“张三”MySQL 表里显示“???”或“寮犱笁”另一种情况是表里正常页面显示乱码JSP 页面上全是问号。原因连接字符集、数据库表字符集、页面编码三层只要有一层不一致就乱。最典型的是连接串没带 characterEncodingutf8或者建库时没指定字符集落到 MySQL 默认的 latin1 上。解决建库时显式写DEFAULT CHARACTER SET utf8mb4建表语句里也带DEFAULT CHARSETutf8mb4连接串里加characterEncodingutf8页面端 JSP 顶部写pageEncodingUTF-8HTTP 响应头也要是 UTF-8。强调一下直接用 utf8mb4 而不是 utf8MySQL 的 utf8mb4 才是完整的 UTF-8 编码遇到生僻字或 emoji 不会二次踩坑。注意只改库不改进程的三层要同步。5.4 PreparedStatement 防注入占位符不是简单的字符串替换现象代码里已经用了 PreparedStatement但模糊查询还是查不出结果或者登录接口依然能被注入。原因很多人把 LIKE 的百分号写进了 SQL 字符串比如WHERE name LIKE %?%。这个写法下? 已经不是占位符MySQL 把%?%当成字面量预编译完全失效。另一个翻车写法是参数值本身没加百分号导致模糊查询变成了精确查询。解决占位符只写一个?通配符合并进参数值里ps.setString(1, % keyword %)。所有用户输入都走 setString 或 setInt 传参不拼接字符串。判断是否安全有个土办法把 PreparedStatement 的 SQL 打印出来如果看到参数位置是 ? 而不是展开后的值说明预编译生效如果看到拼接好的完整语句赶紧改成占位符。5.5 删除业主被外键挡住物理删除的代价和软删除的取舍现象执行DELETE FROM owner WHERE owner_id 1;时 MySQL 报Cannot delete or update a parent row: a foreign key constraint fails。原因删除父表记录时子表仍有引用。这是外键在保护数据完整性正确做法不是把外键删掉——答辩老师看到你为了能删数据而去掉外键一定会追问到逻辑漏洞。解决方案一事务里按子表到父表的顺序依次删除第 3 章的 deleteOwner 就是这个思路但历史流水也会一起没掉方案二业务层做软删除owner 表已有 status 字段页面删除按钮改为UPDATE owner SET status 0 WHERE owner_id ?查询时统一加AND status 1。课设答辩我更推荐方案二好讲且不会误删数据。但要注意所有关联查询都得带上 status 过滤漏一处列表里就会出现已注销业主挂在报修单上的诡异画面。6. 验收自检清单把课设从“能跑”抬到“能答辩”6.1 环境检查与现场演示顺序课设代码能跑只是第一步演示过程能不能流畅走完才是关键。我每次验收前会按下面这个表过一遍顺序不要乱检查项操作通过标准数据库服务systemctl is-active mysqld 或服务管理器确认MySQL 进程存活建表脚本完整执行一次第 2 章脚本无外键报错六张表齐全演示数据执行 INSERT 脚本查询 fee_record账单、业主、房屋均能查到登录链路先输错误密码再输正确密码错误被拒正确进入首页缴费闭环对一条未缴账单执行确认支付fee_status 变 1payment_log 新增流水报修联表打开报修列表页翻第二页JOIN 查询正常分页不重复日志与控制台看后端控制台无红字异常中文无乱码答辩时我习惯准备一段固定演示脚本先讲表关系说清楚 owner 和 house 的一对多、house 和 fee_record 的一对多再登录系统故意点开一条待派工报修把它改成处理中展示状态流转然后去缴费模块确认一笔未缴账单立刻打开 Workbench 查 payment_log告诉老师“刚才那笔支付已经写进流水表两个表在同个事务里提交”。这一套下来数据库设计和代码结构都覆盖到了。演示前夜最容易翻车的反而不是业务逻辑而是环境。我有一次课设答辩前改连接串把端口写成 3307现场演示时登录页转圈三分钟老师直接让我回座位根本没机会展示系统。从那以后我每次进演示环境都强制走一遍流程先确认 MySQL 服务在跑再重跑建表脚本和演示数据然后登录、缴一笔费、查一次报修联表最后看一眼控制台有没有红字。这套课设源码包下载下来后你也先用这个顺序完整跑一遍再改自己的库名、密码和演示数据后面做功能扩展时就不容易中途翻车。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网