简介:工艺卡片系统是面向数据库课程设计的完整项目案例,适合需要完成类似课设任务并顺利通过老师检查的高校学生。资源采用ASP.NET作为开发框架,实现了基于B/S架构的工艺卡片管理平台,覆盖登录注册、卡片查询、后台备份等典型功能,同时包含管理员与普通员工两种角色权限划分,体现了数据库应用中的安全控制思想。整个资源共27个文件,压缩包大小4.07MB,主要文件类型包括aspx页面文件、C#代码文件、SQL Server数据库文件、配置文件及界面截图,设计过程完整覆盖需求分析、E-R图设计、逻辑表定义和物理存储,目前已有245人学习下载。通过学习这份资源,不仅可直接运行查看现场效果,还能对照代码理解表单操作、数据绑定、存储过程等知识点;同时可借助实物截图辅助撰写设计报告,降低答辩阶段被老师追问的风险。
1. 工艺卡片课设落地:先想清楚它能过检查的理由
工艺卡片系统这个选题能在数据库课设答辩里站住脚,靠的不是界面做了多少个窗口,而是它的数据模型恰好覆盖了课程考察的所有关键点:实体关系、外键约束、事务、视图、用户权限。很多同学习惯性把课设做成“能跑就行”,界面堆了七八个面板,数据库里就一张大表硬撑,老师一问“这张表为什么拆不开”“删除时外键怎么处理”,当场卡壳。这个系统围绕机械加工中的工艺文件,把零件、工艺卡片、工序明细、设备和用户串成一条有依赖关系的完整链路,简单到能一个人写完,结构又深到能回答答辩追问,属于“做得完、讲得清、查得严”的典型选题。如果你正在为数据库课程设计选题发愁,这套设计思路可以直接照搬。
2. 从一张工艺卡片反推数据建模:零件、工序与版本的主外键设计
2.1 工艺卡片实体:从车间表单反推表结构
工艺卡片是机械加工车间的工艺规程文件,一张标准的卡片上写着零件号、零件名称、材料、毛坯种类,下面是工序表格:工序号、工序名称、加工内容、使用设备、单件工时。这一张纸就是数据建模的起点。拿到选题先别急着建表,把表单里的字段按“独立变化”的原则拆成实体:零件信息单独成表,因为一个零件会有多张工艺卡片;卡片本身主表,记录编号、版本、状态;工序是明细表,挂在卡片下面,一张卡片对应N道工序;设备被工序引用,单独建表是为了避免设备名称反复写在工序内容里;编制人属于用户表。
设计时还有一个容易被忽略的点:版本。工艺卡片是会改的,改一次工序内容,不应该把旧记录覆盖掉。常见做法是给卡片主表加一个version字段,新建一个版本而不是删旧记录。这一条在答辩时非常好讲——“为什么加版本字段”可以引出范式、唯一约束、历史数据保留三个话题,老师基本挑不出毛病。
实体与业务关系如下:
| 表 | 核心字段 | 与其它表的关系 |
|---|---|---|
| part | part_code, part_name, material | 卡片通过 part_id 引用零件 |
| device | device_code, device_name | 工序通过 device_id 引用设备 |
| sys_user | user_name, password | 卡片通过 creator_id 引用编制人 |
| process_card | card_code, part_id, version, status | 主表,状态区分草稿/发布/作废 |
| process_step | card_id, step_no, step_name, content | 明细表,一张卡片 N 道工序 |
字段之间注意避免“万能字段”陷阱:有人会把工序信息直接拼在卡片表的一个varchar字段里,存成“10-铣削-设备A-1.5小时;20-钻孔-设备B-0.8小时”,表是能建,但查询、统计工时、按工序名检索全部报废,范式上也不达标。既然课设考核的就是设计能力,这种设计会被老师一句话问穿。
2.2 建库脚本:外键、唯一约束与视图一次到位
下面这段 SQL 是整套系统的基础,MySQL 5.7 和 8.0 都能直接跑。工序表的card_id外键用了ON DELETE CASCADE,主表删除时明细自动清理;卡片表的part_id外键则保持默认的RESTRICT,防止零件还在被引用就被删掉。
CREATE DATABASE IF NOT EXISTS process_card_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE process_card_db; CREATE TABLE part ( id INT PRIMARY KEY AUTO_INCREMENT, part_code VARCHAR(32) NOT NULL, part_name VARCHAR(64) NOT NULL, material VARCHAR(32), raw_type VARCHAR(32), UNIQUE KEY uk_part_code (part_code) ) ENGINE=InnoDB; CREATE TABLE device ( id INT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(32) NOT NULL, device_name VARCHAR(64) NOT NULL ) ENGINE=InnoDB; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB; CREATE TABLE process_card ( id INT PRIMARY KEY AUTO_INCREMENT, card_code VARCHAR(32) NOT NULL, part_id INT NOT NULL, version INT NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 0 COMMENT '0=草稿,1=发布,2=作废', creator_id INT NOT NULL, create_time DATETIME NOT NULL, remark VARCHAR(255), UNIQUE KEY uk_card_code (card_code), KEY idx_part_id (part_id), CONSTRAINT fk_card_part FOREIGN KEY (part_id) REFERENCES part(id), CONSTRAINT fk_card_creator FOREIGN KEY (creator_id) REFERENCES sys_user(id) ) ENGINE=InnoDB; CREATE TABLE process_step ( id INT PRIMARY KEY AUTO_INCREMENT, card_id INT NOT NULL, step_no INT NOT NULL, step_name VARCHAR(64) NOT NULL, content VARCHAR(255), device_id INT, hours DECIMAL(6,2) DEFAULT 0, UNIQUE KEY uk_card_step (card_id, step_no), CONSTRAINT fk_step_card FOREIGN KEY (card_id) REFERENCES process_card(id) ON DELETE CASCADE, CONSTRAINT fk_step_device FOREIGN KEY (device_id) REFERENCES device(id) ) ENGINE=InnoDB;这里有几个关键设计点。第一,库和表的字符集统一定成utf8mb4,不要偷懒用默认latin1,否则后面插入中文全是乱码,还得返工。第二,process_step上加了UNIQUE KEY uk_card_step (card_id, step_no),保证同一张卡片内工序号不会重复,这是业务规则在数据库层的落地,比在 Java 代码里if判断更有说服力。第三,级联删除只放在明细表这一侧:删卡片连带删工序是合理的,但删零件时如果它下面还挂着工艺卡片,应该报错提醒,而不是静默把卡片一起删掉。
建完基础表之后,再补一个查询视图。课程设计里查明细是最高频操作,视图能把这层逻辑固定下来,答辩展示时也直观:
CREATE VIEW v_card_detail AS SELECT c.card_code, p.part_code, p.part_name, c.version, c.status, s.step_no, s.step_name, s.content, s.hours, d.device_code, d.device_name, u.user_name FROM process_card c JOIN part p ON c.part_id = p.id LEFT JOIN process_step s ON s.card_id = c.id LEFT JOIN device d ON s.device_id = d.id JOIN sys_user u ON c.creator_id = u.id;LEFT JOIN device而不是INNER JOIN,是因为工序可以不指定设备,保留空设备工序的查询结果。这一步属于数据库设计的“复习题”,把连接、别名、视图全复习了一遍,老师抽查任何一层都能答上来。
2.3 加分项:存储过程与独立账号权限
课设答辩被问“除了增删改查还会什么”,很多同学拿不出东西。最稳的补充是两个:存储过程和权限控制。下面这个存储过程统计各状态卡片数量,简单但能说明你对流程化操作有意识:
DELIMITER // CREATE PROCEDURE sp_count_card_by_status() BEGIN SELECT status, COUNT(*) AS cnt FROM process_card GROUP BY status; END// DELIMITER ;权限部分不要直接用 root 跑应用,单独建一个课设账号,只授业务需要的权限:
CREATE USER 'card_app'@'localhost' IDENTIFIED BY 'Card@123456'; GRANT SELECT, INSERT, UPDATE, DELETE ON process_card_db.* TO 'card_app'@'localhost'; FLUSH PRIVILEGES;这个账号配合 2.2 里的视图一起用,就可以讲“应用账号只能通过视图和基础 DML 操作数据,不能改表结构”,权限边界清清楚楚。带着这套设计进答辩,老师问权的欲望会明显下降。
3. 把增删改查做成三层结构:JDBC 连接层与业务事务边界
3.1 连接层:手动 JDBC 还是连接池
课设阶段一般不允许上 Spring 全家桶,最稳妥的方案是 JDBC + 一个轻量连接池,或者干脆裸DriverManager。我一般建议裸 JDBC 起步,代码量少,逻辑透明,老师问“连接怎么管理”时你能逐行讲明白。连接配置放到db.properties里,不要在 Java 里写死账号密码——工程发布时如何配置数据库,这个问题在答辩高频出现,配置文件就是标准答案。
jdbc.url=jdbc:mysql://localhost:3306/process_card_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true jdbc.username=card_app jdbc.password=Card@123456 jdbc.driver=com.mysql.cj.jdbc.Driver连接串里几个参数各有用途,缺一个就可能翻车:
| 参数 | 作用 | 不设的后果 |
|---|---|---|
| characterEncoding=utf8 | 指定客户端字符集 | 中文写入变成??或乱码 |
| serverTimezone=Asia/Shanghai | 指定时区 | 报Server returns invalid timezone异常 |
| useSSL=false | 关闭 SSL | 本地连接出现证书告警(不影响运行但碍眼) |
| rewriteBatchedStatements=true | 合并批量 SQL | 批量插入几百条时速度慢到难以忍受 |
连接工具类写法如下,每次从连接池拿一个连接,用完归还:
public class DbUtil { private static Properties props = new Properties(); static { try (InputStream in = DbUtil.class.getResourceAsStream("/db.properties")) { props.load(in); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( props.getProperty("jdbc.url"), props.getProperty("jdbc.username"), props.getProperty("jdbc.password")); } }注意静态块里的Class.forName在 JDBC 4.0 之后其实可以省略,但保留它没有坏处,还能体现你知道驱动加载机制。真实项目里这一步换成 HikariCP 的DataSource即可,课设阶段用DriverManager完全够。
3.2 DAO 层:以工序明细为例的查询与写入
三层结构里,DAO 层只做一件事:把 SQL 封装成 Java 对象。以工序明细为例,最常用的查询是按卡片 ID 查出所有工序:
public class ProcessStepDao { public List<ProcessStep> findByCardId(int cardId) { String sql = "SELECT id, step_no, step_name, content, device_id, hours " + "FROM process_step WHERE card_id = ? ORDER BY step_no"; List<ProcessStep> list = new ArrayList<>(); try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, cardId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { ProcessStep s = new ProcessStep(); s.setId(rs.getInt("id")); s.setStepNo(rs.getInt("step_no")); s.setStepName(rs.getString("step_name")); s.setContent(rs.getString("content")); s.setDeviceId(rs.getInt("device_id")); s.setHours(rs.getBigDecimal("hours")); list.add(s); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }写这段代码时注意三点。第一,try-with-resources保证 Connection、PreparedStatement、ResultSet 自动关闭,防止连接泄漏,这是课设代码里最常见的减分项。第二,参数用?占位符而不是拼字符串,既防 SQL 注入,又能在老师追问时讲出预编译的原理。第三,ORDER BY step_no必须写,工序顺序是业务规则,不排序的话明细 Grid 显示会乱。
插入和更新的写法同理,参数按?顺序绑定即可。注意hours建议用BigDecimal接收,数据库DECIMAL(6,2)映射到float/double会在精度上埋雷。
3.3 服务层与事务:整张卡片的原子保存
课设里最常见的错误是把事务写在 DAO 里。一个 DAO 方法开一个连接,cardDao.insertCard()和stepDao.insertStep()各自拿连接,第一个成功第二个失败,卡片主表留下了半截数据。事务必须上浮到 Service 层,用同一个 Connection 贯穿整张卡片的保存:
public class CardService { public void saveCard(Card card, List<ProcessStep> steps) throws SQLException { Connection conn = DbUtil.getConnection(); conn.setAutoCommit(false); try { int cardId = cardDao.insertCard(conn, card); for (ProcessStep step : steps) { stepDao.insertStep(conn, cardId, step); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } } }这段逻辑是“一张工艺卡片包含主表和 N 道工序”这个业务规则的事务化表达。setAutoCommit(false)之后,所有 DAO 方法必须接收外部传入的conn,不能再自己getConnection(),这是三层结构里的隐形约定,代码评审时一眼就能看出来。finally里把setAutoCommit(true)恢复,是为了避免连接归还连接池后,下次复用还带着旧事务设置。
4. Swing 界面接上数据库:列表加载、卡片编辑与下拉联动
4.1 主界面与列表加载:JTable 绑定查询结果
界面层我建议用 Swing 而不是 JavaFX,课设答辩环境对 Swing 兼容性最好,而且资料多。主界面左侧是工艺卡片列表,右侧是工序明细,布局用JSplitPane或者BorderLayout嵌套都行。列表加载的核心是把查询结果反射到DefaultTableModel:
DefaultTableModel model = new DefaultTableModel( new Object[]{"卡片编号", "零件号", "零件名", "版本", "状态", "编制人"}, 0); cardDao.queryCardList().forEach(c -> model.addRow(new Object[]{ c.getCardCode(), c.getPartCode(), c.getPartName(), c.getVersion(), c.getStatusName(), c.getCreatorName() })); table.setModel(model);这里我一般不会写JTable table = new JTable()就完事,而是单独封装一个CardListPanel,把DefaultTableModel的刷新逻辑收敛到loadCards()方法里,新增、修改、作废卡片后统一调一次刷新。列表查询的 SQL 记得ORDER BY create_time DESC,新做的卡片排在最上面,演示时不用翻页找。
4.2 卡片编辑:明细表格与数据库的双向同步
编辑卡片时,用户在下方的 JTable 里增删工序行,保存时把表格内容重新组装成对象列表。这里最容易踩的坑是——JTable 正在编辑的单元格内容不会自动写回TableModel。用户刚输完最后一格就点保存,拿到的还是旧值。保存前必须调用table.getCellEditor().stopCellEditing(),强制提交当前编辑状态。
private void onSave() { if (stepTable.getCellEditor() != null) { stepTable.getCellEditor().stopCellEditing(); } List<ProcessStep> steps = new ArrayList<>(); for (int i = 0; i < stepTable.getRowCount(); i++) { ProcessStep s = new ProcessStep(); s.setStepNo(Integer.parseInt(stepTable.getValueAt(i, 0).toString())); s.setStepName(stepTable.getValueAt(i, 1).toString()); s.setContent(stepTable.getValueAt(i, 2).toString()); s.setDeviceId(parseDeviceId(stepTable.getValueAt(i, 3).toString())); s.setHours(new BigDecimal(stepTable.getValueAt(i, 4).toString())); steps.add(s); } cardService.saveCard(currentCard, steps); loadCards(); }getValueAt返回的是Object,转成具体类型时必须确保列顺序和TableModel的列定义一致。设备列在表格里显示的是“编号 名称”,存库时要把名称剥掉只留编号,这里用自定义的parseDeviceId处理即可。保存后调用loadCards()刷新左侧列表,状态从草稿变成发布时列表同步更新。
4.3 下拉加载设备与 B/S 版改造
工序表里的设备列用下拉框比手输友好。下拉数据来自设备表:
JComboBox<String> deviceBox = new JComboBox<>(); deviceDao.queryAll().forEach(d -> deviceBox.addItem(d.getDeviceCode() + " " + d.getDeviceName()));下拉项存的是“编码 + 名称”的展示串,选中后通过编码前缀解析出deviceId。这里有个经验:外键字段在界面上永远不要只显示数字 ID,用户看不懂“device_id=3”是什么设备,这会直接影响演示效果。如果老师要求做 Web 版,Swing 这一层换成 JSP + Servlet,4.1 到 4.3 里的 DAO 和 Service 完全不用改,这就是当初拆三层的回报。演示时提一句“界面层是可替换的”,老师对架构意识的印象分会加上去。
5. 课设翻车排查:五个数据库设计与提交阶段的高频问题
5.1 中文乱码:插入“铣削端面”变成问号
现象:界面输入中文正常显示,但查看 MySQL 数据表时,所有中文变成了???或白色菱形符号。
原因:建库时用了latin1默认字符集,或者连接串里没加characterEncoding=utf8。MySQL 的乱码通常不是服务器一个环节的问题,而是建库字符集、连接字符集、表字段字符集三层不统一。
解决:建库语句明确写DEFAULT CHARACTER SET utf8mb4;连接串加characterEncoding=utf8;如果数据已经写坏,把表结构改成 utf8mb4 后重新插入,不要在坏数据上直接 UPDATE,字符已经丢失无法还原。
5.2 删除工艺卡片报外键约束错误
现象:删除一张没有工序的卡片能成功,但删除带工序的卡片时报Cannot delete or update a parent row: a foreign key constraint fails。
原因:process_step表里还存在引用该卡片的明细记录,外键约束阻止父表删除。这其实是 InnoDB 外键在正常工作,不是 bug。
解决:在process_step的外键上加ON DELETE CASCADE(见 2.2),删除主表自动清理明细;或者在 Service 层先删明细再删主表。两种方式选一个,不要同时做——CASCADE 之后再手动查明细删除,明细已经为空,delete返回 0 行,反而容易误导 UI 提示逻辑。
5.3 Navicat 连 MySQL 8 报 1251 认证错误
现象:用 Navicat 连接本地 MySQL 8.0,弹出Client does not support authentication protocol requested by server; consider upgrading MySQL client。
原因:MySQL 8.0 默认认证插件是caching_sha2_password,旧版 Navicat 只认mysql_native_password,双方握手失败。这是工具版本兼容问题,不是代码问题,但答辩演示时最容易当场卡住。
解决:开发机上执行下面 SQL 把认证方式改回去:
ALTER USER 'card_app'@'localhost' IDENTIFIED WITH mysql_native_password BY 'Card@123456'; FLUSH PRIVILEGES;如果还连着别的库,别忘了重启服务或重连。正式环境不建议这么改,课设机器为了省时间可以接受。
5.4 批量导入工序时死锁,报 Lock wait timeout exceeded
现象:用 POI 从 Excel 导入几百行工序,跑到一半报Lock wait timeout exceeded; try restarting transaction,而且每次崩的位置不一样。
原因:事务内批量 INSERT 多行时,多个连接并发操作相邻索引区间,InnoDB 的行锁升级成间隙锁,互相等待超时。工序表UNIQUE KEY uk_card_step (card_id, step_no)的存在让并发插入时锁竞争更明显。
解决:三个方向一起改。批量插入在连接串加rewriteBatchedStatements=true,让 JDBC 把多条 INSERT 合并成一条多 VALUES 语句,减少锁持有时间;事务按逻辑拆分,每 50 条提交一次而不是全部插入完才提交;插入前先按step_no排序,保证并发连接访问索引的顺序一致,降低死锁概率。
5.5 界面提示保存成功,重启后数据消失
现象:工艺卡片保存时没有任何报错,关闭程序重启,列表里刚保存的卡片不见了。
原因:代码里执行了setAutoCommit(false),但成功路径上漏了conn.commit(),程序退出时未提交事务被数据库回滚。这种问题特别隐蔽,因为不报错、不抛异常,就是数据“悄悄消失”。
解决:强制走标准事务模板——setAutoCommit(false),所有 DAO 执行完立刻commit(),异常分支rollback(),finally里恢复setAutoCommit(true)并关闭连接。从那以后我每次写事务代码都会检查一遍commit()是否存在,养成习惯了。
6. 进阶验证:用一条 SQL 审计工艺数据完整性并批量导入工序
答辩前最稳的一步,是让数据自圆其说。演示时如果每张卡片都有工序、有工时,整套系统的可信度立刻不一样。下面这条 SQL 能一次性找出所有“空工序卡片”和“零工时工序”,我一般称作工艺数据的完整性审计:
SELECT c.card_code, p.part_name, c.version, COUNT(s.id) AS step_count, COALESCE(SUM(s.hours), 0) AS total_hours FROM process_card c JOIN part p ON c.part_id = p.id LEFT JOIN process_step s ON c.id = s.card_id GROUP BY c.id, c.card_code, p.part_name, c.version HAVING step_count = 0 OR total_hours <= 0;HAVING里直接引用step_count和total_hours别名,MySQL 允许这样写。这条查询至少有三个用途:检查草稿卡片是否漏录工序;检查发布状态的卡片工时是否合计为 0;作为答辩前最后一遍数据检查清单。跑出来的每一行,要么补工序,要么把卡片状态改成作废,别让半成品数据留在演示库里。
同样的思路可以延展成一个批量导入的落地技巧。准备一个 Excel 模板,表头固定为“工序号、工序名称、加工内容、设备编号、工时”,用 POI 读取后组装成List<ProcessStep>,直接复用 3.3 的cardService.saveCard()——事务逻辑已经写好了,批量导入只是数据源的替换。注意导入前先读设备表把设备编号映射成deviceId,查不到的设备编号直接跳过并在日志里提示,避免外键插入失败。
这个批量导入加上完整性审计,正好串起整个系统的闭环:初始数据从 Excel 进库,审计 SQL 兜底检查,工艺卡片从草稿到发布再到作废,零部件版本可追溯。如果还有余力,可以做历史版本回滚——查询同一零件下所有版本的卡片,选中旧版本复制成新版本,这个功能用 2.1 的version字段和一条WHERE part_id = ? ORDER BY version DESC就能实现,但演示效果非常亮眼。
从那以后我做任何数据库课设,交项目前都会强制把审计 SQL 跑一遍,把空数据、零工时、孤立记录全部清掉再截图存档。这个习惯帮我挡掉了不少答辩追问,也省了很多临场改数据的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取