简介:本资源是面向高校计算机专业学生与Web全栈初学者的超市管理系统课程设计实践项目,基于Java后端、Vue前端及HTML/JavaScript技术栈完整实现湖北工业大学超市管理业务场景,涵盖商品管理、库存统计、员工权限与基础订单流程。压缩包共240个文件,总计58.68MB,其中41个Java文件构成Spring Boot服务层与数据访问逻辑,30个Vue组件实现响应式管理界面,43个JS脚本支撑交互功能,27个drawio文件提供系统架构图、数据库ER图及模块流程图,另有docx实验报告模板、xlsx数据样例与pptx答辩材料,便于课程设计全过程交付。内容预览显示包含nginx部署配置、Element UI样式文件及多份规范化的课程设计文档,体现工程化开发意识。目前已有291人学习下载,适合用于软件工程实训、系统分析与设计课程实践或毕业设计参考,可直接运行调试、快速理解前后端分离架构落地细节。
1. 这不是“又一个学生作业”:湖北工业大学超市管理系统源码背后的真实工程逻辑
你搜“湖北工业大学超市管理系统”,大概率会看到一堆压缩包、百度网盘链接和“含数据库+文档”的宣传语。但真正打开代码,很多人卡在第一步:为什么用 Java 做后端却要配 Vue?为什么 HTML 文件里混着<script src="vue.min.js">和new Vue({ el: '#app' })?这不是技术堆砌,而是高校后勤场景下典型的分层约束——前端必须轻量可离线(教室机房无 Node 环境)、后端需对接校内统一身份认证(Java Spring Security 更易集成)、数据库要兼容学校现有 Oracle 或 MySQL 实训平台。这套系统真正解决的,是管理员在无公网 IP 的校园内网中,用浏览器直接完成商品入库、会员充值、销售流水导出三类高频操作。它面向的是信息学院大三实训课学生、后勤处临时录入员、以及需要快速验证 Java Web + Vue 组合可行性的课程设计指导教师。如果你正被“前后端分离怎么部署到 Tomcat”“Vue 组件如何调用 Java 接口”“HTML 表单提交后页面不刷新”卡住,这篇就是为你写的实操路径。
2. 后端用 Java 实现业务闭环:从 Spring Boot 路由到 JDBC 数据库连接
2.1 为什么选 Spring Boot 而非传统 Servlet?——应对高校实训环境的三个硬约束
高校机房普遍禁用 Maven 私服、限制外网下载依赖、且 JDK 版本常锁定在 8 或 11。Spring Boot 的嵌入式 Tomcat 和spring-boot-starter-web自动装配机制,能绕过手动配置web.xml和server.xml的繁琐步骤。更重要的是,其@RestController注解天然适配 Vue 的 AJAX 请求,避免学生因Content-Type不匹配或 CORS 配置错误导致前端收不到 JSON 数据。
提示:不要用
spring-boot-starter-parent的最新版。湖北工业大学实训服务器常见 JDK 1.8,应锁定spring-boot-starter-parent为2.3.12.RELEASE,否则@RequestBody解析会因 Jackson 版本冲突报HttpMessageNotReadableException。
2.2 商品管理模块的核心 Controller 与 Service 层实现
以“添加商品”为例,Java 后端需暴露 RESTful 接口并处理字段校验。关键点在于:校验逻辑必须落在 Service 层而非 Controller,因为实训作业常要求展示分层架构理解度。
// src/main/java/com/hbut/supermarket/controller/GoodsController.java @RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; // 注意:此处用 @Validated 而非 @Valid,支持分组校验 @PostMapping public ResponseEntity<Map<String, Object>> addGoods(@Validated @RequestBody Goods goods) { Map<String, Object> result = new HashMap<>(); try { goodsService.save(goods); result.put("code", 200); result.put("msg", "添加成功"); return ResponseEntity.ok(result); } catch (IllegalArgumentException e) { result.put("code", 400); result.put("msg", e.getMessage()); return ResponseEntity.badRequest().body(result); } } }// src/main/java/com/hbut/supermarket/service/GoodsService.java @Service public class GoodsService { @Autowired private GoodsMapper goodsMapper; // MyBatis Mapper 接口 @Transactional public void save(Goods goods) { // 校验规则:商品名不能为空、价格必须大于 0、条形码长度为 13 位 if (goods.getName() == null || goods.getName().trim().isEmpty()) { throw new IllegalArgumentException("商品名称不能为空"); } if (goods.getPrice() <= 0) { throw new IllegalArgumentException("商品价格必须大于 0"); } if (goods.getBarcode() == null || !goods.getBarcode().matches("\\d{13}")) { throw new IllegalArgumentException("条形码必须为13位数字"); } goodsMapper.insert(goods); } }2.2.1 Goods 实体类的关键注解与数据库映射
// src/main/java/com/hbut/supermarket/model/Goods.java public class Goods { private Long id; @NotBlank(message = "商品名称不能为空") @Size(max = 50, message = "商品名称不能超过50个字符") private String name; @NotNull(message = "价格不能为空") @DecimalMin(value = "0.01", message = "价格不能小于0.01") private BigDecimal price; @NotBlank(message = "条形码不能为空") @Pattern(regexp = "\\d{13}", message = "条形码必须为13位数字") private String barcode; // getter/setter 省略 }注意:
@NotBlank和@Pattern是 Hibernate Validator 提供的校验注解,需在pom.xml中显式引入:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2.3 数据库连接配置:适配湖北工业大学机房常见的 Oracle/MySQL 双环境
实训环境常要求同一套代码切换数据库。Spring Boot 的application.yml应采用 profile 方式隔离配置:
# src/main/resources/application.yml spring: profiles: active: mysql # 默认启用 mysql 配置 --- spring: config: activate: on-profile: mysql datasource: url: jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver --- spring: config: activate: on-profile: oracle datasource: url: jdbc:oracle:thin:@192.168.1.100:1521:orcl username: supermarket password: supermarket driver-class-name: oracle.jdbc.driver.OracleDriver启动时指定 profile:java -jar supermarket.jar --spring.profiles.active=oracle。这样无需修改代码即可切换数据库,符合高校多机房环境的实际需求。
3. 前端用 Vue + HTML 构建可离线运行的管理界面:避开 Node 环境依赖
3.1 为什么不用 Vue CLI 而用 CDN 引入?——直击高校机房真实限制
HBuilderX 是湖北工业大学计算机学院指定的前端开发工具,其默认不集成 Node.js 环境,且实训机禁止安装 npm。因此,项目前端采用CDN 直接引入 Vue 2.6.14(兼容 IE11) + Axios + Element UI的组合,所有 JS/CSS 通过<script>和<link>标签加载,HTML 文件双击即可运行,完全脱离构建工具。
<!-- src/main/resources/static/index.html --> <!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>湖北工业大学超市管理系统</title> <!-- Element UI CSS --> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <!-- Vue 2.6.14(非 Vue 3,因 Vue 3 需要 Proxy,IE11 不支持) --> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script> <!-- Axios --> <script src="https://cdn.jsdelivr.net/npm/axios@0.21.4/dist/axios.min.js"></script> <!-- Element UI JS --> <script src="https://unpkg.com/element-ui/lib/index.js"></script> </head> <body> <div id="app"> <el-container> <el-header>湖北工业大学超市管理系统</el-header> <el-main> <goods-list></goods-list> </el-main> </el-container> </div> <script> // 全局配置 Axios 基础 URL,指向本地 Tomcat axios.defaults.baseURL = 'http://localhost:8080/api'; // 定义商品列表组件 Vue.component('goods-list', { data() { return { tableData: [], loading: false, dialogVisible: false, form: { name: '', price: '', barcode: '' } } }, methods: { fetchData() { this.loading = true; axios.get('/goods').then(res => { this.tableData = res.data.data; // 注意后端返回结构 }).catch(err => { this.$message.error('获取商品列表失败:' + err.response?.data?.msg || '网络错误'); }).finally(() => { this.loading = false; }); }, submitForm() { axios.post('/goods', this.form).then(res => { this.$message.success(res.data.msg); this.dialogVisible = false; this.fetchData(); this.form = { name: '', price: '', barcode: '' }; }).catch(err => { this.$message.error(err.response?.data?.msg || '添加失败'); }); } }, mounted() { this.fetchData(); }, template: ` <div> <el-button type="primary" @click="dialogVisible = true">添加商品</el-button> <el-table :data="tableData" stripe style="width: 100%" v-loading="loading"> <el-table-column prop="name" label="商品名称"></el-table-column> <el-table-column prop="price" label="价格"></el-table-column> <el-table-column prop="barcode" label="条形码"></el-table-column> </el-table> <el-dialog title="添加商品" :visible.sync="dialogVisible"> <el-form :model="form"> <el-form-item label="商品名称" prop="name"> <el-input v-model="form.name"></el-input> </el-form-item> <el-form-item label="价格" prop="price"> <el-input v-model="form.price" type="number"></el-input> </el-form-item> <el-form-item label="条形码" prop="barcode"> <el-input v-model="form.barcode" maxlength="13"></el-input> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="submitForm">确 定</el-button> </div> </el-dialog> </div> ` }); new Vue({ el: '#app' }); </script> </body> </html>3.1.1 关键参数说明与避坑点
| 参数 | 说明 | 常见错误 |
|---|---|---|
axios.defaults.baseURL | 必须设为http://localhost:8080/api,因 Spring Boot 默认端口 8080,且 Controller@RequestMapping("/api/goods") | 设成/api会导致跨域请求失败;设成http://127.0.0.1:8080/api在部分机房 DNS 解析失败 |
res.data.data | 后端返回格式为{ code: 200, msg: "success", data: [...] },Vue 需取data字段 | 前端未解构直接res.data赋值给tableData,导致表格空白 |
v-model="form.price" | 输入框绑定price字段,但后端接收的是BigDecimal,需确保字符串能被@RequestBody自动转换 | 若输入12.5正常,但输入12,5(逗号小数点)会 400 错误 |
提示:HBuilderX 中调试此 HTML 时,务必右键选择“在内置浏览器中运行”,而非“在 Chrome 中运行”——后者会触发 CORS,而内置浏览器等同于本地文件协议,无跨域限制。
4. Java 与 Vue 的数据契约:定义统一的 API 响应结构与错误处理机制
4.1 后端统一封装 Result 类——让 Vue 前端不再写重复的 if-else 判断
高校实训作业常被要求体现“前后端协作规范”。一个Result<T>包装类能强制约定所有接口返回格式,避免前端对每个接口单独解析code和msg。
// src/main/java/com/hbut/supermarket/common/Result.java public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.msg = "操作成功"; result.data = data; return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.code = 500; result.msg = msg; return result; } // getter/setter 省略 }Controller 改写为:
@PostMapping public Result<Void> addGoods(@Validated @RequestBody Goods goods) { try { goodsService.save(goods); return Result.success(null); } catch (IllegalArgumentException e) { return Result.error(e.getMessage()); } }此时前端 Axios 请求可统一处理:
axios.post('/goods', this.form).then(res => { if (res.data.code === 200) { this.$message.success(res.data.msg); this.dialogVisible = false; this.fetchData(); this.form = { name: '', price: '', barcode: '' }; } else { this.$message.error(res.data.msg); } }).catch(err => { this.$message.error('网络错误,请检查后端是否启动'); });4.2 前端拦截器注入 Token:对接湖北工业大学统一身份认证(UAA)的预备设计
虽然当前版本未接入 UAA,但预留了扩展点。若后续需集成校内 CAS 单点登录,只需在axios.interceptors.request.use中注入 ticket:
// 在 index.html 的 <script> 标签内,Vue 实例创建前添加 axios.interceptors.request.use(config => { // 从 localStorage 读取 ticket(CAS 登录后存入) const ticket = localStorage.getItem('cas_ticket'); if (ticket && config.url.includes('/api/')) { config.headers['X-CAS-Ticket'] = ticket; } return config; });后端对应添加拦截器:
@Component public class CasAuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ticket = request.getHeader("X-CAS-Ticket"); if (ticket == null || !validateCasTicket(ticket)) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或票据无效\"}"); return false; } return true; } }5. 本地运行与调试全流程:从 JDK 配置到 HBuilderX 页面验证
5.1 Java 环境检查清单(湖北工业大学机房典型配置)
| 检查项 | 命令 | 正常输出示例 | 异常处理 |
|---|---|---|---|
| JDK 版本 | java -version | java version "1.8.0_291" | 若显示11.0.15,需切换 JDK:export JAVA_HOME=/opt/jdk1.8.0_291 |
| Maven 版本 | mvn -v | Apache Maven 3.6.3 | 若提示 command not found,使用mvn compile替代mvn package,跳过打包阶段 |
| 端口占用 | netstat -ano | findstr :8080(Windows)lsof -i :8080(Linux/macOS) | 显示 PID | taskkill /PID 1234 /F(Windows)或kill -9 1234(Linux/macOS) |
5.2 三步启动法:确保前后端同时就绪
第一步:启动后端
# 进入项目根目录(含 pom.xml) cd supermarket-backend # 编译并运行(跳过测试,节省时间) mvn clean compile exec:java -Dexec.mainClass="com.hbut.supermarket.SupermarketApplication" -Dmaven.test.skip=true观察控制台输出Tomcat started on port(s): 8080,即启动成功。
第二步:确认接口可用在浏览器访问http://localhost:8080/api/goods,应返回 JSON 数组(即使为空)。若返回 404,检查GoodsController是否加了@RestController和@RequestMapping。
第三步:HBuilderX 打开前端页面
- 将
index.html拖入 HBuilderX 工作区 - 右键 → “在内置浏览器中运行”
- 页面加载后,点击“添加商品”,输入测试数据,观察控制台 Network 标签页中
/api/goods请求状态码是否为 200
5.2.1 最常遇到的 3 个错误及定位方法
| 现象 | 控制台报错 | 定位路径 | 解决方案 |
|---|---|---|---|
| 页面空白,控制台无报错 | Failed to load resource: net::ERR_CONNECTION_REFUSED | 浏览器开发者工具 → Network → 点击/api/goods→ 查看 Preview | 后端未启动,或端口被占,执行netstat检查 8080 |
| 表格有数据但点击“添加”无反应 | Uncaught (in promise) TypeError: Cannot read property 'msg' of undefined | 前端代码中res.data.msg报错 | 后端返回格式不符,检查Result类是否被正确序列化,确认@ResponseBody生效 |
| 添加成功但表格不刷新 | 控制台无错误,Network 显示 200 | Vue Devtools → Components → 查看goods-list组件 data | this.tableData未响应式更新,改用this.$set(this, 'tableData', newData)或确保fetchData()被正确调用 |
5.3 一键导出销售报表:HTML + JavaScript 实现客户端 Excel 生成
系统需支持导出销售流水为 Excel,但高校机房禁用后端 POI 依赖(体积大、易冲突)。采用纯前端方案:用xlsx库将 JSON 数据转为.xlsx文件。
在index.html的<head>中追加:
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>在 Vue 组件中添加导出方法:
methods: { exportToExcel() { // 模拟销售流水数据(实际从 /api/sales 接口获取) const data = [ { id: 1, goodsName: '矿泉水', price: 2.00, quantity: 5, time: '2023-09-01 08:30' }, { id: 2, goodsName: '方便面', price: 5.50, quantity: 2, time: '2023-09-01 09:15' } ]; const ws = XLSX.utils.json_to_sheet(data); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "销售流水"); XLSX.writeFile(wb, "湖北工业大学超市销售报表.xlsx"); } }注意:
xlsx.full.min.js体积约 800KB,若机房网络慢,可改用更轻量的SheetJS精简版,或预生成 CSV(用Blob+URL.createObjectURL),兼容性更强。
6. Vue 组件通信优化:用 Event Bus 解耦商品列表与搜索框
6.1 为什么不用 Vuex?——契合课程设计的轻量级状态管理
Vuex 需额外学习 store、mutation、action 概念,而实训课重点在“能跑通”。Event Bus 是 Vue 2 的经典模式,仅需 3 行代码即可实现跨组件通信,且不增加构建复杂度。
在index.html的<script>标签顶部添加:
// 创建全局事件总线 const EventBus = new Vue();6.1.1 商品搜索功能的完整实现链路
步骤 1:创建搜索组件
<!-- 在 index.html 的 <el-header> 内添加 --> <el-input placeholder="请输入商品名称" v-model="searchKeyword" @keyup.enter.native="onSearch"> <el-button slot="append" icon="el-icon-search" @click="onSearch"></el-button> </el-input>步骤 2:在 Vue 实例 data 中声明搜索关键词
new Vue({ el: '#app', data: { searchKeyword: '' }, methods: { onSearch() { // 触发全局搜索事件 EventBus.$emit('search-goods', this.searchKeyword); } } });步骤 3:商品列表组件监听事件
// 在 goods-list 组件的 mounted 钩子中监听 mounted() { this.fetchData(); // 监听搜索事件 EventBus.$on('search-goods', (keyword) => { if (keyword.trim() === '') { this.fetchData(); // 清空关键词时重载全部 } else { axios.get(`/goods/search?keyword=${encodeURIComponent(keyword)}`).then(res => { this.tableData = res.data.data; }); } }); }, // 组件销毁时移除监听,避免内存泄漏 beforeDestroy() { EventBus.$off('search-goods'); }步骤 4:后端提供搜索接口
@GetMapping("/search") public Result<List<Goods>> searchGoods(@RequestParam String keyword) { List<Goods> list = goodsService.searchByName(keyword); return Result.success(list); }// GoodsService.java public List<Goods> searchByName(String keyword) { return goodsMapper.selectByNameLike("%" + keyword.trim() + "%"); }<!-- GoodsMapper.xml --> <select id="selectByNameLike" resultType="Goods"> SELECT * FROM goods WHERE name LIKE #{keyword} </select>此方案彻底解耦了搜索输入框与商品表格,无需父子组件 props 传递,也无需引入 Vuex,符合高校课程设计“够用就好”的工程原则。
本文还有配套的精品资源,点击获取