Spring Boot + Vue + MySQL 流动摊位管理系统设计与实现
2026/9/17 0:44:10 网站建设 项目流程

简介:面向高校毕业设计、课程设计场景的流动摊位管理系统完整项目,基于 Java、Spring Boot、Vue、MySQL 构建,采用前后端分离架构,覆盖摊位管理、商品管理、销售管理、库存管理、数据分析与权限管理等核心模块,功能完整且界面友好。zip 压缩包共 449 个文件,约 76.52MB,以 130 个 Java 后端源码、100 个 Vue 前端组件、42 个 JavaScript 脚本、21 个 XML 与 CSS 样式文件为主,另含数据库 SQL 脚本、图片素材、说明文档及演示视频,便于按模块学习。项目使用 IntelliJ IDEA 开发,搭配 Maven 部署与 Navicat 数据库管理工具,可直接导入运行,数据库脚本完整,有助于理解数据表设计与前后端交互流程。目前已有 64 人学习下载,适合需要快速完成毕业设计、课程设计或期末大作业的学生。除完整源码与数据库外,资源还包含环境配置与页面示例,可支撑二次开发,且经过严格调试确保可运行,具备较高的工程实践参考价值。

1. 流动摊位管理系统选型:为什么要用 Spring Boot + Vue + MySQL

摆摊不是一个新生意,但它的管理方式还停留在纸质台账和微信记账上。摊主关心今天卖了多少、剩多少货、哪条街的摊位费该交了;管理者关心摊位有没有空置、哪个片区流水高、月底报表从哪来。这套基于 Java + Spring Boot + Vue + MySQL 的流动摊位管理系统,把这些问题压缩成了一套前后端分离的毕业设计工程:后端负责登录鉴权、摊位档案、商品上下架、销售与库存流水,前端用 Vue 做后台管理界面,MySQL 存全部业务数据。适合两类人:一是拿它做毕业设计或课程设计的学生,下载后能直接跑通流程;二是真正想把路边摊、集市、夜市摊位管起来的小团队,把演示数据清掉就能当简易 SaaS 用。它的价值不在于技术多新,而在于业务闭环完整——从建表到出报表,每个环节都能对照源码改。

2. 数据模型设计:摊位、商品、订单、库存的 MySQL 表结构

2.1 核心表拆分与字段选型

流动摊位系统里最容易被做成一张大表的,就是把商品、销售、库存全塞进orders。实际工程里正确的拆法是按业务实体拆,再通过外键关联。这个项目我在看库结构时,重点确认的是四张业务表:stall(摊位)、product(商品)、sale_order(销售单)、inventory_log(库存流水)。

摊位表不能只存“哪个位置”,还要区分状态。流动摊位的核心属性是“今天在不在、在哪个点”,所以状态字段推荐用枚举值:0 空闲1 营业中2 停业。位置信息不要用字符串硬拼,分成arealocation两个字段,后续做按片区统计就要靠它。商品表要冗余一个stall_id,因为一个摊位卖什么、卖了多少,是系统最频繁的查询维度。

销售单和库存流水必须分开。销售单记录“一次性卖出了什么”,库存流水记录“每个商品每次增减的明细”。如果只记销售不记流水,退货、调拨、报损都没法追溯。我一般会给每张表都加create_timeupdate_time,这个项目里也保留了这两个审计字段,做数据报表时它们就是时间维度的唯一依据。

2.2 建表脚本与 Navicat 导入

源码包里带的是.sql脚本,用 Navicat 导入即可。如果你要自己重建,核心脚本可以按下面这个骨架来写,注意外键别建太多,流动摊位这种规模的项目,逻辑关联用应用层控制比数据库外键更灵活。

