商场管理系统课程设计:从数据库设计到Spring Boot全栈实践
2026/9/19 23:39:48 网站建设 项目流程

简介:这是一份面向软件工程或计算机相关专业学生的商场管理系统课程设计文档,适合需要完成同类综合实践项目、或希望系统掌握软件工程流程的读者参考。文档以典型商业场景为背景,从项目计划书编写目的出发,依次展开系统背景、需求分析(含关联图、功能/性能/数据需求)、质量管理、进度管理、规模分析与风险分析,并配有进度表、鱼骨图及风险规避方法表,结构完整、步骤清晰。资源为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 是日报统计的最小实现,可以直接放到管理端 dashboard:

SELECT 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.x,MyBatis-Plus 对应使用 mybatis-plus-spring-boot3-starter,不要照抄旧教程里的组合:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</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) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), Product::getName, keyword) .orderByDesc(Product::getCreateTime); Page<Product> p = productService.page(new Page<>(page, size), wrapper); return Result.ok(p); }

这里的逻辑是:keyword 为空时,like 的第一个条件参数为 false,MyBatis-Plus 不会把该条件拼进 SQL;Page 对象会自动把 page/size 换算成 MySQL 的 LIMIT 子句。注意不要把前端传来的 page 直接减一后去拼 offset,Page 内部已经做了这个换算,手写反而容易出现第 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-model="keyword" placeholder="商品名称" clearable style="width: 240px" /> <el-button type="primary" @click="loadData(1)">查询</el-button> <el-table :data="records" stripe> <el-table-column prop="name" label="商品名称" /> <el-table-column prop="categoryId" label="分类ID" width="100" /> <el-table-column prop="price" label="售价" /> <el-table-column prop="status" label="状态" width="80" /> </el-table> <el-pagination v-model:current-page="page" v-model:page-size="size" :total="total" layout="total, prev, pager, next" @current-change="loadData" /> </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 和 total,Element Plus 分页组件的 total、current-page 字段才能直接对上。这里有一个坑:如果把后端返回的 total 直接当 Number 用没有问题,但如果后端序列化成了字符串,分页组件的 total 校验就会被拒,联调时要先看清 network 面板里的原始响应。

4.2 库存预警:一个查询条件即可演示业务价值

库存预警是这个系统里“看起来最像真系统”的功能,实现成本却很低。商品表维护两个基础数据:已经有的库存数量,和一条预警阈值。页面上加一个“只看预警”开关,后端只需要在查询条件里增加stock_quantity <= low_stock_threshold,再用一个徽标或行背景把预警商品标红。数据库侧一条 SQL 就能看到“商品 + 库存 + 分类”汇总的 dashboard:

SELECT 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 Security:

public 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: GMT+8

第三个问题看起来小,却在联调时反复出现:后端分页参数叫 page、size,前端组件传出的是 pageSize,两边没对齐就导致第一页正常、翻页后数据越翻越少。修正方式是接口约定里固定字段名,列表函数与分页组件的传参完全共用同一份常量。这类问题写代码时顺手统一,能省掉一整晚查 bug 的时间,也能避免在答辩演示时出现“分页后页面空白”的尴尬。

5. 课程设计文档、验收动线与答辩话术

5.1 文档结构按“决策”写,不按“代码量”写

报告写不好通常不是因为代码太少,而是老师看不到决策过程。建议顺序是:问题陈述与用例图、数据库 ER 图与设计说明、接口设计、关键模块实现、测试用例与运行截图。ER 图建议画出至少五张核心表,并用文字解释为什么订单明细要存快照,而不是贴一堆建表语句了事。

测试用例是很多课程设计文档里最弱的部分,建议按功能场景写:正常下单、库存不足、未登录访问、重复提交。每一条写清“前置条件、操作步骤、期望结果”,能直接证明系统是被真实测试过的。最后的不足与改进里,大胆写“库存没有多仓支持、下单没有引入消息队列”,但每一项必须带上可落地的演进思路。

5.2 答辩高概率被问的三个问题

问题应答思路加分点
库存扣减怎么防超卖?条件 UPDATE + 事务回滚能说出受影响行数判断
销售报表为什么用 order_item 而不是 product?事实表概念与快照字段能区分主数据与事实数据
换数据库或换前端框架能跑吗?Service 接口与页面组件解耦能说清依赖边界在哪里

回答技术问题有一个通用姿势:先给结论,再给代码落点,最后补一句边界。比如防超卖,先说“用 UPDATE 条件判断库存”,再指出受影响行数,最后说“分布式集群下可以再引入 Redis 预扣或消息队列削峰,课程设计里数据库方案已经能解决单机并发”。这比背一段概念更有说服力,也更符合评审老师对课程设计的预期。

5.3 验收演示动线:按业务场景走,而不是按菜单走

到验收前,把系统重启一次,清理掉测试数据,然后按下面这条动线完整走一遍:管理员登录 → 新建分类 → 新增商品并设置库存和预警阈值 → 退出登录 → 收银员账号登录 → 搜索商品并下单三件 → 回到管理端查看库存预警 → 打开销售统计确认订单和销售额 → 导出 CSV 或打印日报。每条步骤不要跳,因为接口状态是一步步叠加的,跳过任何一步都有可能导致演示中途页面拿到空数据。

把这条动线在答辩前至少完整走三次,同时记录每一步的 URL 和期望数据。真正上场时最怕的不是问题答不上来,而是点完按钮页面没有反应后开始慌。先煮熟的演示流程走顺,再谈临场发挥。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询