商场管理系统课程设计:从数据库设计到Spring Boot全栈实践
简介这是一份面向软件工程或计算机相关专业学生的商场管理系统课程设计文档适合需要完成同类综合实践项目、或希望系统掌握软件工程流程的读者参考。文档以典型商业场景为背景从项目计划书编写目的出发依次展开系统背景、需求分析含关联图、功能/性能/数据需求、质量管理、进度管理、规模分析与风险分析并配有进度表、鱼骨图及风险规避方法表结构完整、步骤清晰。资源为1个docx文档压缩包仅71KB内容精炼便于直接阅读或二次修改。目前已有123人学习浏览对于正在开展课程设计或项目管理的同学而言具有较强的借鉴价值。1. 商场管理系统课程设计先想清楚交付边界再动手写代码课程设计里最常见的开场白是“我的系统能跑”但能跑并不代表能通过。商场管理系统这个题目评审老师真正关注的不是页面数量而是你有没有打通“商品档案—库存变化—销售订单—统计分析”这条最小业务闭环。也就是说交付物其实是两条线一条是能演示的收银与管理页面另一条是能支撑数据库设计和答辩的 ER 图、接口说明、异常处理片段。这个题目适合信息管理与计算机相关专业的课程设计也适合想用一次全栈练习建立工程感的开发者。在动 IDE 之前先把这两条交付线想清楚后面每一步都会省时间。设计上不需要一上来就追求大型分布式方案课程设计考察的是“会不会按软件工程的方法把一个小系统做完整”。这套做法按“业务模型 → 后端事务 → 管理员端页面 → 文档答辩”的顺序展开前提是本地已经有 JDK、Maven、MySQL 和 Node.js最好再准备一个能覆盖 product、order、member 几张核心表的数据库。后面每一章给出的代码都是最小可复现版本能跑通也能够支撑答辩时的追问。2. 商场管理系统的业务模型商品、库存、订单、会员的关键表设计2.1 先画主链路再定表结构“商场管理系统”的核心链路很直白管理员录入商品商品进入库存收银台根据 SKU 下订单系统扣减库存并生成销售明细会员累积积分管理端根据明细做日销售与品类汇总。这张图一旦画清楚系统的表清单基本就定了。它决定了核心表至少有五张商品分类表、商品表、库存表、订单主表、订单明细表可选加会员表和库存流水表。表与表之间的关系用一张小表整理写报告时可以直接复用到数据库设计章节主数据核心表表间关系设计要点商品category、productcategory 1:n product商品表里冗余分类名避免高频联表库存inventoryproduct 1:1 inventory库存与商品解耦后续支持多仓库订单order_main、order_item主表 1:n 明细明细表写死商品快照会员memberorder_main n:1 member积分只在成交时结算这里有一个课程设计里经常被忽略但答辩时很加分的点订单明细表要把“商品名称、成交单价”在创建订单时复制一份过来而不是统计时再去 join product 表。好处是商品后续改价、下架甚至删除历史订单的统计口径都不变这就是快照思维。以后做电商或供应链系统订单明细同样是这么设计的。2.2 商品与 SKU单表还是独立 SKU 表课程设计里的商品通常没有复杂的颜色、尺码维度。常见做法是让商品单表直接承担“可售商品”的角色规格字段用普通列存起来库存数量也直接挂在 inventory 表的 stock_quantity 上。这种方案的表结构简单、查询直观、页面也容易对齐。只有当你希望演示“同一款商品按颜色、尺码分别管库存”时才需要引入独立 SKU 表把库存粒度从 product_id 细化到 sku_id。方案适用规模库存粒度实现成本商品单表 规格字段课程设计默认单商品低推荐独立 sku 表有颜色/尺码按 SKU 管控库存按 SKU 管中按单表方案建表 SQL 可以收敛在 50 行以内。要注意的关键点有两个金额一律用 DECIMAL(10,2)不能用 FLOAT浮点数做累计时会出现 0.30000000000000004 这类精度问题库存字段放在独立表里是为了后续扩展仓库概念时不用改动商品主表。下面给出一组可运行的核心建表语句CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_no INT NOT NULL DEFAULT 0 ); CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-上架 0-下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id) ); CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, stock_quantity INT NOT NULL DEFAULT 0, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE order_main ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, member_id BIGINT, total_amount DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );注意 order_item 里 product_name 和 price 是显式从商品表复制过来的快照字段所以这段建表语句本身就是在固化前一小节讲的思路。ORDER BY、索引和 COMMENT 都是可以直接放进课程设计报告的内容不要在文档里只贴截图不贴语句。2.3 销售统计以订单明细为事实表商品、库存、订单都有了接下来是统计。很多初学者会把“统计”做成在 product 表上做 group by这是错的。销售事件的事实来源是 order_item它每行是“一次购买行为中的一件商品”SUM 它才是真实销售额如果去 SUM product.price会把没卖出去的商品也纳入报表。下面这条 SQL 是日报统计的最小实现可以直接放到管理端 dashboardSELECT DATE_FORMAT(oi.create_time, %Y-%m-%d) AS sale_date, SUM(oi.quantity * oi.price) AS revenue, COUNT(DISTINCT oi.order_id) AS order_cnt FROM order_item oi WHERE oi.create_time 2025-01-01 00:00:00 AND oi.create_time 2025-02-01 00:00:00 GROUP BY sale_date ORDER BY sale_date;这条 SQL 里有三个设计细节可以写进报告第一区间判断用左闭右开 且 避免 BETWEEN 把边界时刻重复计入第二COUNT(DISTINCT order_id) 才是单量如果直接 COUNT(*) 会按明细行数计数订单里买 3 件商品就会变成 3 单第三GROUP BY 使用了 DATE_FORMAT 表达式数据量大了以后索引会失效课程设计里没关系但答辩时可以主动说出“生产环境我会在每日定时任务里预聚合”作为演进方向。3. 商场管理系统的后端实现Spring Boot MyBatis-Plus 最小方案3.1 技术选型与工程骨架课程设计常用技术组合是 Spring Boot MyBatis-Plus MySQL。Spring Boot 能直接跑出可执行 jar规避了旧式 SSM 在 Tomcat 部署上的繁琐配置MyBatis-Plus 把单表 CRUD 封装成现成方法省出来时间可以放到事务和权限这些更能体现水平的点上。前端用 Vue3 Element Plus与后端完全解耦后面一章会展开。依赖作用课程设计场景的说明spring-boot-starter-webMVC 与内嵌 Tomcat打包后可直接 java -jar 演示spring-boot-starter-validation参数校验答辩时属于“系统健壮性”的直接证据mybatis-plus-boot-starter单表 CRUD 封装保留自定义 SQL 能力mysql-connector-jMySQL 驱动按本地 JDK 选择对应版本pom 关键依赖如下版本号以你本地的 JDK 环境为准。使用 JDK 17 时Spring Boot 应切到 3.xMyBatis-Plus 对应使用 mybatis-plus-spring-boot3-starter不要照抄旧教程里的组合parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependencies依赖层面的问题是答辩最容易出现冷场的地方。把 validation 引入进来实际做一次空值校验就已经比“只做 if 判空”高一个档次。如果用的是本课程设计自己建的表记得在 application.yml 里配置逻辑删除字段这类设置时只写实际存在的列不要为了“看起来全”而配一堆没用的开关。3.2 商品模块实体、Service、Controller 的最小闭环实体类与建表语句一一对应字段名保持驼峰风格MyBatis-Plus 默认开启驼峰下划线映射。这里有一个值得注意的细节实体里不要把“库存”字段直接写在 Product 上应该通过查询去关联 inventory 表。虽然这样写列表页要多一次查询但保持了库存与商品两条线的独立性后面做库存流水时不用改商品表结构。Data TableName(product) public class Product { private Long id; private Long categoryId; private String name; private BigDecimal price; private Integer status; private LocalDateTime createTime; }Controller 这一层不需要写复杂逻辑加上条件构造器即可完成分页与模糊搜索这已经是课程设计里最高频的接口形态GetMapping(/products) public Result pageProducts( RequestParam(defaultValue 1) long page, RequestParam(defaultValue 10) long size, RequestParam(required false) String keyword) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword) .orderByDesc(Product::getCreateTime); PageProduct p productService.page(new Page(page, size), wrapper); return Result.ok(p); }这里的逻辑是keyword 为空时like 的第一个条件参数为 falseMyBatis-Plus 不会把该条件拼进 SQLPage 对象会自动把 page/size 换算成 MySQL 的 LIMIT 子句。注意不要把前端传来的 page 直接减一后去拼 offsetPage 内部已经做了这个换算手写反而容易出现第 2 页与第 3 页数据重复的问题。返回体 Result 建议统一为{ code, message, data }结构后续所有接口共用前端 axios 拦截器处理起来也一致。3.3 下单与库存扣减事务与条件更新是核心商场管理系统最容易被追问的模块是“下单”。一个订单创建会同时写 order_main、order_item、inventory、会员积分四类数据任何一个环节失败都不允许留下脏数据所以方法必须加事务。库存扣减不能先查再改并发下两条请求同时读到库存 1就会卖出去 2 件。常见做法是直接让数据库在 UPDATE 时做条件判断用受影响行数决定是否继续UPDATE inventory SET stock_quantity stock_quantity - #{quantity}, update_time NOW() WHERE product_id #{productId} AND stock_quantity #{quantity} AND status 1这段 SQL 把“判断库存是否充足”和“扣减”放进同一个原子操作。受影响行数如果为 0说明库存不足或商品下架直接抛业务异常事务则回滚订单和积分都不会落库。在 Service 方法上标注 Transactional 并指定 rollbackFor即可把抛出的业务异常纳入回滚下面是一个最小落点Transactional(rollbackFor Exception.class) public void createOrder(CreateOrderDTO dto) { OrderMain main buildOrderMain(dto); orderMainMapper.insert(main); for (OrderItemDTO item : dto.getItems()) { orderItemMapper.insert(buildOrderItem(main.getId(), item)); int affected inventoryMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected ! 1) { throw new BusinessException(库存不足或商品已下架); } } memberService.addScore(dto.getMemberId(), main.getTotalAmount()); }这里有一个容易踩的误区在 Controller 方法上加 synchronized 企图防超卖。单实例时有效但部署两个节点就失效了而且锁粒度覆盖整个请求性能并不好。用条件 UPDATE 依赖数据库行锁是更本质的解法也符合真实电商系统降库存的思路。需要再扩展时可以在 inventory 表加 version 字段做乐观锁重试或者在查询库存的 SELECT 后面加 FOR UPDATE 走悲观锁课程设计能讲到“条件更新和悲观锁的区别”就已经超过大部分人预期了。4. 商场管理系统管理员端Vue3 Element Plus 页面实战与接口联调4.1 商品列表页一页代码打通查询、分页与搜索管理员端最先要做的页面是商品列表它把后台分页接口、Element Plus 表格、查询条件串在一起。组件直接用 el-table、el-input、el-pagination不需要额外引入图表库。参考代码如下template div el-input v-modelkeyword placeholder商品名称 clearable stylewidth: 240px / el-button typeprimary clickloadData(1)查询/el-button el-table :datarecords stripe el-table-column propname label商品名称 / el-table-column propcategoryId label分类ID width100 / el-table-column propprice label售价 / el-table-column propstatus label状态 width80 / /el-table el-pagination v-model:current-pagepage v-model:page-sizesize :totaltotal layouttotal, prev, pager, next current-changeloadData / /div /template script setup import { ref } from vue import http from /api/http const keyword ref() const page ref(1) const size ref(10) const total ref(0) const records ref([]) async function loadData(p page.value) { const { data } await http.get(/products, { params: { page: p, size: size.value, keyword: keyword.value } }) records.value data.data.records total.value data.data.total } /script需要注意两个细节查询按钮要把页码重置为 1否则在第二页输入关键字搜索会因为当前页超出结果总数而渲染空白后端返回的分页字段名建议统一成 records 和 totalElement Plus 分页组件的 total、current-page 字段才能直接对上。这里有一个坑如果把后端返回的 total 直接当 Number 用没有问题但如果后端序列化成了字符串分页组件的 total 校验就会被拒联调时要先看清 network 面板里的原始响应。4.2 库存预警一个查询条件即可演示业务价值库存预警是这个系统里“看起来最像真系统”的功能实现成本却很低。商品表维护两个基础数据已经有的库存数量和一条预警阈值。页面上加一个“只看预警”开关后端只需要在查询条件里增加stock_quantity low_stock_threshold再用一个徽标或行背景把预警商品标红。数据库侧一条 SQL 就能看到“商品 库存 分类”汇总的 dashboardSELECT p.name AS product_name, iv.stock_quantity AS stock, iv.low_stock_threshold AS threshold FROM inventory iv JOIN product p ON p.id iv.product_id WHERE iv.stock_quantity iv.low_stock_threshold ORDER BY iv.stock_quantity ASC LIMIT 20;这个查询把低库存商品限定为一个可观测列表演示时最有冲击力的操作是用管理员账号把某商品库存改成 5阈值设成 10刷新页面立刻出现警示。这比单纯展示“CRUD 功能齐全”更靠近业务价值也适合写进课程设计报告的测试用例部分。要注意 low_stock_threshold 的默认值不能为 0否则预警在数据初始化后就永远不出现答辩演示会冷场。4.3 权限管控拦截器在后端把关而不是只在菜单上隐藏课程设计里通常有管理员和收银员两个角色。前端只要控制菜单显隐没登录的用户直接改 localStorage 或调接口依然能访问全部功能。正确做法是后端对接口做权限校验并且返回状态码区分“未登录”和“无权限”也就是 401 与 403 的语义区别。Spring Boot 中用 HandlerInterceptor 即可没必要引入整套 Spring Securitypublic class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) { LoginUser user (LoginUser) req.getAttribute(loginUser); if (user null) { throw new UnauthorizedException(未登录); } if (req.getRequestURI().startsWith(/admin/) !ADMIN.equals(user.getRole())) { throw new ForbiddenException(无权限访问); } return true; } }这个拦截器把两个决策都放在同一个位置用户身份是全局的管理员接口是带 /admin/ 前缀的。登录接口放行其余接口先过身份校验再按 URI 前缀判断角色。前端配合 axios 响应拦截器在 code 为 401 时跳登录页403 时提示无权限即可。比起在每个 Controller 方法里手写权限分支这个方案的代码更集中答辩时也更容易讲清楚“鉴权发生在哪一层”。4.4 联调时最容易翻车的三个问题前后端分离后的头号问题是跨域。开发环境不要直接在后端 CORS 配置里写“允许所有来源”更常见的做法是让前端 Vite 开发服务器做代理生产环境再依赖反向代理或同域部署server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }第二个问题是时间格式。MySQL 返回的 DATETIME 序列化成 JSON 后默认是 UTC 时间戳前端直接渲染会看到 8 小时偏差。解决方法是 application.yml 固定 Jackson 输出格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三个问题看起来小却在联调时反复出现后端分页参数叫 page、size前端组件传出的是 pageSize两边没对齐就导致第一页正常、翻页后数据越翻越少。修正方式是接口约定里固定字段名列表函数与分页组件的传参完全共用同一份常量。这类问题写代码时顺手统一能省掉一整晚查 bug 的时间也能避免在答辩演示时出现“分页后页面空白”的尴尬。5. 课程设计文档、验收动线与答辩话术5.1 文档结构按“决策”写不按“代码量”写报告写不好通常不是因为代码太少而是老师看不到决策过程。建议顺序是问题陈述与用例图、数据库 ER 图与设计说明、接口设计、关键模块实现、测试用例与运行截图。ER 图建议画出至少五张核心表并用文字解释为什么订单明细要存快照而不是贴一堆建表语句了事。测试用例是很多课程设计文档里最弱的部分建议按功能场景写正常下单、库存不足、未登录访问、重复提交。每一条写清“前置条件、操作步骤、期望结果”能直接证明系统是被真实测试过的。最后的不足与改进里大胆写“库存没有多仓支持、下单没有引入消息队列”但每一项必须带上可落地的演进思路。5.2 答辩高概率被问的三个问题问题应答思路加分点库存扣减怎么防超卖条件 UPDATE 事务回滚能说出受影响行数判断销售报表为什么用 order_item 而不是 product事实表概念与快照字段能区分主数据与事实数据换数据库或换前端框架能跑吗Service 接口与页面组件解耦能说清依赖边界在哪里回答技术问题有一个通用姿势先给结论再给代码落点最后补一句边界。比如防超卖先说“用 UPDATE 条件判断库存”再指出受影响行数最后说“分布式集群下可以再引入 Redis 预扣或消息队列削峰课程设计里数据库方案已经能解决单机并发”。这比背一段概念更有说服力也更符合评审老师对课程设计的预期。5.3 验收演示动线按业务场景走而不是按菜单走到验收前把系统重启一次清理掉测试数据然后按下面这条动线完整走一遍管理员登录 → 新建分类 → 新增商品并设置库存和预警阈值 → 退出登录 → 收银员账号登录 → 搜索商品并下单三件 → 回到管理端查看库存预警 → 打开销售统计确认订单和销售额 → 导出 CSV 或打印日报。每条步骤不要跳因为接口状态是一步步叠加的跳过任何一步都有可能导致演示中途页面拿到空数据。把这条动线在答辩前至少完整走三次同时记录每一步的 URL 和期望数据。真正上场时最怕的不是问题答不上来而是点完按钮页面没有反应后开始慌。先煮熟的演示流程走顺再谈临场发挥。本文还有配套的精品资源点击获取