资讯动态

送水系统数据库课设:触发器、存储过程与库存月度统计实操详解

发布时间:2026/9/13 4:28:46 来源:尧图企业网站定制
简介面向数据库课程设计的送水公司送水系统完整方案涵盖需求分析、数据库设计与实现。资源包含课程设计报告、可执行SQL脚本及数据库备份文件共3个文件压缩包仅388KB报告内附清晰的设计思路、流程图和E-R图并针对工作人员与客户信息、矿泉水类别与供应商、入库出库管理等模块给出了建表与操作实现。SQL脚本进一步实现了触发器在入库、出库时自动增减对应矿泉水数量提供存储过程统计指定月份送水员工送水量以及查询当月用水量最大的前10位用户并按用水量递减排序同时建立了表间参照完整性约束。适合正在完成数据库课程设计或需要复习SQL触发器、存储过程与完整性控制的学生参考。已有1542人学习下载。1. 送水系统数据库课设库存、触发器与月度统计一次讲透做过数据库课设的人都知道选题最怕两种一种是图书管理系统另一种是学生选课系统表结构闭着眼都能写答辩时老师问两句就露馅。送水系统这个题目不一样它有真实的业务流动——水从供应商进库再由送水员工送到客户手里中间牵扯库存增减、员工绩效统计、大客户用水量排名刚好把触发器、存储过程、参照完整性这几个数据库课的硬知识点全串起来。这份课设资源包含完整的 SQL 脚本、.bak 备份文件和设计报告报告里附了流程图和 E-R 图拿到之后不是直接交上去就完事而是要学会看懂表之间的关系、触发器的执行时机以及存储过程的入参设计。下文按建表、触发器、存储过程、报告验收的顺序把整个系统的实现思路和可复现代码拆开讲。2. 从 E-R 图到建表送水系统的表结构与参照完整性约束设计2.1 以业务单据为核心拆解实体设计数据库的第一步不是急着写 CREATE TABLE而是把业务流程翻译成实体关系。送水公司的核心业务是「进」和「出」矿泉水从供应商采购进来入库客户打电话订水送水员工出库配送。所以实体至少包含人员、商品、往来单位和单据这几类。人员分两种工作人员和客户。工作人员里有管理库存的也有送水的客户记录的是订水的单位或个人。商品就是矿泉水但不同品牌、不同规格的矿泉水要区分开定价所以单独建矿泉水类别表。供应商负责供货商家需要知道每一批水是从哪进的。这些实体之间靠入库单和出库单关联起来一个入库单对应多行入库明细一行明细对应一种矿泉水数量自己记。这样设计的好处是主表存单据的公共信息日期、经办人、供应商明细表存具体商品和数量避免了字段冗余也方便后面对明细做统计——触发器直接作用在明细表上即可。E-R 图在报告里建议画成三种关系工作人员与客户之间是送水服务关系一个员工可以服务多个客户供应商与矿泉水是一对多一个供应商供多种水矿泉水与入库/出库明细是多对多通过明细表实现。图中重点突出「入库明细」和「出库明细」两类联系因为触发器就挂在它们上面。下图文字描述只列核心实体实体关键字段说明工作人员工号、姓名、电话、角色角色区分管理员/送水员客户客户编号、名称、地址、电话支持按地区统计矿泉水类别类型编号、名称、规格、单价、库存量库存量由触发器维护供应商供应商编号、名称、联系人、电话入库单的往来方入库单/明细单号、日期、供应商、操作员类型、数量主表明细表出库单/明细单号、日期、客户、送水员类型、数量明细表关联触发器2.2 建表 SQL 与外键约束的写法下面给出 SQL Server 版本的核心建表脚本这套脚本在 SSMS 里直接执行即可。表名用英文字段名用业务缩写方便写存储过程时引用。-- 工作人员表 CREATE TABLE staff ( staff_id INT IDENTITY(1,1) PRIMARY KEY, -- 工号自增 staff_name NVARCHAR(20) NOT NULL, role NVARCHAR(10) DEFAULT 送水员, -- 管理员 / 送水员 phone NVARCHAR(20) ); -- 客户表 CREATE TABLE customer ( cust_id INT IDENTITY(1,1) PRIMARY KEY, cust_name NVARCHAR(50) NOT NULL, address NVARCHAR(100), phone NVARCHAR(20) ); -- 矿泉水类别表库存量字段 stock_qty 受触发器控制 CREATE TABLE water_type ( type_id INT IDENTITY(1,1) PRIMARY KEY, type_name NVARCHAR(50) NOT NULL, spec NVARCHAR(50), -- 规格如 18.9L/桶 price DECIMAL(10,2), stock_qty INT DEFAULT 0 -- 当前库存数量 ); -- 供应商表 CREATE TABLE supplier ( sup_id INT IDENTITY(1,1) PRIMARY KEY, sup_name NVARCHAR(50) NOT NULL, contact NVARCHAR(20), phone NVARCHAR(20) ); -- 入库主表 CREATE TABLE inbound_order ( in_id INT IDENTITY(1,1) PRIMARY KEY, sup_id INT NOT NULL, staff_id INT NOT NULL, in_date DATE DEFAULT GETDATE(), FOREIGN KEY (sup_id) REFERENCES supplier(sup_id), FOREIGN KEY (staff_id) REFERENCES staff(staff_id) ); -- 入库明细表 CREATE TABLE inbound_detail ( in_id INT NOT NULL, type_id INT NOT NULL, qty INT NOT NULL CHECK (qty 0), PRIMARY KEY (in_id, type_id), FOREIGN KEY (in_id) REFERENCES inbound_order(in_id) ON DELETE CASCADE, FOREIGN KEY (type_id) REFERENCES water_type(type_id) );逻辑说明明细表的主键是(in_id, type_id)联合主键防止同一张入库单里出现重复商品的记录。ON DELETE CASCADE表示删除主表单据时自动清掉它的明细这在测试阶段很实用否则删一张入库单要手动删多行明细。CHECK (qty 0)约束数量必须为正。出库表结构与入库表类似只是把sup_id换成cust_id另外加一个staff_id表示哪个送水员送的单这个字段在月度统计存储过程中会被用到。至此参照完整性已经建立外键约束保证明细表中的类型、员工、客户必须在主表中存在联合主键避免重复记录CHECK 约束拦截非法数量。课程设计报告里要把这段脚本的执行结果截图放进去并用一段文字说明每个外键解决的是哪类业务异常——例如没有外键时可以录入一个不存在的供应商编号业务上就变成「无来源的进货」这在报表里会留下脏数据。3. 触发器实现矿泉水库存自动增减入库与出库的完整写法3.1 为什么必须用触发器维护库存库存量stock_qty放在water_type表里如果每次入库、出库都靠应用程序去 UPDATE很容易出现两个问题一是漏更新业务代码里少写一行就导致库存和明细对不上二是并发时丢失更新两个窗口同时出库最后只有一方的修改生效。触发器把库存更新固化到数据库内部只要有人往明细表插数据数据库就强制同步库存无论数据来自 SSMS、Java 程序还是手工 SQL行为完全一致。选择 AFTER INSERT 而不是 INSTEAD OF因为明细表的数据本身需要保留我们只是希望在插入完成后补做一次 UPDATEAFTER 触发器不会干扰原始插入动作。对于出库还要考虑库存不足的问题触发器里可以提前判断也可以依赖 UPDATE 后的检查。比较稳妥的做法是插入后如果库存为负抛出异常让整个事务回滚这样既能保证数据一致又能在报告里多写一个「异常处理」亮点。3.2 入库触发器库存自动增加CREATE TRIGGER trg_inbound_stock ON inbound_detail AFTER INSERT AS BEGIN SET NOCOUNT ON; UPDATE wt SET wt.stock_qty wt.stock_qty i.qty FROM water_type AS wt INNER JOIN inserted AS i ON wt.type_id i.type_id; END;说明inserted是 SQL Server 触发器特有的临时表保存本次插入的所有行它的结构和inbound_detail一致。触发器可以处理一次插入多行明细的情况所以用INNER JOIN关联而不是逐行处理。SET NOCOUNT ON避免触发器执行时返回额外的行数信息防止程序端受影响行数出现异常。如果一次插入 100 行明细这个 UPDATE 会一次性把 100 种矿泉水的库存全部加上没有循环和游标性能方面没有压力。3.3 出库触发器库存减少与防负数CREATE TRIGGER trg_outbound_stock ON outbound_detail AFTER INSERT AS BEGIN SET NOCOUNT ON; -- 如果任何一种水库存不足回滚整个事务 IF EXISTS ( SELECT 1 FROM inserted i INNER JOIN water_type wt ON i.type_id wt.type_id WHERE i.qty wt.stock_qty ) BEGIN THROW 51000, 库存不足出库失败, 1; ROLLBACK TRANSACTION; RETURN; END; UPDATE wt SET wt.stock_qty wt.stock_qty - i.qty FROM water_type AS wt INNER JOIN inserted AS i ON wt.type_id i.type_id; END;逻辑说明先检查本次出库涉及的每一种矿泉水若任意一种的申请数量大于当前库存则抛出错误并回滚后续的 UPDATE 不会执行。THROW后面的参数分别是错误号、错误信息和状态位。没有把ROLLBACK TRANSACTION放在THROW之前是因为THROW本身如果在一个事务内会中断批处理此时事务需要显式回滚实际执行时更常见的写法是IF EXISTS (...) BEGIN RAISERROR(库存不足, 16, 1); ROLLBACK TRANSACTION; RETURN; END;两者都能达到目的。注意触发器和明细插入是在同一个事务里的所以ROLLBACK会把明细表的插入一并撤销不会出现「明明库存不够明细却留下了」的中间状态。在测试时可以先插入一条库存为 5 的水再尝试出库 10观察系统报错且库存不变这个测试过程要写进课设报告。与直接写代码更新库存相比触发器方案把业务规则下沉到数据库层。送水系统规模不大这种方式完全够用而且在课设答辩时老师往往会问「库存同步怎么做」触发器是标准答案。唯一的边界是如果后续系统要支持多数据源同步或者需要把库存变化异步通知给其他系统触发器就会变成扩展性的阻碍这是实际生产环境需要考虑的事课设阶段不用自找麻烦。4. 存储过程处理月度统计送水员送水量与用水大户排行4.1 统计指定送水员工指定月份的送水总量需求里明确要「统计每个送水员工指定月份送水的数量」这里的数量指的是出库明细中的qty总和。注意不是出库单数量因为一张出库单可能包含多种水要按明细聚合。存储过程接收两个参数员工编号和月份字符串。CREATE PROCEDURE proc_staff_monthly_delivery staff_id INT, month VARCHAR(7) -- 格式2025-06 AS BEGIN SET NOCOUNT ON; SELECT s.staff_id, s.staff_name, month AS stat_month, SUM(d.qty) AS total_delivery FROM outbound_order o INNER JOIN outbound_detail d ON o.out_id d.out_id INNER JOIN staff s ON o.staff_id s.staff_id WHERE o.staff_id staff_id AND CONVERT(VARCHAR(7), o.out_date, 120) month GROUP BY s.staff_id, s.staff_name; END;参数说明month用VARCHAR(7)接收YYYY-MM格式CONVERT(VARCHAR(7), o.out_date, 120)把out_date转成同样的格式然后等值比较这样能用到日期列上的索引吗不一定因为对列做了转换索引会失效。更好的写法是用范围比较WHERE o.out_date month -01 AND o.out_date DATEADD(MONTH, 1, month -01)这样是纯范围扫描out_date列的索引能派上用场。课设数据量小两种写法结果一样但在报告的存储过程说明里我会把这种索引细节写出来老师会觉得你不只是照抄代码。GROUP BY s.staff_id, s.staff_name确保同一员工只输出一行汇总结果。4.2 查询指定月份用水量最大的前 10 个客户这部分要用TOP 10加ORDER BY递减注意递减是DESC。统计口径仍然以出库明细为基准因为客户一次订多桶水明细才是实际消费量。CREATE PROCEDURE proc_top10_customer month VARCHAR(7) AS BEGIN SET NOCOUNT ON; SELECT TOP 10 c.cust_id, c.cust_name, SUM(d.qty) AS water_quantity FROM outbound_order o INNER JOIN outbound_detail d ON o.out_id d.out_id INNER JOIN customer c ON o.cust_id c.cust_id WHERE CONVERT(VARCHAR(7), o.out_date, 120) month GROUP BY c.cust_id, c.cust_name ORDER BY water_quantity DESC; END;如果希望相同用水量的人共享排名可以改用RANK() OVER (ORDER BY SUM(d.qty) DESC)但题目要求「前 10 个用户」TOP 10更直接。water_quantity是别名在ORDER BY里可以直接引用SQL Server 允许聚合列的别名参与排序。调用方式是EXEC proc_top10_customer 2025-06;运行结果里会出现 10 行或不足 10 行如果当月客户少于 10 个。这个存储过程对于送水公司很实际——大客户往往是企事业单位他们需要优先保障供水和定期回访这份排名可以直接导出成报表。4.3 存储过程与触发器配套的坑一个很容易被忽略的问题是存储过程里查询的是outbound_detail而库存更新是靠触发器做的这两者不在同一个时间点发生。如果应用在插入出库单后立刻查询库存MySQL 中可能需要等待触发器提交但 SQL Server 里触发器在执行时和原语句处于同一个隐式事务中所以查询一定发生在事务提交之后不会读到旧库存。但如果程序端把多条 SQL 放在一个显式事务里事务隔离级别默认是 READ COMMITTED事务内部能看到自己的修改其他连接要等提交后才能看到。另一个坑是参数类型不一致。如果应用程序传来的月份是202506而存储过程里写的是2025-06转换就会失败。建议在程序端统一格式化或者在存储过程里用FORMAT或STUFF做一次兼容处理比如先判断长度再插入分隔符。课设报告里可以把这种传参规范性单独列一点它属于数据库应用的常见错误能体现你的调试经验。使用存储过程的另一个收益是权限管理给应用程序账号只授权EXECUTE不给直接读写表权限这样外部程序无法绕过触发器手工改库存。设计报告里可以画一张访问链路的图应用 → 存储过程 → 基表 触发器。送水系统业务不复杂这个链路既能满足需求又能把数据库课程的权限控制知识点用上。5. 课设报告如何验收还原.bak、测试用例与答辩前的边界检查5.1 还原备份并验证触发器逻辑.bak文件是 SQL Server 的完整备份拿到后先还原再验证比直接跑.sql更省事。在 SSMS 中右键「数据库」→「还原数据库」源设备选择该.bak文件目标库名保持默认或改成自己的名字。还原成功后执行下列验证脚本-- 查看现有触发器和存储过程 SELECT name, type_desc FROM sys.triggers WHERE parent_class_desc OBJECT_OBJECT; SELECT name AS proc_name FROM sys.procedures;如果开发环境没有 SQL Server也可以直接在装有数据库课设环境的机器上用sqlcmd还原sqlcmd -S localhost -E -Q RESTORE DATABASE WaterCompany FROM DISKC:\path\送水系统.bak WITH REPLACE;WITH REPLACE覆盖同名数据库使用前确认不要误伤已有数据。还原后测试触发器的标准动作是先查某类型水的库存SELECT * FROM water_type;再插入一张出库单和明细立刻再查库存。前后对比如果数量差了出库量说明触发器生效。测试用例建议做成表格放进报告测试操作预期结果实际结果插入入库明细 10 桶对应库存 10通过插入出库明细 3 桶对应库存 -3通过库存为 2 时出库 5抛出异常事务回滚通过删除入库主表明细级联删除通过5.2 用 SQL 脚本构造测试月份数据演示存储过程必须有月度数据人工插入太慢写一段生成测试数据的脚本更实用-- 先插入 12 个客户和 20 条出库记录 INSERT INTO customer(cust_name, address, phone) SELECT 客户 CAST(n AS VARCHAR(3)), 地址 CAST(n AS VARCHAR(3)), 1380000 RIGHT(000 CAST(n AS VARCHAR(3)), 4) FROM (SELECT TOP 12 ROW_NUMBER() OVER (ORDER BY object_id) n FROM sys.objects) t; -- 随机生成出库单日期落在 2025 年 6 月 INSERT INTO outbound_order(cust_id, staff_id, out_date) SELECT 1 ABS(CHECKSUM(NEWID())) % 12, 1 ABS(CHECKSUM(NEWID())) % 5, DATEADD(DAY, ABS(CHECKSUM(NEWID())) % 30, 2025-06-01) FROM sys.objects; -- 为每张出库单插入 1~2 条明细 INSERT INTO outbound_detail(out_id, type_id, qty) SELECT o.out_id, 1 ABS(CHECKSUM(NEWID())) % 3, 1 ABS(CHECKSUM(NEWID())) % 10 FROM outbound_order o;CHECKSUM(NEWID())生成随机值ABS(...) % n把随机值约束到 1~n 区间。注意插入明细前要保证water_type表里至少有 3 种水且初始库存足够大否则触发器会拦截并回滚。跑完数据后执行两个存储过程EXEC proc_staff_monthly_delivery staff_id 1, month 2025-06; EXEC proc_top10_customer month 2025-06;得到的排名结果可以直接截图进报告并把每列含义解释清楚。如果某个送水员当月数量为 0不是 bug说明该员工没有在这个月出过单存储过程不会返回该员工的行这是 SQL 聚合的内在逻辑——没有明细就没有分组行。要显示所有员工包括 0 值得用左连接把staff表作为主表这可以作为报告的进阶思考题。5.3 报告里容易被忽视却加分的细节流程图的绘制重点是入库和出库两条线入库流程是「供应商 → 入库单 → 入库明细 → 更新库存」出库流程是「客户订单 → 出库单 → 出库明细 → 检查库存 → 更新库存」。E-R 图不要画成孤立表要标出联系上的基数比如客户与出库单是 1:N入库单与入库明细是 1:N。答辩时高频问题集中在三个地方为什么用触发器而不用应用层代码、存储过程和普通 SELECT 相比有什么优势、库存不足时怎么处理。回答思路分别对应事务一致性、网络开销与权限控制、事务回滚。另外报告里附上.sql文件和.bak文件的说明注明每个文件的作用——.sql是纯脚本可教学演示.bak是还原用的完整备份.doc是设计文档。最后检查一遍数据库名字是否统一避免出现「某送水公司」和WaterCompany混用导致还原时找不到对象。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价