尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
电力公司收费系统数据库实战:表设计、存储过程与并发事务控制
简介数据库课程设计《电力公司收费管理信息系统》配套文档面向需要完成同类系统设计与数据库实验的学生。文档从课程实验目的和设计规定出发以客户、用电类型、员工、用电信息、费用管理、收费登记六大核心表为主线覆盖关系模型、E-R 图、Oracle 建表语句与示例数据、视图/触发器/存储过程实现并附有系统概要设计、数据流程图、程序流程图和功能模块图可帮助读者理清从需求分析、关系建模到数据库落地的完整流程。资源包共 1 个文件类型为 doc 文档包体大小约 261KB内容集中便于按章节查阅。已有 241 人浏览学习。文档不仅给出各表的字段结构、主外键关系与插入数据示例还结合收费登记场景说明了收费自动更新结余、月份格式规则等关键实现思路适合作为课程设计参考或答辩准备材料。1. 数据库课程设计里的电力公司收费系统表面是文档核心是数据模型与事务如果你在数据库课程设计里翻到“电力公司收费系统”这个题目通常拿到的是一份 .doc 任务书要求做的不是排页面而是把“用户—电表—抄表—账单—缴费”这一整条钱相关的链路用数据库串起来。这类题目年年有因为考得特别集中表结构、约束、索引、事务、存储过程、报表全在里面。适合正在做课程设计的学生也适合理一遍 JDBC 与 MySQL 的最佳实践。我帮人看过几十份这类作业90% 的翻车都在同一个点上业务规则散在 Java 里数据库只当成“存数据的盒子”结果重复出账、金额对不上、并发漏单全来了。2. 把电力公司收费系统拆成六张核心表实体、外键与关键约束课程设计拿到题目先别急着写代码第一步一定是画 E-R 图、定关系模式。电力收费系统表面业务很多但抽象下来无非六个核心对象客户、电表、抄表读数、电价标准、账单、缴费记录。把这六张表定清楚了后面的存储过程和页面都是水到渠成的事。2.1 实体与关系为什么 meter 和 bill 都要做历史快照客户和电表是一对多关系一个客户可以有多块电表但一块电表只属于一个客户。电表和抄表读数是一对多每块表每月一条读数。电价标准要单独建表因为居民阶梯电价不是写死在代码里的一两个 if而是“档位区间 单价 生效日期”的结构化数据以后调价只改表不用改程序。账单表是关键。它不能只存客户和账期必须把begin_reading、end_reading、usage_amount、amount_due这些快照字段一起存进去。原因很实际账单一旦生成就是历史凭证之后电表再抄多少条读数都不该影响这笔已出账的数据。如果报表每次都靠实时计算等电价调整、数据补录之后历史账单金额会被悄悄改掉对账时根本说不清。所以 bill 表存的是“出账那一刻的完整状态”不是“指向某个状态的引用”。2.2 最小可运行 DDL六张表一句话讲清字段设计下面这套 MySQL 建表语句我一般直接作为课程设计的基础版本MySQL 5.7 和 8.0 都能跑。注意先删子表再删父表避免外键检查报错。CREATE DATABASE IF NOT EXISTS power_billing DEFAULT CHARACTER SET utf8mb4; USE power_billing; DROP TABLE IF EXISTS payment; DROP TABLE IF EXISTS bill; DROP TABLE IF EXISTS reading; DROP TABLE IF EXISTS tariff; DROP TABLE IF EXISTS meter; DROP TABLE IF EXISTS customer; -- 客户表 CREATE TABLE customer ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, customer_no VARCHAR(20) NOT NULL COMMENT 客户编号业务唯一键, name VARCHAR(50) NOT NULL COMMENT 户主姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, address VARCHAR(120) DEFAULT NULL COMMENT 用电地址, id_card VARCHAR(18) DEFAULT NULL COMMENT 身份证号Excel导入时按文本处理, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_customer_no (customer_no) ) ENGINEInnoDB; -- 电表 CREATE TABLE meter ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, meter_no VARCHAR(20) NOT NULL COMMENT 表号, customer_id INT UNSIGNED NOT NULL COMMENT 所属客户, install_date DATE NOT NULL COMMENT 安装日期, initial_reading DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 安装时表底首次出账用, meter_type VARCHAR(10) NOT NULL DEFAULT single COMMENT 单相/三相, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0停用, UNIQUE KEY uk_meter_no (meter_no), KEY idx_customer (customer_id), CONSTRAINT fk_meter_customer FOREIGN KEY (customer_id) REFERENCES customer(id) ) ENGINEInnoDB; -- 抄表记录 CREATE TABLE reading ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, meter_id INT UNSIGNED NOT NULL, read_date DATE NOT NULL COMMENT 抄表日期, read_value DECIMAL(12,2) NOT NULL COMMENT 本次表盘读数, operator_name VARCHAR(30) DEFAULT NULL COMMENT 抄表员, source_type TINYINT NOT NULL DEFAULT 0 COMMENT 0现场抄表 1远程采集 2手工补录, UNIQUE KEY uk_meter_date (meter_id, read_date), CONSTRAINT fk_reading_meter FOREIGN KEY (meter_id) REFERENCES meter(id) ) ENGINEInnoDB; -- 阶梯电价标准 CREATE TABLE tariff ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, effective_date DATE NOT NULL COMMENT 生效日期支持调价, tier_no TINYINT NOT NULL COMMENT 第几档, min_range DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 该档用量下限, max_range DECIMAL(12,2) DEFAULT NULL COMMENT 该档用量上限NULL表示不封顶, unit_price DECIMAL(6,4) NOT NULL COMMENT 单价元/度, UNIQUE KEY uk_tier (effective_date, tier_no) ) ENGINEInnoDB; -- 账单 CREATE TABLE bill ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, bill_no VARCHAR(32) NOT NULL COMMENT 账单号对外凭证, customer_id INT UNSIGNED NOT NULL, meter_id INT UNSIGNED NOT NULL, period VARCHAR(10) NOT NULL COMMENT 账期如2025-04, begin_reading DECIMAL(12,2) NOT NULL COMMENT 期初表底, end_reading DECIMAL(12,2) NOT NULL COMMENT 期末表底, usage_amount DECIMAL(12,2) NOT NULL COMMENT 用电量, amount_due DECIMAL(12,2) NOT NULL COMMENT 应缴金额, due_date DATE NOT NULL COMMENT 缴费截止日, status VARCHAR(10) NOT NULL DEFAULT UNPAID COMMENT UNPAID/PAID, paid_time DATETIME DEFAULT NULL, UNIQUE KEY uk_bill_no (bill_no), UNIQUE KEY uk_meter_period (meter_id, period), KEY idx_customer_status (customer_id, status), CONSTRAINT fk_bill_customer FOREIGN KEY (customer_id) REFERENCES customer(id), CONSTRAINT fk_bill_meter FOREIGN KEY (meter_id) REFERENCES meter(id) ) ENGINEInnoDB; -- 缴费记录 CREATE TABLE payment ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, receipt_no VARCHAR(40) NOT NULL COMMENT 票据号, bill_no VARCHAR(32) NOT NULL COMMENT 关联账单号, customer_id INT UNSIGNED NOT NULL, pay_amount DECIMAL(12,2) NOT NULL COMMENT 实收金额允许预存或找零, pay_method VARCHAR(20) NOT NULL COMMENT 现金/微信/支付宝/对公, operator_name VARCHAR(30) DEFAULT NULL COMMENT 收费员, pay_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_receipt_no (receipt_no), KEY idx_bill_no (bill_no), CONSTRAINT fk_payment_bill FOREIGN KEY (bill_no) REFERENCES bill(bill_no) ) ENGINEInnoDB;字段类型有几个点必须说明。金额全部用DECIMAL(12,2)电价用DECIMAL(6,4)绝不能用 float 或 double原因后面避坑章节专门讲。id_card在数据库里用 varchar不是 int否则身份证会溢出变成科学计数法。这是从 Excel 导入客户资料时最容易踩的坑。外键关联上payment.bill_no引用bill.bill_no要求被引用的列有唯一索引所以 bill 表里建了uk_bill_no。reading表加了uk_meter_date唯一键从数据库层面保证同一块表同一天只能抄一次这是后面防重复出账的第一道防线。2.3 索引和唯一约束数据不乱的关键课程设计评分里“数据库设计合理性”占了很大比重而评卷人最常看的就是约束和索引。上面这套表里已经埋了几个关键点我整理成一张速查表约束 / 索引所在表作用uk_customer_nocustomer客户编号唯一业务上不允许两个客户共用编号uk_meter_nometer表号唯一电表是实物资产编号必须全局唯一uk_meter_datereading同一表同一天只能有一条读数防止重复抄表uk_meter_periodbill同一表同一账期只能有一张账单防重复出账uk_receipt_nopayment票据号唯一缴费凭证不能重复idx_customer_statusbill支撑“查某客户未缴账单”的常用查询这里有个容易忽略的细节uk_meter_date和uk_meter_period这类唯一键本身就是索引所以不需要再单独为meter_id或period建普通索引。唯一键同时承担了“查重”和“加速查询”两个职责再加索引就是冗余。真正需要额外建的是idx_customer_status因为“按客户查账单状态”是收费系统最高频的查询而它又不是唯一约束必须手动补上。我一般会提醒做一个查询习惯把上面这六张表的关系和约束抄进课程设计文档的“关系模式”一节E-R 图上的每一个 1:n 关系都要能在这里找到对应的外键。评卷时这一页和后面的 SQL 脚本是重点看的关系模式写不全后面做得再花哨也容易扣分。3. 用存储过程做抄表出账和缴费业务规则收进数据库表结构定了接下来是最核心的业务环节抄表出账和缴费。很多人习惯在 Java 代码里先查出上次读数、再算电费、再 insert 两条记录这样做不是不行但并发一上来就会出错而且业务规则散落在好几层代码里改一次阶梯电价要动三个地方。3.1 为什么把计费规则放进数据库而不是 Java 代码我的做法是把“抄表、算费、出账”做成一个存储过程把“缴费、核销、写票据”做成另一个存储过程。Java 端只负责传参数和拿结果不直接拼 insert 语句。理由有三个。第一计费规则要保证唯一来源。阶梯电价、逾期违约金这类规则只有一份写在数据库里JSP、Swing、小程序端都调用同一个过程不会出现“网页端一个算法、桌面端另一个算法”的尴尬。第二一致性需要事务边界。抄表要同时插 reading 和 bill 两张表缴费要同时改 bill 状态和插 payment这两个操作天然是一个事务而事务的边界放存储过程里最清晰。第三课程设计评卷时“存储过程 事务 锁”是明确的加分点比在 Java 里写一百行业务逻辑更有说服力。3.2 抄表出账存储过程读数校验、阶梯计价、账单生成一步完成下面这个存储过程接收表计 ID、账期、本次读数和抄表员先校验重复再取上次表底计算用电量按居民阶梯电价算钱最后在同一个事务里同时插入抄表记录和账单。MySQL 5.7 以上都支持。DELIMITER $$ CREATE PROCEDURE sp_generate_bill( IN p_meter_id INT, IN p_period VARCHAR(10), IN p_read_value DECIMAL(12,2), IN p_operator VARCHAR(30), OUT p_bill_no VARCHAR(32) ) BEGIN DECLARE v_last_value DECIMAL(12,2); DECLARE v_customer_id INT; DECLARE v_usage DECIMAL(12,2); DECLARE v_amount DECIMAL(12,2) DEFAULT 0; DECLARE v_duplicate INT DEFAULT 0; -- 异常时整体回滚避免只插入了抄表记录却没有账单 DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; -- 幂等检查同一表计同一账期只允许出一次账 SELECT COUNT(*) INTO v_duplicate FROM bill WHERE meter_id p_meter_id AND period p_period; IF v_duplicate 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 该表计本账期已出账不能重复抄表; END IF; -- 取客户ID SELECT customer_id INTO v_customer_id FROM meter WHERE id p_meter_id; -- 取上次表底优先取最近一次抄表值没有则用安装表底 SELECT IFNULL(MAX(r.read_value), m.initial_reading) INTO v_last_value FROM meter m LEFT JOIN reading r ON r.meter_id m.id WHERE m.id p_meter_id; SET v_usage p_read_value - v_last_value; -- 读数回退必须拦截 IF v_usage 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 本次读数小于上次抄表值请现场复核; END IF; -- 居民阶梯电价第一档0.52元/度第二档0.57元/度第三档0.82元/度 IF v_usage 260 THEN SET v_amount ROUND(v_usage * 0.52, 2); ELSEIF v_usage 600 THEN SET v_amount ROUND(260 * 0.52 (v_usage - 260) * 0.57, 2); ELSE SET v_amount ROUND(260 * 0.52 340 * 0.57 (v_usage - 600) * 0.82, 2); END IF; -- 生成账单号并插入两张表 SET p_bill_no CONCAT(BILL, DATE_FORMAT(NOW(), %Y%m%d%H%i%s), p_meter_id); INSERT INTO reading(meter_id, read_date, read_value, operator_name, source_type) VALUES(p_meter_id, CURDATE(), p_read_value, p_operator, 0); INSERT INTO bill(bill_no, customer_id, meter_id, period, begin_reading, end_reading, usage_amount, amount_due, due_date, status) VALUES(p_bill_no, v_customer_id, p_meter_id, p_period, v_last_value, p_read_value, v_usage, v_amount, DATE_ADD(CURDATE(), INTERVAL 15 DAY), UNPAID); COMMIT; END$$ DELIMITER ;这段代码里有几个地方值得细看。ROLLBACK出现在SIGNAL之前是因为 MySQL 的SIGNAL会抛出异常并进入EXIT HANDLER如果不在SIGNAL前手动回滚事务里已经做的操作并不会自动撤销。EXIT HANDLER FOR SQLEXCEPTION捕获所有 SQL 异常后统一回滚并RESIGNAL把错误原样抛给调用端这样 Java 里能拿到明确的报错信息而不是只看到存储过程执行失败。阶梯电价目前是写死在过程里的三个档位。如果想让课程设计更完整可以把档位区间和单价改成从tariff表动态读取每次出账前按effective_date取当天生效的价格。不过写死版本的逻辑更直观适合答辩时讲清楚“为什么这么算”动态版本则更贴近真实电力公司做法两者选一个即可。3.3 缴费存储过程SELECT FOR UPDATE 与事务边界缴费比抄表更敏感因为它直接涉及钱和票据。缴费动作包含两步把 bill 状态改成 PAID再往 payment 插一条缴费记录。这两步之间如果并发必须锁住账单行否则可能发生“两个人同时缴同一笔单都查到未缴结果一个缴了两次另一个交的钱凭空消失”。DELIMITER $$ CREATE PROCEDURE sp_pay_bill( IN p_bill_no VARCHAR(32), IN p_amount DECIMAL(12,2), IN p_method VARCHAR(20), IN p_operator VARCHAR(30), OUT p_receipt_no VARCHAR(40) ) BEGIN DECLARE v_status VARCHAR(10); DECLARE v_amount_due DECIMAL(12,2); DECLARE v_customer_id INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; -- 锁定账单行直到事务结束防止并发重复缴费 SELECT status, amount_due, customer_id INTO v_status, v_amount_due, v_customer_id FROM bill WHERE bill_no p_bill_no FOR UPDATE; IF v_status PAID THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 该账单已缴费不要重复支付; END IF; IF p_amount v_amount_due THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 缴费金额小于应缴金额; END IF; -- 票据号用时间戳加随机值避免并发下重复 SET p_receipt_no CONCAT(REC, DATE_FORMAT(NOW(), %Y%m%d%H%i%s), UUID_SHORT()); UPDATE bill SET status PAID, paid_time NOW() WHERE bill_no p_bill_no; INSERT INTO payment(receipt_no, bill_no, customer_id, pay_amount, pay_method, operator_name, pay_time) VALUES(p_receipt_no, p_bill_no, v_customer_id, p_amount, p_method, p_operator, NOW()); COMMIT; END$$ DELIMITER ;SELECT ... FOR UPDATE是关键。它会在读取 bill 行时加排他锁另一个事务再执行同一个缴费过程时只能等前一个事务提交或回滚。前一个提交后后一个读到的是 PAID 状态直接抛“不要重复支付”这就避免了“并发漏单”和“重复入账”两类问题。如果你想把课程设计做得更严谨缴费金额不足的校验可以支持“部分缴费 记录欠费余额”。但课程设计通常不需要做那么深保持“一笔账单一次性缴清”就够了重点是把事务和锁讲明白。日常的数据库增删改查里锁是最容易讲不清的点答辩论据落在这几句上很加分。4. JDBC 调用与页面数据Java 端最小闭环存储过程写好后Java 端的工作量其实只剩三层获取连接、调用过程、渲染结果。这一步很多人会卡在 JDBC 驱动和连接参数上所以我先把连接配置和调用代码完整给出。4.1 连接配置与分层先把驱动和连接池的坑填上课程设计一般用 MySQL 8.xJava 端对应mysql-connector-j8.x 驱动。连接 URL 我习惯写成下面这样String url jdbc:mysql://localhost:3306/power_billing ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String user root; String password your_password;serverTimezoneAsia/Shanghai是 MySQL 8.0 之后的必备参数不写会报时区错误。useSSLfalse是本地开发必备避免自签名证书的告警刷屏。字符集一定要带utf8否则页面显示中文乱码这是课程设计检查时最常见的翻车现场。我一般建议在项目里单独写一个DBUtil类负责拿连接业务层不直接写连接串。如果是 Swing 或 JSP 的小作业用DriverManager.getConnection就能交差不必强上连接池。但如果你已经会用 HikariCP写上反而能体现工程能力只是别把连接池参数调得太激进课程设计环境数据量小默认配置就够。4.2 调用存储过程CallableStatement 的参数与返回值Java 调用存储过程用CallableStatement比拼 insert 语句干净得多。以抄表出账为例public String generateBill(int meterId, String period, BigDecimal readValue, String operator) throws SQLException { String sql {call sp_generate_bill(?,?,?,?,?)}; try (Connection conn DBUtil.getConnection(); CallableStatement cs conn.prepareCall(sql)) { cs.setInt(1, meterId); cs.setString(2, period); cs.setBigDecimal(3, readValue); cs.setString(4, operator); // 注册输出参数第5个?是账单号 cs.registerOutParameter(5, Types.VARCHAR); cs.execute(); return cs.getString(5); } }这里的{call sp_generate_bill(?,?,?,?,?)}是 JDBC 调用存储过程的标准写法五个问号对应存储过程的五个参数。前四个是IN参数用setXxx传值第五个是OUT参数必须先用registerOutParameter注册类型执行后再用getString取返回值。用 try-with-resources 可以自动关闭连接和语句省掉 finally 块里的一堆close()。缴费调用同理只是多传一个支付方式参数再取一个票据号返回public String payBill(String billNo, BigDecimal amount, String method, String operator) throws SQLException { String sql {call sp_pay_bill(?,?,?,?,?)}; try (Connection conn DBUtil.getConnection(); CallableStatement cs conn.prepareCall(sql)) { cs.setString(1, billNo); cs.setBigDecimal(2, amount); cs.setString(3, method); cs.setString(4, operator); cs.registerOutParameter(5, Types.VARCHAR); cs.execute(); return cs.getString(5); } }有一点要提醒存储过程里是用SIGNAL抛的业务异常Java 端会被包装成SQLExceptione.getMessage()里能看到“该账单已缴费”这类中文提示可以直接弹给用户。课程设计里很多人会漏掉这层导致页面只显示“SQLException”用户根本不知道是重复缴费还是金额不足。4.3 账单列表和欠费查询PreparedStatement 的常用增删改查页面上的账单列表、欠费查询这类只读操作不需要走存储过程直接用PreparedStatement写 SQL 就够了。重点是传参永远用?占位符不要拼字符串既能防 SQL 注入也省去处理引号转义的麻烦。public ListMapString, Object queryBills(String customerName, String status) throws SQLException { String sql SELECT b.period, b.bill_no, c.customer_no, c.name, b.usage_amount, b.amount_due, b.status FROM bill b JOIN customer c ON b.customer_id c.id WHERE c.name LIKE ? AND b.status ? ORDER BY b.id DESC LIMIT 100; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % customerName %); ps.setString(2, status); try (ResultSet rs ps.executeQuery()) { // 把ResultSet转为ListMapString,Object返回给页面层渲染 } } }LIKE查询时%要放在参数里拼不能直接写进 SQL 模板的?位置。status参数传UNPAID能直接查欠费账单后台催缴页面就用这条。报表类的汇总查询和复杂统计放到后面第 6 章那里是对账的核心。5. 收费系统 5 个高频翻车点现象、原因、解决这个系统我见过太多次在同一批问题上栽跟头而且这些问题在课程设计答辩时几乎一问一个准。这里按“现象—原因—解决”写出来照着排查能省大量时间。5.1 重复出账抄表按钮点两次当月账单翻倍现象用户在前台页面多点了一次“保存抄表”月底一看同一块表的同一账期出现两张账单金额翻倍。原因Java 代码里没有先查 bill 表有没有已存在记录或者说查了但没走事务两个请求同时进来都查到“不存在”然后各自插入一条。解决数据库层面用uk_meter_period唯一键兜底应用层再在存储过程里做幂等检查。真正有效的防线是唯一键因为任何应用层的“先查后插”在并发下都有窗口期。-- 手工复核是否存在重复账单 SELECT meter_id, period, COUNT(*) FROM bill GROUP BY meter_id, period HAVING COUNT(*) 1;5.2 电费差一分钱float 的精度账现象收费员月底对账银行收款和系统应收总是差几分钱查明细发现某些账单的金额是 110.549999 或 130.000001。原因建表时金额字段用了 float 或 double十进制小数转二进制浮点数本身就是不精确的累加多了误差就暴露。解决所有金额字段已经统一改成DECIMAL(12,2)这是数据库设计阶段就要定的规矩。DECIMAL是定点数存储和计算都按十进制精度处理不会出现二进制浮点的“玄学误差”。另外在存储过程里算金额时用ROUND(x, 2)保证每一步都四舍五入到位。5.3 并发缴费丢单缺少 FOR UPDATE 的读改写现象同一张账单用户同时用微信和柜台缴费两个请求都显示缴费成功但 payment 表只有一条记录或者两条记录对应同一张账单之后状态混乱。原因Java 代码里的逻辑是“查状态 → 改状态 → 插记录”三步之间没有锁两个线程同时读到 UNPAID各自继续执行。解决见 3.3 的SELECT ... FOR UPDATE把“读状态、校验、更新、插入”收敛成存储过程里的一个事务。数据库并发锁是这类金额操作的标准答案没有别的捷径。5.4 抄表出账途中崩溃reading 有记录但账单没生成现象程序在插入抄表记录后、插入账单前抛了异常表里多一条 reading但 bill 里没有记录下个月出账时上月用量被吞掉或者账单对不上。原因两条 insert 没放在同一个事务里或者虽然放在 Java 的 try 块里但事务没有正确回滚。解决把整个抄表出账逻辑收敛到存储过程用START TRANSACTIONEXIT HANDLER保证两条 insert 原子提交。这条也适用于缴费缴费的 update 和 insert 必须同生共死。5.5 票据号重复用 MAX(id)1 生成流水号现象缴费高峰期两个窗口同时开票报“票据号重复”或者票据号错乱。原因代码写成SELECT MAX(id)1 FROM payment再拼到票号里。这个做法在并发下读到的最大值是一样的后插入的那个必然撞唯一键。解决用数据库生成号或随机值我上面用了UUID_SHORT()拼时间戳单纯演示也可以用UUID()只是票据号的长度和可读性差一点。业务编号不该依赖“先查再算”这是分布式系统里人人都懂、课程设计里人人会犯的规则。6. 交付前最后一步用三条 SQL 做全量对账并导出报表存储过程能保证单笔业务正确但不能保证整体数据没被历史脏数据污染。我的习惯是交付前跑一遍“对账脚本”把系统里的应收、实收、欠费、表底异常一次查清楚顺带把课程设计报告里最需要的统计报表数据导出来。第一个脚本是应收实收汇总按账期统计每天或每月的数据这组数字会直接出现在报告的“系统测试”章节SELECT b.period, COUNT(*) AS bill_count, SUM(b.amount_due) AS receivable_amount, SUM(CASE WHEN b.status PAID THEN b.amount_due ELSE 0 END) AS paid_amount, SUM(CASE WHEN b.status UNPAID THEN b.amount_due ELSE 0 END) AS unpaid_amount FROM bill b GROUP BY b.period ORDER BY b.period DESC;第二个脚本是欠费客户排名给催缴页面用也用来验证缴费状态更新是否正常SELECT c.customer_no, c.name, COUNT(b.id) AS unpaid_count, SUM(b.amount_due) AS total_arrears FROM bill b JOIN customer c ON b.customer_id c.id WHERE b.status UNPAID GROUP BY c.id ORDER BY total_arrears DESC LIMIT 20;第三个脚本专门抓“表底回退”异常。正常情况下电表读数只增不减一旦出现回退说明有补录错误或程序 bugWITH rd AS ( SELECT meter_id, read_date, read_value, LAG(read_value) OVER (PARTITION BY meter_id ORDER BY read_date) AS prev_value FROM reading ) SELECT m.meter_no, rd.read_date, rd.prev_value, rd.read_value FROM rd JOIN meter m ON m.id rd.meter_id WHERE rd.read_value rd.prev_value;这三个脚本对 MySQL 8.0 有效窗口函数和 CTE 是 8.0 才有的特性。如果用的是 5.7就用子查询改写效果相同。跑完这三条应收、实收、欠费三栏能对平没有读表回退记录我才会认为这套收费系统的数据是可交付的。这是我做数据库课程设计多年留下的习惯——不查对账不交作业哪怕页面再漂亮数据对不上就是白做。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

