简介面向高校毕业设计、课程设计场景的流动摊位管理系统完整项目基于 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 停业。位置信息不要用字符串硬拼分成area和location两个字段后续做按片区统计就要靠它。商品表要冗余一个stall_id因为一个摊位卖什么、卖了多少是系统最频繁的查询维度。销售单和库存流水必须分开。销售单记录“一次性卖出了什么”库存流水记录“每个商品每次增减的明细”。如果只记销售不记流水退货、调拨、报损都没法追溯。我一般会给每张表都加create_time和update_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 quantity和version。前者防止超卖后者防止两个人同时改同一件商品时互相覆盖。如果更新影响行数为 0说明库存不够或数据已被别人改过后端要抛业务异常并回滚事务。对于毕业设计答辩来说能讲清楚这一条 SQL 的意图比背十个设计模式更有说服力。3. Spring Boot 后端JWT 登录、权限控制与业务接口实现3.1 工程结构与 Maven 依赖源码包解压后是标准的 Maven 工程src/main/java下按controller/service/mapper/entity分层。pom.xml里最核心的依赖是spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java和jjwt。用 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.connectionTimeout120000 -Dmaven.wagon.http.soTimeout120000同时确认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里放的是userId和role后续业务接口直接从 request 里取不需要再查一次库。角色判断在WebMvcConfigurer里配置拦截路径时做/api/admin/**要求 role 为admin/api/stall/**允许摊主角色访问。3.3 核心接口实现商品上架与销售下单商品上架接口是最能体现项目完整度的部分。除了常规的新增记录还要做参数校验和库存初始化。PostMapping(/product) public ResultProduct 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 里不能出现id、createTime这类前端不该传入的字段否则会有越权覆盖的风险。我的习惯是 DTO 只放前端允许传的字段实体里其他的保持默认值。销售下单接口要加Transactional因为要同时写sale_order和更新库存。这里的事务边界是插入订单成功、扣减库存成功两个操作要么都成功要么都回滚。如果扣库存那条 UPDATE 影响行数为 0直接throw new RuntimeException(库存不足)事务会自动回滚订单记录也不会残留。3.4 application.yml 配置要点配置文件中需要留意的不是端口和数据库账号而是三个容易踩坑的地方。第一spring.datasource.url要带useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8不加时区参数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.StdOutImplmap-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下按admin和stall分包。路由配置使用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.js里app.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-upload的action要指向后端的上传接口并且带上 token。headers里加Authorization的方式和request.js拦截器一致。销售统计页面的核心是调用后端聚合接口然后把返回的数组直接传给 ECharts。这里有一个高频坑后端返回的日期格式是2025/06/01ECharts 的 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_amount是decimal类型后端转 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 DESCCOUNT(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 -jar报no 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.htmlnginx 不会。解决方式是配一条 try_files 规则location / { try_files $uri $uri/ /index.html; }这条配置的含义是请求的路径找不到对应文件时直接返回index.html由前端路由接管。部署完成后验证的方法是直接访问http://ip:port/stall这类子路由按 F5 刷新不报 404再打开浏览器开发者工具的 Network 面板检查所有静态资源的状态码是否为 200。如果发现有资源返回 304 但样式错乱优先清空浏览器缓存再硬刷新一次。本文还有配套的精品资源点击获取