拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MySQL校园卡食堂消费系统数据库设计:表结构、事务与日结

简介支持校园卡的食堂消费信息管理系统数据库设计文档是一份完整的高校数据库课程设计/大作业参考范本面向需要完成相似选题的计算机专业学生。资源以单个docx文档呈现大小约540KB内容涵盖需求分析、概念结构设计、逻辑结构设计、物理设计及数据库实施等核心阶段包含学生信息、校园卡、食堂消费等实体关系说明及数据字典并附有系统功能模块图与部分程序源代码。文档结构清晰从现实需求到E-R图再到关系模式转换均有展开可用于理解数据库设计全流程或作为课程报告撰写蓝本。目前已有218人学习下载适合正在做食堂消费、校园卡类管理系统数据库设计的读者参考。1. 支持校园卡的食堂消费信息管理系统数据库设计先想清楚它难在哪拿到“支持校园卡的食堂消费信息管理系统数据库设计”这个数据库大作业别急着开一个库就开始建表。校园卡的出现让这个系统和普通点餐系统拉开差距扣款必须和卡状态、账户余额联动挂失补卡不能丢钱消费要能按餐次、窗口、菜品统计还要能回答“拍了卡没扣钱”“扣了钱没拍到卡”这类边界问题。很多同学交上去的作业表结构不少但一问到补卡怎么处理、余额会不会扣成负数回答就含糊了。这个标题真正要考核的不是你能不能画 ER 图而是你能不能把食堂消费场景里的状态变化、金额变化、时间边界用表和约束表达清楚。适合正在做数据库课程设计、准备答辩以及想给简历里加一个“完整业务闭环”项目的读者。2. 食堂消费业务建模从开卡到日结的实体与边界2.1 最小业务闭环开卡、充值、消费、补卡、结算做设计前先把业务闭环走一遍。新生入学时系统先给这个人建立一个“账户”再发一张“校园卡”这张卡绑定到账户上。开卡之后充值余额进入账户而不是进入卡本身。就餐时持卡人在食堂窗口刷卡终端读取卡号验证卡状态为正常、账户状态为正常、余额足够这三点然后扣减账户余额同时生成一笔消费流水和对应的菜品明细。每个窗口今天卖了多少按流水汇总得到每个账户剩多少钱按余额字段得到。这个闭环里最容易出错的是补卡。卡丢了先挂失挂失后旧卡不能再消费补发新卡后新卡要继承旧账户里的余额。如果你把余额字段设计在“卡表”里补卡的那一刻就面临一个尴尬问题旧卡的钱怎么迁移到新卡更合理的做法是把余额放在账户表卡表只负责身份验证这样换卡不用动金额旧卡注销、新卡绑定同一个账户即可所有消费流水挂在账户上。日结也要在这个阶段想清楚。食堂的“营业日”不等于自然日夜宵可能营业到凌晨一两点凌晨零点后的那笔消费应该归属前一天的营业额。行业里管这个叫“营业日偏移”一般把凌晨 4 点或凌晨 2 点作为日切点。这个规则必须在建模阶段定下来否则后期写统计 SQL 会非常痛苦。2.2 实体怎么分用户、账户、卡、食堂、窗口、菜品、流水把这个场景拆开核心实体并不复杂。下表列出了我认为最值得单独建表的九个实体以及每个实体必须包含的关键属性。实体核心属性为什么要单独立表用户表学号/工号、姓名、人员类型、院系部门保存基础信息不参与余额计算账户表账户ID、余额、状态、最近交易时间余额和状态在这里维护与实体卡解耦卡表卡号、绑定账户ID、卡状态、发卡时间一账户可关联多张卡记录挂失补卡历史食堂表食堂ID、食堂名称、位置作为窗口的上级组织窗口表窗口ID、所属食堂ID、窗口名称日结按窗口统计必须独立菜品表菜品ID、所属窗口ID、菜名、单价菜品由窗口维护价格可能调整餐次表餐次ID、餐次名称、起止时间、是否跨日早餐/午餐/晚餐/夜宵由配置驱动流水主表流水ID、账户ID、卡号、食堂/窗口、金额、扣后余额、餐次一笔消费一行不可修改流水明细表明细ID、流水ID、菜品快照、数量、小计一笔消费可对应多个菜记录快照九个实体之间最核心的关系是账户与卡。一个用户只能有一个主账户一个账户可以绑定多张卡但同一时刻只有一张卡处于“正常”状态。食堂与窗口是一对多窗口与菜品是一对多。用户与流水是一对多账户是流水和余额的连接点。很多参考作业会省略食堂表和窗口表直接在流水表里写一个“食堂名称”字符串。这样做的后果是食堂改名后历史数据跟着变窗口业绩没法聚合而且老师追问“哪个食堂哪个窗口收入最高”时只能靠 LIKE 查询。建议不要省这两个表它们并不增加多少工作量却能明显改善设计的完整度。2.3 核心业务规则的“数据库语言”表达业务规则不是写在需求文档里的而是要用数据库技术实现出来的。至少有以下几条必须翻译成表结构或事务逻辑。第一余额不能为负。这不能只靠应用层判断因为多个窗口同时读到一个余额时先后扣款可能把余额扣成负数。正确的做法是把“余额 消费金额”这个条件写进 UPDATE 语句的 WHERE 子句中让数据库行锁来保证原子性。第二卡状态必须意义明确。卡状态至少要有正常、挂失、注销三种。消费前必须过滤掉非正常状态的卡。这个状态要放在卡表而不是账户表因为挂失是针对物理卡的账户可能仍然是正常状态。第三流水一经生成不可修改。不要设计一个“更新消费记录”的功能。如果发生退款可以增加一条负金额的退款流水或者给流水加一个“退款状态”字段但绝对不能直接改原流水金额。这是对账的基础。第四餐次不能靠字符串。早餐、午餐、晚餐、夜宵应该是一张字典表每条记录包含开始时间、结束时间和是否跨日标记。业务系统在生成流水时根据当前时间自动匹配餐次而不是在代码里写死 if 判断。第五金额一律用 DECIMAL。大作业中用 FLOAT 存金额是典型的翻车点浮点数在累加和比较时会出现精度问题。余额用 DECIMAL(10,2)单笔消费金额用 DECIMAL(6,2)小计用 DECIMAL(8,2)足够覆盖校园食堂场景。3. 落成 MySQL 表结构用户、账户、卡、食堂、窗口、菜品、流水建表 SQL3.1 用户、账户、卡三张表把“人”和“钱”分开我一般在 MySQL 8.0 上用 InnoDB 引擎字符集选 utf8mb4。先建用户表用户表只存人的基本信息不碰余额。CREATE TABLE tb_user ( user_id CHAR(12) NOT NULL COMMENT 学号/教工号主键, user_name VARCHAR(50) NOT NULL COMMENT 姓名, user_type TINYINT NOT NULL COMMENT 1-学生 2-教工 3-其他, dept_name VARCHAR(100) DEFAULT NULL COMMENT 院系/部门, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户基础信息表;user_id 选用 CHAR(12) 是因为学号固定长度如果用 VARCHAR 也可以但整张表里这个字段的类型必须完全一致否则后面 JOIN 索引会失效。user_type 用 TINYINT 而不是 VARCHAR便于扩展和查询。账户表把余额和状态独立出来这是整个设计最关键的一步。CREATE TABLE tb_account ( acct_id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 账户ID, user_id CHAR(12) NOT NULL COMMENT 关联用户表, balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 账户余额, acct_status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 2-冻结, last_trade_time DATETIME DEFAULT NULL COMMENT 最近消费时间, UNIQUE KEY uk_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT校园卡账户表;acct_id 使用自增主键业务中不直接暴露给用户。balance 字段没有设置 CHECK 约束是因为 MySQL 8.0.16 之前的版本对 CHECK 约束支持不完整更可靠的做法是让消费 SQL 在 UPDATE 时通过 WHERE 条件限制余额不小于本次扣款。uk_user 唯一键保证一个用户只能有一个主账户。卡表绑定账户但不存余额。CREATE TABLE tb_card ( card_no CHAR(20) NOT NULL COMMENT 校园卡物理卡号, acct_id BIGINT NOT NULL COMMENT 绑定账户ID, card_status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 2-挂失 3-注销, bind_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 绑定时间, lost_time DATETIME DEFAULT NULL COMMENT 最近挂失时间, PRIMARY KEY (card_no), KEY idx_acct (acct_id), CONSTRAINT fk_card_acct FOREIGN KEY (acct_id) REFERENCES tb_account(acct_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT校园卡表;card_no 是物理卡号由发卡中心印制所以作为主键。一账户多卡完全允许旧卡注销后新卡绑定同一 acct_id余额天然得到保留。外键约束在大作业里建议加上让关系更清晰但要注意只有在两边字段类型完全一致时外键才能建立成功。3.2 食堂、窗口、菜品与餐次字典表决定统计口径食堂和窗口是典型的父子字典表。CREATE TABLE tb_canteen ( canteen_id INT AUTO_INCREMENT PRIMARY KEY, canteen_name VARCHAR(100) NOT NULL, canteen_addr VARCHAR(200) DEFAULT NULL, build_time DATETIME DEFAULT NULL COMMENT 投入使用时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT食堂表; CREATE TABLE tb_window ( window_id INT AUTO_INCREMENT PRIMARY KEY, canteen_id INT NOT NULL, window_name VARCHAR(100) NOT NULL COMMENT 窗口名称或编号, manager_name VARCHAR(50) DEFAULT NULL, is_active TINYINT NOT NULL DEFAULT 1 COMMENT 1-营业 0-停业, KEY idx_canteen (canteen_id), CONSTRAINT fk_window_canteen FOREIGN KEY (canteen_id) REFERENCES tb_canteen(canteen_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT食堂窗口表;菜品表挂在窗口下价格字段使用 DECIMAL。CREATE TABLE tb_dish ( dish_id INT AUTO_INCREMENT PRIMARY KEY, window_id INT NOT NULL, dish_name VARCHAR(100) NOT NULL, price DECIMAL(6,2) NOT NULL COMMENT 当前售价, is_active TINYINT NOT NULL DEFAULT 1, KEY idx_window (window_id), CONSTRAINT fk_dish_window FOREIGN KEY (window_id) REFERENCES tb_window(window_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表;餐次表要设计成可配置的重点在于开始时间、结束时间和跨日标记。这里把“营业日偏移”也落进表结构里用 offset_hours 表示日切偏移量。CREATE TABLE tb_meal_period ( period_id INT AUTO_INCREMENT PRIMARY KEY, period_name VARCHAR(20) NOT NULL COMMENT 早餐/午餐/晚餐/夜宵, begin_time TIME NOT NULL COMMENT 本餐开始时间, end_time TIME NOT NULL COMMENT 本餐结束时间, cross_day TINYINT NOT NULL DEFAULT 0 COMMENT 是否跨日1表示结束时间在次日, offset_hours TINYINT NOT NULL DEFAULT 4 COMMENT 营业日偏移凌晨4点日切 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT餐次配置表;参数说明offset_hours4 的含义是发生在凌晨 4 点之前的消费仍然归属前一营业日。这个字段在日结统计时非常关键如果放到代码里写死换一个食堂运营策略就要改代码。3.3 交易流水与明细快照查询统计的核心战场流水主表是全文最重要的一张表业务上称为“流水底账”。CREATE TABLE tb_trade_flow ( flow_id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 流水内部ID, biz_no CHAR(40) NOT NULL COMMENT 业务单号全局唯一, acct_id BIGINT NOT NULL COMMENT 账户ID, card_no CHAR(20) NOT NULL COMMENT 消费时使用的卡号, canteen_id INT NOT NULL, window_id INT NOT NULL, trade_amount DECIMAL(6,2) NOT NULL COMMENT 本次消费总额, balance_after DECIMAL(10,2) NOT NULL COMMENT 消费后余额快照, meal_period_id INT NOT NULL COMMENT 餐次ID, occurs_at DATETIME(3) NOT NULL COMMENT 交易时间精确到毫秒, refund_status TINYINT NOT NULL DEFAULT 0 COMMENT 0-正常 1-已退款, UNIQUE KEY uk_biz_no (biz_no), KEY idx_acct_time (acct_id, occurs_at), KEY idx_window_time (window_id, occurs_at), CONSTRAINT fk_flow_account FOREIGN KEY (acct_id) REFERENCES tb_account(acct_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT交易流水主表;biz_no 是用于防重复的业务单号后文会讲生成规则。balance_after 是消费完成后余额的快照对账时直接用这个值核对账户当前余额的变化轨迹。occurs_at 用 DATETIME(3) 是为了避免同一毫秒内卡在同一窗口产生两笔金额完全一样的交易方便排查。流水明细存菜品快照这是为了应对菜单价格调整。CREATE TABLE tb_trade_item ( item_id BIGINT AUTO_INCREMENT PRIMARY KEY, flow_id BIGINT NOT NULL, dish_id INT NOT NULL, dish_name_snapshot VARCHAR(100) NOT NULL COMMENT 下单时菜名快照, price_snapshot DECIMAL(6,2) NOT NULL COMMENT 下单时价格快照, item_qty TINYINT NOT NULL DEFAULT 1 COMMENT 数量, item_amount DECIMAL(8,2) NOT NULL COMMENT 该菜品小计, KEY idx_flow (flow_id), CONSTRAINT fk_item_flow FOREIGN KEY (flow_id) REFERENCES tb_trade_flow(flow_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT交易流水明细表;dish_id 仍然保留是为了能和菜品表做关联分析比如查“这个窗口哪几个菜卖得最好”。真正的金额和菜名永远以快照字段为准。4. 消费核心 SQL扣款、幂等、日结和卡状态联动4.1 卡状态校验与扣款条件更新替代先查后改刷卡消费最忌讳“先 SELECT 余额判断够不够再 UPDATE 扣款”因为两个窗口同时操作同一账户时后执行的 UPDATE 可能覆盖前一个结果造成余额为负。正确做法是把余额判断写进 UPDATE 的 WHERE 条件中。以下是一笔完整消费的核心流程。START TRANSACTION; -- 第一步校验卡和账户状态 SELECT a.acct_id, a.balance, c.card_status FROM tb_card c JOIN tb_account a ON c.acct_id a.acct_id WHERE c.card_no 20240000100001 AND c.card_status 1 FOR UPDATE; -- 第二步条件扣款余额不足时影响行数为 0 UPDATE tb_account SET balance balance - 12.50, last_trade_time NOW() WHERE acct_id 20240001 AND acct_status 1 AND balance 12.50; -- 第三步判断第二步影响行数等于 1 才写流水否则 ROLLBACK INSERT INTO tb_trade_flow ( biz_no, acct_id, card_no, canteen_id, window_id, trade_amount, balance_after, meal_period_id, occurs_at ) VALUES ( C2025060112000012340001, 20240001, 20240000100001, 1, 3, 12.50, 87.50, 2, NOW() ); COMMIT;这条 UPDATE 相当于用一个原子操作同时完成了两件事判断余额充足以及扣减余额。数据库在更新某一行时会锁住该行其他事务的扣款操作必须排队因此不会出现余额被扣成负数的情况。balance - 12.50 这里使用的是“补货金额 - 扣款金额”的无条件运算但 WHERE 条件里的 balance 12.50 确保运算结果非负。SELECT ... FOR UPDATE 是可选步骤。如果直接使用条件 UPDATE代码会精简很多。我通常保留这步因为一次消费可能涉及多张菜品明细需要先拿到 acct_id 和消费后余额再写入多条 tb_trade_item。4.2 交易幂等与防重复业务单号怎么设计刷卡机与后台通信存在超时重发的可能一次扣款请求可能被重复提交。如果流水表只有自增主键两次相同的消费请求会产生两笔扣减学生账户被扣两次。解决思路是为每笔消费生成一个业务单号 biz_no并在流水表上加唯一索引。业务单号的生成规则建议如下。单号组成段示例值说明消费标识C表示 Consumption日期时间20250601120000到秒卡号后8位10000001标识持卡人窗口编号3位003标识消费点随机或顺序序号4位0001防同秒冲突拼接结果是C20250601120000100000010030001。这个单号在刷卡机端生成随请求提交到后台。写入流水时如果违反 uk_biz_no 唯一约束应用层捕获到重复键错误后应当把这次请求当作“已处理过的重复请求”返回成功但不扣款而不是让事务继续执行。需要注意biz_no 的生成规则必须包含窗口编号和卡号不能只靠时间戳。因为同一时刻完全可能有两个请求并发时间戳单独使用不能保证全局唯一。随机序号段建议使用应用层计数器或 Redis 自增如果没有这些设施用 MySQL 的 AUTO_INCREMENT 申请一个临时 ID 也可以但那样会引入额外的写入开销。4.3 日结统计按营业日而非自然日聚合食堂日结一般关注三个问题每个窗口今天卖了多少笔、多少钱哪个菜品卖得最好每个餐次分别收入多少。下面这条 SQL 实现窗口维度的日结报表注意它使用的时间边界。SELECT c.canteen_name, w.window_name, COUNT(f.flow_id) AS trade_cnt, SUM(f.trade_amount) AS total_amount FROM tb_trade_flow f JOIN tb_canteen c ON f.canteen_id c.canteen_id JOIN tb_window w ON f.window_id w.window_id WHERE f.occurs_at 2025-06-01 04:00:00 AND f.occurs_at 2025-06-02 04:00:00 AND f.refund_status 0 GROUP BY c.canteen_id, w.window_id ORDER BY total_amount DESC;这里的时间参数不是2025-06-01 00:00:00而是2025-06-01 04:00:00。凌晨 0 点到 4 点发生的夜宵消费会被归入前一天。餐次统计可以进一步读取 tb_meal_period 表先计算当前时间落在哪个餐次区间再给流水打上 meal_period_id。按餐次统计时我建议先写一个函数或存储过程完成“时间戳转餐次 ID”避免在每条 INSERT 里重复写复杂的 CASE WHEN 判断。餐次表的 begin_time 和 end_time 是 TIME 类型跨日餐次的结束时间小于开始时间判断逻辑要处理跨日边界例如凌晨 1 点的结束时间显示为 01:00:00但实际代表次日凌晨。5. 数据库设计避坑五个会丢分的并发与数据一致性问题5.1 余额为负仍然扣款成功条件更新被写到了事务外面现象压力测试或多人同时消费时账户余额出现负数流水表里的扣后余额也对不上。原因应用层先执行了SELECT balance FROM tb_account WHERE acct_id ?在 Java 或 Python 代码里判断余额足够再执行UPDATE tb_account SET balance balance - ? WHERE acct_id ?。两个窗口同时读到余额 10 元都认为可以买 8 元的饭结果两次扣减后余额变成 -6 元。这个问题的本质是“先查后改”没有使用数据库锁保护整个区间。解决把余额判断合并进 UPDATE 的 WHERE 条件写成WHERE acct_id ? AND balance ?。这一步自带行锁并发请求会排队执行。扣款后判断影响行数如果为 0立即回滚并给前端返回“余额不足”。5.2 挂失补卡后旧卡仍在消费余额错绑到了卡表现象学生挂失旧卡并补办新卡后旧卡在部分机器上仍然能刷卡消费新卡余额却是 0或者补卡时账户余额消失。原因设计时把 balance 字段直接放在 tb_card 表里消费 SQL 只判断卡状态和卡上余额。补卡时 insert 一张新卡balance 初始化为 0旧的挂失卡状态虽然改成了挂失但某些离线终端缓存了卡状态没有及时同步旧卡继续扣减卡上的余额。解决把余额全部转移到 tb_account 表消费流程先通过 card_no 找到 acct_id再锁定账户行。补卡只是把新卡绑定到同一个 acct_id旧卡即使被非法读取也只会查到状态异常或无法通过账户状态校验。离线终端的兜底方案是在刷卡的本地黑名单里维护挂失卡号但这属于终端逻辑数据库设计层面要保证“卡上无余额”这个原则。5.3 历史流水里的菜价变了快照字段没加现象导出上个月的窗口销售报表发现某个菜品金额跟着当前菜单价格变了上个月卖出的鸡腿由 8 元变成了 10 元汇总金额全部错位。原因流水明细表只存了 dish_id报表查询时 JOIN tb_dish 取菜名和价格。菜品调价后历史流水通过 JOIN 拿到的是最新价格跟踪记录就不是当天交易的真实情况了。解决在 tb_trade_item 中保留 dish_name_snapshot 和 price_snapshot 字段点菜下单时从菜品表读取当前价格一并写入。查询历史流水时优先展示快照字段只有做菜品结构分析时才通过 dish_id 关联菜品表。5.4 餐次统计跨夜翻车自然日与营业日混用现象夜宵营业额全部算到了第二天早餐营业额异常偏低日结报表和刷卡机终端汇总金额对不上。原因所有统计 SQL 都使用WHERE DATE(occurs_at) 2025-06-02而食堂夜宵经营到凌晨 1 点凌晨 0 点到 1 点的消费被归入 6 月 2 日。如果食堂规定营业日从凌晨 4 点开始这些夜宵应当归入 6 月 1 日。解决明确一个日切时刻把日切偏移量作为统一参数。统计时使用半开区间[日切点, 次日日切点)例如occurs_at 2025-06-01 04:00:00 AND occurs_at 2025-06-02 04:00:00。时间区间不要使用 BETWEEN因为 BETWEEN 包含两端会把紧贴着日切点的数据重复计算。5.5 JOIN 越查越慢且索引不生效字段类型不一致现象数据量到 10 万条流水后关联用户表和流水表的查询耗时从几十毫秒退化到几秒甚至十几秒EXPLAIN 里看到某个 JOIN 条件没有使用索引。原因tb_user.user_id 定义成 CHAR(12)而 tb_account.user_id 定义成 VARCHAR(20)或者 tb_trade_flow.acct_id 定义成 VARCHARJOIN 时 MySQL 会对字符串做隐式类型转换转换后索引失效造成全表扫描。这是最隐蔽的坑表面看表结构没问题实测才知道索引根本没被用上。解决所有主外键字段类型严格一致char 就是 charbigint 就是 bigint字符集也要统一。自查手段是执行EXPLAIN SELECT ... FROM tb_trade_flow f JOIN tb_account a ON f.acct_id a.acct_id看 key 列是否为 NULLtype 列是否为 ALL。如果出现CONVERT或明显的类型转换提示优先改表结构而不是加索引。6. 答辩前的快速验证别让数据库设计停在 ER 图上6.1 用一条自查 SQL 完成完整性检查答辩时老师最常问的一句话是“你怎么证明这个设计是对的”。不要只说“我测过了”可以准备几条能实际执行的自查 SQL。下面这条查询同时检查负余额、挂失卡消费和孤儿流水三类问题。SELECT 负余额账户 AS check_item, COUNT(*) AS bad_cnt FROM tb_account WHERE balance -0.001 UNION ALL SELECT 挂失卡消费, COUNT(*) FROM tb_trade_flow f LEFT JOIN tb_card c ON f.card_no c.card_no WHERE c.card_status 2 UNION ALL SELECT 无账户流水, COUNT(*) FROM tb_trade_flow f LEFT JOIN tb_account a ON f.acct_id a.acct_id WHERE a.acct_id IS NULL;执行结果里任何 bad_cnt 不为 0都说明有数据一致性问题。这个验证脚本应当和初始化数据脚本一起放在项目里每次跑完模拟数据后执行一次确认关键约束没有被破坏。6.2 快速生成模拟数据的存储过程手工造几百条 INSERT 太慢答辩前可以用存储过程批量生成模拟流水。先生成 500 个账户再随机生成消费记录。DELIMITER $$ CREATE PROCEDURE sp_generate_fake_trades(IN p_times INT) BEGIN DECLARE i INT DEFAULT 0; DECLARE v_acct BIGINT; DECLARE v_card CHAR(20); DECLARE v_win INT; DECLARE v_dish DECIMAL(6,2); WHILE i p_times DO SET v_acct 1 FLOOR(RAND() * 500); SET v_card LPAD(v_acct, 8, 0); SET v_win 1 FLOOR(RAND() * 8); SET v_dish ROUND(2 RAND() * 18, 2); INSERT INTO tb_trade_flow ( biz_no, acct_id, card_no, canteen_id, window_id, trade_amount, balance_after, meal_period_id, occurs_at ) VALUES ( CONCAT(T, DATE_FORMAT(NOW(), %Y%m%d%H%i%s), LPAD(i, 6, 0)), v_acct, v_card, 1, v_win, v_dish, 100 - v_dish, 1 FLOOR(RAND() * 4), NOW() ); SET i i 1; END WHILE; END$$ DELIMITER ;注意这个存储过程只适合演示用因为它直接把 balance_after 写成100 - v_dish没有真实扣减账户余额。如果要在正式测试中使用应该把流水写入和账户扣减放在同一事务里并通过业务层或存储过程正确计算余额快照。我最早做这个课题时把余额放在卡表上结果补卡演示当场翻车。反复排查才发现不是 SQL 写错而是实体划分从一开始就是把“钱”绑错了对象。这样的坑踩过一次之后再看任何消费金融类系统我都会先问一句“余额在哪张表”。这个习惯比任何技巧都重要。希望这份设计思路帮你少踩几个坑做出一个禁得住追问的数据库大作业。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门