MBD三维模型智能标注落地指南:从PMI语义到规则引擎

MBD三维模型智能标注落地指南:从PMI语义到规则引擎

简介:一份面向制造业设计、工艺与检验人员的PDF技术资料,围绕基于MBD的三维模型智能标注技术展开,针对传统二维工程图在信息传递中易遗漏数据、影响设计意图理解等痛点,给出以三维实体模型为核心承载完整制造信息的解决思路。资源…

📅 2026/10/11 15:46:42
YOLOv5打电话行为检测:从数据集训练到PyQt界面部署全流程

YOLOv5打电话行为检测:从数据集训练到PyQt界面部署全流程

简介:本资源面向计算机视觉入门与进阶开发者,提供一套完整的YOLOv5打电话行为检测方案,可用于课堂演示、安防场景原型验证或毕业设计参考。包内包含训练好的打电话识别权重与配套数据集,标注同时提供txt和xml两种格式并分目录存放…

📅 2026/10/11 15:46:42
3ds Max+Vray系统设置指南:单位、Gamma与备份一个都不能少

3ds Max+Vray系统设置指南:单位、Gamma与备份一个都不能少

简介:这是一套面向环境艺术与三维设计初学者的培训课程幻灯片,聚焦软件概述与系统设置,从界面四视图、主工具栏到几何体与样条线的创建方法均有清晰讲解。资源共1个PPT文件,压缩包约8.29MB,便于直接演示或自学。内容涵…

