十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

饭卡管理系统课程设计:从UML建模到Spring Boot分层实现

饭卡管理系统课程设计:从UML建模到Spring Boot分层实现 简介本资源是一份面向高校软件工程专业本科生的课程设计实践文档聚焦饭卡管理信息系统开发全流程解决传统手工饭卡管理效率低、数据更新滞后等实际问题。文档为完整版Word说明书1个.doc文件2.17MB涵盖可行性研究、需求分析含数据流图与数据字典、概要设计E-R图、系统功能结构图、详细设计模块说明与界面设计及软件测试全过程严格遵循软件工程六大设计阶段规范正文超5000字、20页以上满足课程设计大纲对文档深度与格式的全部要求。内容预览显示其包含54页详细目录、分角色任务分工表、技术方案比选及投资效益分析等实用模块兼具教学指导性与工程参考价值。目前已有2821人学习下载适合软件工程课程设计实践、数据库设计训练及信息系统开发入门者系统复盘设计逻辑与交付成果。1. 饭卡管理系统不是“做个登录页充值按钮”就叫软件工程课程设计很多同学拿到“饭卡管理系统”这个题目第一反应是用 Java Swing 拉个界面、连个 MySQL、写几条 CRUD 就交差——结果答辩时被问“需求怎么来的UML 图在哪模块怎么解耦异常怎么分级处理并发充值怎么防重”当场卡壳。这暴露了一个关键事实饭卡管理系统本质是典型的小型事务型业务系统它逼着你把软件工程课上讲的“需求分析→建模→设计→实现→测试→维护”整条链路走通而不是拼凑功能。它适合大三学生练手因为数据实体清晰学生、卡、消费记录、商户、业务规则明确挂失即冻结、余额不足拒绝消费、日结需对账但又足够复杂到必须引入分层架构、事务控制和状态机思维。真正拉开差距的不是能不能跑起来而是你能否说清“为什么用 Spring Boot 而不用 Servlet 原生为什么消费记录表要冗余商户名称而非只存 ID为什么挂失操作要加分布式锁而充值不需要”——这些才是软件工程课程设计要考察的核心能力。2. 从需求建模到数据库落地用标准 UML 和范式约束反推设计2.1 用用例图锁定核心参与者与业务边界拒绝“功能清单式”需求课程设计最常犯的错误是把“管理员能查报表”“学生能充值”直接当需求写进文档。正确做法是从真实校园场景出发画出带关系的用例图主要参与者持卡学生核心、食堂商户主动发起结算、后勤管理员管理卡状态、财务人员核对日结流水关键用例学生自助挂失含短信验证、商户扫码扣款需实时返回余额、管理员批量发卡带模板导入、财务日终对账比对系统流水与 POS 机小票扩展关系挂失后自动触发“冻结卡状态”和“通知辅导员”两个扩展用例包含关系“查询余额”被“消费”“充值”“挂失”三个用例包含说明它是基础能力。提示用例描述必须带前置条件、后置条件和主事件流。例如“商户扫码扣款”的前置条件是“卡状态为正常且余额 ≥ 扣款金额”后置条件是“生成消费记录且余额实时更新”主事件流要写清“POS 机发送扣款请求 → 系统校验卡状态 → 校验余额 → 执行扣款 → 返回成功码 → 记录流水”。漏掉任一环节后续数据库设计就会出错。2.2 用类图定义实体关系用状态图刻画卡生命周期饭卡不是静态对象它有明确的状态变迁未激活 → 正常 → 挂失 → 补办 → 注销。类图中必须体现Card类的status字段并用状态图描述转换条件“未激活”只能通过“管理员发卡”进入“正常”“正常”可通过“学生挂失”进入“挂失”或通过“消费失败超限”自动进入“临时冻结”需单独状态“挂失”状态下禁止任何消费但允许“补办”生成新卡号旧卡作废“注销”是终态不可逆。数据库设计必须与状态图对齐。常见错误是只建一张card表用status字段存字符串如 normal/lost导致无法约束非法状态跳转。正确做法是card表保留card_id,student_id,balance,create_time新增card_status_log表字段为log_id,card_id,from_status,to_status,operator_typeadmin/student/system,operate_time,reason用外键和检查约束确保to_status只能是预定义枚举值且from_status → to_status组合在card_status_log中可追溯。2.3 用第三范式重构表结构解决“商户名称冗余”这类经典陷阱初学者常把消费记录表设计成id,card_id,merchant_name,amount,time。这违反第三范式——merchant_name依赖于merchant_id而非直接依赖主键。正确拆分方式merchant表merchant_idPK,name,contact_phone,addresstransaction表trans_idPK,card_id,merchant_idFK,amount,trans_time,statussuccess/failed/refunded关键点transaction表不存merchant_name查询时用 JOIN 获取但为提升报表性能在transaction表增加merchant_name_snapshot字段冗余存快照并用触发器或应用层保证其与merchant.name一致。-- 创建 transaction 表含快照字段 CREATE TABLE transaction ( trans_id BIGINT PRIMARY KEY AUTO_INCREMENT, card_id VARCHAR(20) NOT NULL, merchant_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL CHECK (amount 0), trans_time DATETIME DEFAULT CURRENT_TIMESTAMP, status ENUM(success,failed,refunded) DEFAULT success, merchant_name_snapshot VARCHAR(100) NOT NULL, -- 冗余快照非空 FOREIGN KEY (card_id) REFERENCES card(card_id), FOREIGN KEY (merchant_id) REFERENCES merchant(merchant_id) ); -- 创建触发器同步快照MySQL 8.0 DELIMITER $$ CREATE TRIGGER update_merchant_snapshot BEFORE INSERT ON transaction FOR EACH ROW BEGIN SELECT name INTO NEW.merchant_name_snapshot FROM merchant WHERE merchant_id NEW.merchant_id; END$$ DELIMITER ;这段 SQL 的核心逻辑是插入交易记录前自动从merchant表读取当前商户名称填入merchant_name_snapshot。这样既满足范式要求主表无冗余又保证历史记录可追溯即使商户改名快照仍保留交易时的真实名称。参数说明CHECK (amount 0)强制金额为正ENUM限定状态值避免字符串拼写错误。3. 分层架构实现用 Spring Boot MyBatis-Plus 构建可测试的业务内核3.1 Controller 层做协议转换严禁写业务逻辑很多课程设计代码里PostMapping(/recharge)方法里直接调用JdbcTemplate.update()执行 SQL这是典型反模式。Controller 只负责三件事解析 HTTP 请求如RequestBody RechargeRequest request校验基础格式用Valid注解 自定义ValidAmount约束调用 Service 方法并包装响应ResponseEntity.ok(service.recharge(request))。// RechargeRequest.java public class RechargeRequest { NotBlank(message 卡号不能为空) private String cardId; NotNull(message 充值金额不能为空) DecimalMin(value 1.00, message 充值金额不能小于1元) DecimalMax(value 9999.99, message 充值金额不能超过9999.99元) private BigDecimal amount; // getter/setter... } // CardController.java RestController RequestMapping(/api/card) public class CardController { Autowired private CardService cardService; PostMapping(/recharge) public ResponseEntityResponseResult recharge(Valid RequestBody RechargeRequest request) { try { RechargeResult result cardService.recharge(request.getCardId(), request.getAmount()); return ResponseEntity.ok(ResponseResult.success(result)); } catch (CardNotFoundException e) { return ResponseEntity.status(404).body(ResponseResult.error(卡号不存在)); } catch (InvalidAmountException e) { return ResponseEntity.badRequest().body(ResponseResult.error(e.getMessage())); } } }这段代码的关键在于所有校验由注解驱动NotBlank,DecimalMin异常按类型分类捕获CardNotFoundException是业务异常InvalidAmountException是参数异常Controller 不触碰数据库或状态变更逻辑。参数说明DecimalMin的value必须是字符串1.00否则编译报错ResponseResult是自定义统一响应体含code,message,data三字段。3.2 Service 层封装事务边界用状态机驱动核心流程CardService.recharge()方法必须包裹在Transactional中并显式处理状态流转先查卡状态若为“挂失”或“注销”则抛异常执行余额更新UPDATE card SET balance balance ? WHERE card_id ?记录充值流水插入recharge_record表发送异步通知如邮件或站内信。Service public class CardService { Autowired private CardMapper cardMapper; Autowired private RechargeRecordMapper rechargeRecordMapper; Transactional(rollbackFor Exception.class) public RechargeResult recharge(String cardId, BigDecimal amount) { // 1. 查询卡并校验状态 Card card cardMapper.selectById(cardId); if (card null) throw new CardNotFoundException(卡号不存在); if (!normal.equals(card.getStatus())) { throw new InvalidStatusException(当前卡状态不允许充值 card.getStatus()); } // 2. 更新余额乐观锁防止并发超充 int updated cardMapper.updateBalance(cardId, amount); if (updated 0) throw new ConcurrencyException(余额更新失败请重试); // 3. 记录充值流水 RechargeRecord record new RechargeRecord(); record.setCardId(cardId); record.setAmount(amount); record.setRechargeTime(LocalDateTime.now()); rechargeRecordMapper.insert(record); // 4. 返回结果不含敏感信息 return new RechargeResult(cardId, card.getBalance().add(amount)); } }逻辑说明updateBalance是自定义 XML 方法SQL 中用WHERE status normal AND version #{version}实现乐观锁ConcurrencyException是自定义运行时异常被Transactional捕获后自动回滚。参数说明rollbackFor Exception.class确保所有异常都回滚默认只回滚RuntimeExceptionRechargeResult只返回卡号和新余额不暴露内部 ID 或时间戳。3.3 Mapper 层用 MyBatis-Plus 避免硬编码 SQL但关键查询必须手写MyBatis-Plus 的lambdaQuery()适合简单条件但涉及多表关联或复杂聚合时必须手写 XML。例如“查询某学生所有消费记录及对应商户名称”自动生成的selectList()无法 JOINmerchant表必须在TransactionMapper.xml中写select标签用LEFT JOIN关联并用resultMap映射嵌套对象。!-- TransactionMapper.xml -- resultMap idTransactionWithMerchant typeTransaction id propertytransId columntrans_id/ result propertycardId columncard_id/ result propertyamount columnamount/ result propertytransTime columntrans_time/ association propertymerchant javaTypeMerchant id propertymerchantId columnm.merchant_id/ result propertyname columnm.name/ /association /resultMap select idselectByStudentId resultMapTransactionWithMerchant SELECT t.*, m.name FROM transaction t LEFT JOIN merchant m ON t.merchant_id m.merchant_id WHERE t.card_id IN ( SELECT card_id FROM card WHERE student_id #{studentId} ) ORDER BY t.trans_time DESC /select这段 XML 的关键点association标签将merchant表字段映射到Transaction对象的merchant属性类型为Merchant对象避免在 Service 层手动组装子查询SELECT card_id FROM card WHERE student_id #{studentId}确保只查该学生的卡产生的交易。参数说明#{studentId}是预编译占位符防止 SQL 注入ORDER BY t.trans_time DESC保证最新记录在前。4. 关键业务逻辑验证用 JUnit 5 测试状态流转与并发安全4.1 用嵌入式 H2 数据库做隔离测试不依赖真实 MySQL课程设计答辩时老师常问“你怎么证明挂失后真的不能消费”。靠手动点界面演示不可靠必须写单元测试。用 H2 内存数据库每次测试前初始化干净数据在test/resources/application-test.yml中配置spring.datasource.urljdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE用Sql注解在测试方法前执行初始化脚本创建表、插入测试卡测试结束后自动销毁内存库。SpringBootTest(classes {TestConfig.class}) TestMethodOrder(MethodOrderer.OrderAnnotation.class) class CardServiceTest { Autowired private CardService cardService; Test Order(1) Sql(scripts /init-test-data.sql, executionPhase Sql.ExecutionPhase.BEFORE_TEST_METHOD) void testRechargeOnNormalCard() { // 给正常卡充值应成功 RechargeResult result cardService.recharge(CARD001, new BigDecimal(50.00)); assertEquals(CARD001, result.getCardId()); assertEquals(new BigDecimal(150.00), result.getNewBalance()); // 原余额10050 } Test Order(2) void testRechargeOnLostCard() { // 给挂失卡充值应抛异常 assertThrowsInvalidStatusException(() - cardService.recharge(CARD002, new BigDecimal(10.00)) ); } }逻辑说明Sql在每个测试方法前执行init-test-data.sql含INSERT INTO card VALUES (CARD001,STU001,normal,100.00)确保测试环境纯净Order(1)和Order(2)控制执行顺序先测正常流程再测异常分支。参数说明SpringBootTest(classes {TestConfig.class})指定测试专用配置类避免加载完整生产配置。4.2 用 CountDownLatch 模拟高并发充值验证乐观锁有效性“100 人同时给同一张卡充 1 元最终余额是否等于 100” 这是检验并发安全的核心问题。用CountDownLatch启动 100 个线程每个线程调用recharge()最后断言余额Test void testConcurrentRecharge() throws InterruptedException { String cardId CARD003; int threadCount 100; CountDownLatch latch new CountDownLatch(threadCount); // 启动100个线程并发充值 for (int i 0; i threadCount; i) { new Thread(() - { try { cardService.recharge(cardId, new BigDecimal(1.00)); } finally { latch.countDown(); } }).start(); } latch.await(); // 等待所有线程完成 // 查询最终余额 Card finalCard cardMapper.selectById(cardId); assertEquals(new BigDecimal(100.00), finalCard.getBalance()); }这段代码的关键在于latch.await()阻塞主线程直到所有子线程调用countDown()确保所有充值操作完成后再查余额assertEquals断言余额精确等于 100.00不是 99 或 101。如果未加乐观锁大概率出现余额 100 的情况多个线程读到相同旧余额各自加 1 后写回。5. 课程设计交付物 checklist让答辩老师一眼看到工程规范性5.1 文档必须包含的 5 类图表缺一不可软件工程课程设计的评分重点是过程规范性而非代码行数。答辩材料中必须提供以下图表且每张图需标注来源工具如 PlantUML、Draw.io和生成时间图表类型必含要素常见错误用例图至少 4 个参与者、6 个用例、2 个扩展关系用例命名用动词短语如“充值”而非名词如“充值功能”类图Card,Student,Merchant,Transaction四个核心类标出属性类型String,BigDecimal和关联多重性1..*忘记标Card.status的枚举值normal/lost/canceled状态图卡状态节点、带条件标签的转换箭头如[挂失请求]、初态和终态符号状态名用小写字母normal与数据库字段值严格一致ER 图card,student,merchant,transaction四张表标出主外键连线和基数1:Ntransaction表与card表连线旁写N与merchant表连线旁写1部署图展示 Web 服务器Tomcat、数据库MySQL、客户端浏览器/POS 机三者物理分布把“POS 机”画成软件组件而非硬件设备5.2 代码提交前必做的 3 项检查Git 提交信息规范化禁用git commit -m fix bug必须用 feat/fix/docs 前缀如git commit -m feat(card): add recharge validation rulesPOM 文件显式声明 JDK 版本在properties中写java.version17/java.version避免老师环境因 JDK 版本差异编译失败README.md 包含可复现的启动步骤## 快速启动 1. 安装 MySQL 8.0创建数据库 campus_card 2. 执行 src/main/resources/sql/init.sql 初始化表结构 3. 修改 application.yml 中的 spring.datasource.password 4. 运行 mvn spring-boot:run 5. 访问 http://localhost:8080/swagger-ui.html 查看 API 文档最后一行技术内容Swagger UI 自动生成的接口文档必须包含RechargeRequest的参数校验说明如“amount: 最小值1.00最大值9999.99”这直接体现你对Valid注解的实际运用能力。本文还有配套的精品资源点击获取
返回列表