CREATE TABLE `stall` ( `id` int NOT NULL AUTO_INCREMENT, `stall_no` varchar(32) NOT NULL COMMENT '摊位编号', `area` varchar(64) DEFAULT NULL COMMENT '所属片区', `location` varchar(128) DEFAULT NULL COMMENT '具体位置', `owner_name` varchar(32) DEFAULT NULL COMMENT '摊主姓名', `status` tinyint DEFAULT '1' COMMENT '0空闲 1营业中 2停业', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_stall_no` (`stall_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='摊位表'; CREATE TABLE `sale_order` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '销售单号', `stall_id` int NOT NULL COMMENT '摊位ID', `product_id` int NOT NULL COMMENT '商品ID', `quantity` int NOT NULL COMMENT '销售数量', `unit_price` decimal(10,2) NOT NULL COMMENT '成交单价', `total_amount` decimal(10,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_stall_time` (`stall_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售单表';

这段脚本里有两个容易被忽略的点。total_amount用的是 MySQL 5.7 支持的生成列,把“单价乘数量”直接交给数据库计算,应用层少写一行代码也少一次出错机会。idx_stall_time是复合索引,专供“查某个摊位某段时间的流水”这种高频查询,否则数据量上来后按时间范围统计会走全表扫描。Navicat 里导入时选“运行 SQL 文件”,字符集选utf8mb4,不要用默认的utf8,否则商品名里遇到 emoji 或生僻字会变问号。

2.3 库存扣减与数据一致性

销售和库存是两个动作,但从业务上讲必须是一个原子操作。常见做法是在sale_order插入的同时更新product表的stock字段,并且给商品表加一个乐观锁版本号:

UPDATE product SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{productId} AND stock >= #{quantity} AND version = #{version};

注意这里的WHERE条件同时校验了stock >= quantityversion。前者防止超卖,后者防止两个人同时改同一件商品时互相覆盖。如果更新影响行数为 0,说明库存不够或数据已被别人改过,后端要抛业务异常并回滚事务。对于毕业设计答辩来说,能讲清楚这一条 SQL 的意图,比背十个设计模式更有说服力。

3. Spring Boot 后端:JWT 登录、权限控制与业务接口实现

3.1 工程结构与 Maven 依赖

源码包解压后是标准的 Maven 工程,src/main/java下按controller/service/mapper/entity分层。pom.xml里最核心的依赖是spring-boot-starter-webmybatis-plus-boot-startermysql-connector-javajjwt。用 IDEA 打开时,如果你本地的 JDK 版本和项目不一致,可以在Project Structure里把 SDK 切到 1.8 或 11,注意pom.xml里的java.version要和本机一致,否则 Maven 编译会报invalid target release

很多人在这步会卡在 Maven 依赖下载慢。IDEA 里打开Settings -> Maven -> Importing,把VM options设为-Dmaven.wagon.http.connectionTimeout=120000 -Dmaven.wagon.http.soTimeout=120000,同时确认User settings file指向的settings.xml里配置了阿里云镜像。依赖拉不下来时,看的是Local repository路径下有没有lastUpdated后缀的文件,有就删掉对应目录再重新Reload All Maven Projects

3.2 基于 JWT + 拦截器的权限控制项目工程

权限这块没有引入 Spring Security,而是用拦截器 + JWT 手动实现的,这对毕业设计来说更轻,也更容易在答辩时讲清楚。逻辑是:用户登录成功后,后端用jjwt生成一个 token,前端每次请求在Authorization头里带上它,拦截器解析 token 并取出用户角色。

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或token已过期"); } Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }

这段拦截器的关键在OPTIONS请求直接放行。前后端分离后,Vue 开发服务器和 Spring Boot 端口不同,浏览器发复杂请求前会先发一个预检请求,如果拦截器把预检请求也拦了,前端会一直报 401。Claims里放的是userIdrole,后续业务接口直接从 request 里取,不需要再查一次库。角色判断在WebMvcConfigurer里配置拦截路径时做,/api/admin/**要求 role 为admin/api/stall/**允许摊主角色访问。

3.3 核心接口实现:商品上架与销售下单

商品上架接口是最能体现项目完整度的部分。除了常规的新增记录,还要做参数校验和库存初始化。

@PostMapping("/product") public Result<Product> addProduct(@RequestBody @Valid ProductDTO dto) { Product product = new Product(); BeanUtils.copyProperties(dto, product); if (product.getStock() == null || product.getStock() < 0) { product.setStock(0); } product.setStatus(1); productService.save(product); return Result.success(product); }

@Valid配合 DTO 上的@NotBlank@NotNull注解做入参校验,比在方法里写一堆if判断干净。BeanUtils.copyProperties把前端传来的 DTO 属性拷贝到实体,但要注意 DTO 里不能出现idcreateTime这类前端不该传入的字段,否则会有越权覆盖的风险。我的习惯是 DTO 只放前端允许传的字段,实体里其他的保持默认值。

销售下单接口要加@Transactional,因为要同时写sale_order和更新库存。这里的事务边界是:插入订单成功、扣减库存成功,两个操作要么都成功要么都回滚。如果扣库存那条 UPDATE 影响行数为 0,直接throw new RuntimeException("库存不足"),事务会自动回滚,订单记录也不会残留。

3.4 application.yml 配置要点

配置文件中需要留意的不是端口和数据库账号,而是三个容易踩坑的地方。第一,spring.datasource.url要带useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8,不加时区参数,MySQL 8.x 驱动连 5.7 库会报时间类型转换错误;第二,spring.jackson.date-format要显式声明,否则后端返回的LocalDateTime会序列化成数组格式,前端拿不到yyyy-MM-dd HH:mm:ss字符串;第三,上传文件大小限制。

spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

map-underscore-to-camel-case必须开着,它能把数据库的stall_no自动映射成实体里的stallNo字段。否则 MyBatis-Plus 查出来的结果所有驼峰属性都是 null。StdOutImpl会在控制台打印每条 SQL 和参数,调试阶段建议保留,上线前改成org.apache.ibatis.logging.nologging.NoLoggingImpl

4. Vue 前端架构:路由、状态管理与接口联调

4.1 前端工程结构与路由配置

前端工程是vue-cli初始化的 SPA 应用,入口在src/main.js,页面组件放在src/views下,按adminstall分包。路由配置使用vue-router的懒加载方式,同时通过路由元信息里的roles字段控制菜单可见性。

const routes = [ { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') }, { path: '/admin', component: () => import('@/layout/AdminLayout.vue'), meta: { roles: ['admin'] }, children: [ { path: 'stalls', name: 'StallList', component: () => import('@/views/admin/StallList.vue') }, { path: 'products', name: 'ProductList', component: () => import('@/views/admin/ProductList.vue') }, { path: 'reports', name: 'ReportBoard', component: () => import('@/views/admin/ReportBoard.vue') } ] } ]

component: () => import(...)是路由懒加载,打包后每个页面会拆成单独的 JS 文件,首屏只加载登录页,登录后再按需拉取后台页面。meta.roles配合全局前置守卫判断用户角色,不是管理员就跳转到摊主首页。很多人在vue-router里配了路由却没有在main.jsapp.use(router),导致页面白屏且控制台只提示Cannot read properties of undefined,这个要优先排查。

4.2 axios 封装与 token 注入

前端所有请求走封装好的request.js,核心作用是统一注入 token、统一处理 HTTP 错误码、统一拦截业务错误码。如果不做这层封装,每个页面都要重复写headers: { Authorization: 'Bearer ' + token },而且后端一旦改了响应格式,所有页面都要跟着改。

import axios from 'axios' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { return Promise.reject(error) } )

项目正处于开发模式,baseURL用了/api相对路径,依靠 Vue CLI 的 devServer 代理把请求转发到http://localhost:8080

devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

changeOrigin: true必须开,否则后端拿到的请求头Host是前端地址,部分后端框架做校验时会拒绝。登录成功后把 token 存到localStorage而不是内存变量,刷新页面后重新读取,避免刷新一次就要重新登录。应答拦截器里遇到code 401时清掉 token 并跳回登录页,这样后端 token 过期后,用户会被自动踢回登录界面,而不是停留在页面里看着数据加载失败。

4.3 商品上架与销售统计的页面逻辑

摊主端的商品上架页面用的是el-form表单,提交前做二次校验。注意提交的不是 FormData 而是 JSON,所以Content-Type要由 axios 自动设置为application/json。页面里商品图片如果走文件上传,el-uploadaction要指向后端的上传接口,并且带上 token。headers里加Authorization的方式和request.js拦截器一致。

销售统计页面的核心是调用后端聚合接口,然后把返回的数组直接传给 ECharts。这里有一个高频坑:后端返回的日期格式是2025/06/01,ECharts 的 x 轴也能显示,但排序会变成字符串排序,导致06/01排在06/10后面。我的处理方式是让后端直接返回DATE_FORMAT(create_time, '%Y-%m-%d')格式化后的字符串,并保证 SQL 里ORDER BY create_time,前端就不再关心排序问题。

const res = await request.get('/report/daily', { params: { stallId: this.selectedStall, days: 7 } }) this.chartOption = { xAxis: { data: res.data.map(item => item.date) }, series: [{ data: res.data.map(item => Number(item.totalAmount)), type: 'line' }] }

res.data.map做数据提取,比在模板里用v-for硬渲染图表数据更清晰。注意total_amountdecimal类型,后端转 JSON 后可能变成字符串,前端要Number()强转,否则 ECharts 的 y 轴会奇怪地偏大,或者 tooltip 显示为字符串拼接。

5. 数据分析与报表聚合:MySQL 统计 SQL 与前端图表联动

5.1 按摊位维度聚合的多表关联 SQL

数据分析功能是本系统区别于普通 CRUD 项目的关键点。摊主看的是“我今天赚了多少”,管理员看的是“哪个片区空置率高、哪个商品周转快”,这两种需求对应完全不同的聚合粒度。

SELECT s.stall_no, s.area, COUNT(DISTINCT so.order_no) AS order_count, SUM(so.total_amount) AS total_sales FROM sale_order so JOIN stall s ON so.stall_id = s.id WHERE so.create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY s.stall_no, s.area ORDER BY total_sales DESC

COUNT(DISTINCT so.order_no)COUNT(*)的区别在于:一张销售单可能包含多个商品条目,如果直接COUNT(*)会算出 3 条商品记录当成 3 单,而实际只有 1 单。统计销售额时SUM(so.total_amount)是安全的,因为total_amount是生成列,本身已经按商品计算好了。INTERVAL 7 DAY这种写法比DATE_SUB(NOW(), INTERVAL 7 DAY)更精确,按天统计时不会把今天的数据拦掉一半。

5.2 库存预警 SQL:找出需要补货的商品

库存管理不只要能改数字,还要能主动发现异常。我给这个项目补了一个库存预警接口,SQL 如下:

SELECT p.id, p.name, p.stock, p.safety_stock, CASE WHEN p.stock = 0 THEN '已售罄' WHEN p.stock < p.safety_stock THEN '库存偏低' ELSE '正常' END AS stock_status FROM product p ORDER BY FIELD(stock_status, '已售罄', '库存偏低', '正常')

FIELD函数是 MySQL 特有的排序技巧,ORDER BY FIELD(stock_status, '已售罄', '库存偏低')会优先抛出售罄和预警商品,而ORDER BY stock只能排出数字大小,不能体现业务严重程度。safety_stock安全库存字段需要在商品表里预留,否则这个查询跑不起来。

5.3 报表接口的分层实现

统计接口不要写在业务 Controller 里,单独开一个ReportController,它内部走ReportService,再调用多个 Mapper 的聚合方法。前端调用/report/daily得到当天的销售额曲线,调用/report/stall-ranking得到摊位排行。这两套接口的核心区别在前者的GROUP BY粒度是日期,后者是摊位。开发时一个小技巧是先用 Navicat 跑通 SQL,确认数据无误后再把 SQL 复制进 Mapper XML,调整#{}参数。这样可以避免日志里一堆SQLSyntaxErrorException才知道是哪里拼接出错。

6. 部署排错:Maven 打包、跨域、端口占用与 Vue 打包后布局异常

部署顺序通常是这样:先在 IDEA 里用 Maven 的package命令打出jar包,再把前端npm run build生成的dist目录交给 nginx 或直接放进 Spring Boot 的static目录。毕业设计演示两种都行,但单独部署前后端更能体现分离架构。

后端打包时要确认pom.xml里有spring-boot-maven-plugin,并且<mainClass>指向带main方法的启动类。如果打包后java -jarno main manifest attribute,就是这个插件没配好。打出来的 jar 包一般在target目录下,约 40~70MB,这个体积正常,因为内嵌了 Tomcat。启动时如果报端口被占用,用netstat -ano | findstr 8080找到 PID,再taskkill /PID 进程号 /F,别盲目改端口,因为前端 devServer 代理和数据库连接都假设后端在 8080。

Vue 打包后布局异常是个高频问题,报错现象是本地npm run serve一切正常,npm run build部署上去后图片不显示、路由跳转后刷新变 404。图片不显示多半是publicPath配置问题,需要在vue.config.js里显式设置:

module.exports = { publicPath: '/', outputDir: 'dist', assetsDir: 'static' }

publicPath默认值在打包时会生成相对路径./,部署在子目录没问题,但部署在根路径时部分浏览器解析 CSS 里的字体和图片会出现偏差。刷新 404 是history模式路由的问题,开发服务器会帮你回退到index.html,nginx 不会。解决方式是配一条 try_files 规则:

location / { try_files $uri $uri/ /index.html; }

这条配置的含义是:请求的路径找不到对应文件时,直接返回index.html,由前端路由接管。部署完成后验证的方法是直接访问http://ip:port/stall这类子路由,按 F5 刷新不报 404,再打开浏览器开发者工具的 Network 面板,检查所有静态资源的状态码是否为 200。如果发现有资源返回 304 但样式错乱,优先清空浏览器缓存再硬刷新一次。

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

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

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

立即咨询