📅 2026/10/11 15:46:42
MORE NEWS

更多资讯

📰

AI原生应用API编排层高可用:超时、重试、幂等与降级实战

先说个背景。去年我在维护一个智能客服系统时,发现生产环境的故障有一大半不是模型幻觉,也不是底层模型服务宕机,而是API编排层在压力下先撑不住了。一次简单的多轮对话会依次触发意图识别、知识库检索、工具调用、大模型生成,中间…

📰

Unity相机与刚体物理实战:跟随、碰撞与抖动排查指南

不用从“Unity是什么”讲起,直接进入正题。相机和刚体这两个模块,是Unity项目里最容易“看起来没问题、一跑就翻车”的地方。相机决定了玩家看到什么,刚体决定了物体怎么动,而两者一旦组合起来——比如第三人称角色、物理载具、可…

📰

Unity相机与刚体系统核心要点与实战调优指南

1. 项目概览:为什么相机和刚体是Unity开发的“地基” 这两年我带过不少新人,也帮团队review过好几次项目代码,发现一个有意思的现象:很多朋友能熟练地拖拽预制体、写UI逻辑、调Shader,但一碰到相机跟随抖动、物体碰撞穿…

📰

Mac Agent 实时控制接线指南:laya-mlx 让端侧响应快到没感知

Mac Agent 实时控制接线指南:laya-mlx 让端侧响应快到没感知 【免费下载链接】laya-mlx Native MLX runtime for Laya typed decision models — 7–14 ms short decisions on M3 Max. No text generation, PyTorch, or cloud API. 项目地址: https://gitcode.com…

📰

无服务器MLOps实战:从数据集工程到PyTorch分布式训练

简介:《MLOps工程化实践》是一本面向具备一定机器学习基础的工程师与数据科学家的PDF电子书,聚焦大规模机器学习系统的工程化落地。全书围绕MLOps核心原则与无服务器架构的融合展开,系统讲解从数据准备、模型训练到部署监控的全流程自动化&am…

📰

MQTT在工业物联网中的四大不适场景与选型框架

1. 为什么我要给MQTT泼一盆冷水三年前,我第一次把MQTT协议部署到一条真实的产线环境里。当时团队里几乎所有人都觉得这是“天选方案”——轻量、发布订阅、支持断线重连、社区生态成熟,怎么看都像是为工业物联网量身定做的。那会儿我们刚把一条老旧的装配…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