“旧时光咖啡厅管理系统”这个项目就是那种典型的Spring Boot Vue前后端分离后台管理系统——但在开发层面又比纯教学项目多了一层“真实门店业务”的味道。刚拿到这个需求的时候我的第一反应是这不就是一个带界面的CRUD吗真正动手做下来才发现难点全在订单流程、会员积分、库存联动这些业务细节上。这套系统的核心不是“管理数据”而是把咖啡厅每天发生的那点事——点单、结账、进货、算账——全部搬到线上让老板不用再靠Excel和纸质小票过日子。如果你是正在做Java课程设计、毕业设计或者打算用一套完整技术栈练手的同学这篇内容会非常有参考价值。我会从需求拆分、数据库设计、后端业务模块、前端页面搭建到部署上线和常见坑点完完整整讲一遍。文中用到的是我实际测试过的一组稳定组合Spring Boot 2.7 MyBatis-Plus MySQL 5.7 Vue 2 Element UI这套组合基本覆盖了目前市面上大多数中小型管理系统的真实形态。1. 项目整体设计与技术选型1.1 系统定位与核心需求拆解“旧时光”是一家开在老街区的咖啡厅主打安静氛围和手冲咖啡。老板最头疼的事大概有这几件客人到店后靠纸笔点单高峰期容易漏记错记会员信息和充值记录全在Excel里月底对账眼睛都快看花库存全凭感觉到了周末才发现牛奶不够用每天卖了多少、哪个品类最受欢迎只能靠一张张翻小票统计。所以这个系统的定位不是简单的一个“后台增删改查”而是围绕门店日常运营的轻量级管理系统。目标用户主要有两类店长admin和店员staff。店长可以管理员工、查看统计报表、调整商品库存店员负责日常点单、结账、会员登记。从功能模块的拆分来看我最终确定了这些核心模块登录与权限模块JWT登录、区分admin/staff角色。员工管理模块新增、禁用员工账号。商品分类与商品管理咖啡、茶饮、甜品、轻食等分类商品上下架、价格调整。桌台管理模拟门店桌号配合点单流程。点单与订单管理选桌台、选商品、生成订单、结账、查看历史订单。会员管理注册会员、充值、积分记录、会员价。库存管理入库、出库与商品销售联动扣减。销售统计按天、按周的营业额统计品类占比。这里有一个值得注意的设计判断我把“点单”和“管理后台”放在同一个系统里而不是做成两个独立系统。原因是咖啡厅规模不大一套系统里用菜单区分入口开发成本最低老板也只用维护一套账号密码。如果你的场景是连锁门店那才需要考虑独立的点单端和总店管理端。1.2 为什么选Spring Boot Vue而不是其他方案做系统选型最重要的不是追新而是看团队的技术掌控力和项目规模。我当时为什么没有用传统的JSPServlet单体方案最核心的原因是前后端分离后改前端页面不再需要重启后端服务联调效率高很多。像这种管理后台界面改动非常频繁今天老板说“列表要多一列”明天说“按钮换个颜色”前后端分离模式下前端用热更新秒级生效体验完全不同。Spring Boot的优势在于“开箱即用”。内嵌Tomcat、自动配置、起步依赖一个mvn spring-boot:run就能把服务跑起来。对比早年Spring MVC那套繁琐的XML配置Spring Boot把复杂度吃掉了不少让开发者能更专注于业务代码。对课程设计或者小团队来说这是性价比极高的选择。Vue这边我用的是Vue 2 Element UI。你可能会问现在Vue 3都这么成熟了为什么还用Vue 2我的理由很实在Element UI生态稳定网上的踩坑案例最多遇到问题一搜就有答案而且Vue 2的API对新手更友好Options API写起来直白。如果你的项目是新起盘且团队没人用过Vue 2直接上Vue 3 Element Plus也没问题只是部分组件用法略有差异整体思路完全一致。顺便说下为什么没上微服务、也没用若依那类脚手架。这个系统的并发量就是一家店几十个人同时下单的水平单机MySQL加一台服务器绰绰有余微服务纯属给自己找麻烦。至于脚手架它能让你快速出一个后台但如果你是为了学习或者答辩建议至少把登录鉴权、订单主流程自己写一遍不然面试官问你JWT怎么实现的你什么都答不上来。2. 数据库设计与核心模块实现2.1 核心表结构设计数据库设计是整个项目的地基。我在设计表结构上花了一天时间后面写代码明显顺很多。这个系统我最终设计了8张核心表下面用表格列一下表名用途关键字段sys_user员工账号店长/店员id, username, password, real_name, role, status, create_timeproduct_category商品分类id, name, sort, statusproduct商品id, category_id, name, price, cost_price, stock, image, status, sales_counttable_info桌台id, table_no, seats, status(空闲/使用中)orders订单主表id, order_no, table_id, member_id, total_amount, discount_amount, actual_amount, pay_type, status, create_timeorder_item订单明细id, order_id, product_id, product_name, price, quantity, subtotalmember会员id, phone, name, balance, points, level, create_timerecharge_record充值记录id, member_id, amount, gift_amount, create_timeinventory_log库存变动日志id, product_id, change_type(in/out/sale), change_qty, remark, create_time有几个设计要点需要展开讲。首先是订单主表和明细表为什么要分开这是典型的“一父多子”关系一个订单对应多个商品。如果不拆表一个订单如果有三个商品就要在orders表里插三行那订单号、会员、金额都会被重复存三次不仅冗余而且没法在订单级别做扩展比如一次订单统一用一个备注。拆开后orders存订单级信息order_item存每个商品的快照逻辑非常清晰。第二个要点订单明细里为什么把product_name和price冗余存储而不是通过product_id去关联商品表查询因为商品价格和名称会变。今天这个咖啡卖28明天搞活动卖20如果订单明细只存product_id一个月后你查历史订单显示的会是当前价格而不是顾客当时实际支付的价格。这种“历史快照”的思想在很多业务系统里都用得到比如电商订单快照、审批流快照。第三个要点金额字段必须用decimal(10,2)不要用double或float。浮点数在计算机里是二进制近似的0.1 0.2 不等于 0.3这在货币计算里是绝对不能接受的。decimal虽然性能略低但胜在精确。另外订单金额相关字段建议都带默认值0避免空指针。还有一点时间字段统一用datetime。字符类型时间戳虽然也能存日期但没法直接做SQL排序和范围查询维护起来很反人类。MySQL连接串记得加上serverTimezoneAsia/Shanghai否则你会被时区问题虐一遍后面我会专门讲。2.2 后端分层架构与关键代码实现后端我采用的是经典的controller - service - mapper三层结构再加一层VO/DTO做数据隔离。所谓VO就是给前端看的对象比如登录接口返回的userInfo里绝对不能有password字段这一步可以在Service里手动set也可以用BeanUtils.copyProperties处理但注意别把敏感字段拷进去了。依赖清单里几个核心依赖如下spring-boot-starter-webWeb基础mybatis-plus-boot-starterMyBatis-Plus单表CRUD不用写SQLmysql-connector-javaMySQL驱动lombok简化实体类getter/setterjjwtio.jsonwebtokenJWT生成与解析hutool-all工具库处理字符串、日期、Excel导出都很方便spring-boot-starter-validation参数校验我用一个实际例子来讲清楚后端核心业务是怎么实现的。以“创建订单”为例这个操作涉及多个表必须放在一个事务里。我贴一段简化后的核心逻辑Service public class OrderServiceImpl extends ServiceImplOrderMapper, Orders implements OrderService { Override Transactional(rollbackFor Exception.class) public Result createOrder(OrderCreateDTO dto) { // 1. 校验桌台和商品 TableInfo table tableService.getById(dto.getTableId()); if (table null || !空闲.equals(table.getStatus())) { return Result.error(桌台不可用); } ListOrderItemCreateDTO items dto.getItems(); if (items null || items.isEmpty()) { return Result.error(订单商品不能为空); } // 2. 计算金额 BigDecimal totalAmount BigDecimal.ZERO; for (OrderItemCreateDTO item : items) { Product product productService.getById(item.getProductId()); if (product null || product.getStatus() ! 1) { return Result.error(商品不存在或已下架 item.getProductId()); } totalAmount totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 生成订单号并保存主表 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setTableId(dto.getTableId()); order.setMemberId(dto.getMemberId()); order.setTotalAmount(totalAmount); order.setDiscountAmount(BigDecimal.ZERO); order.setActualAmount(totalAmount); order.setStatus(1); // 1待支付 2已支付 3已退款 order.setCreateTime(LocalDateTime.now()); this.save(order); // 4. 保存明细并扣减库存 for (OrderItemCreateDTO item : dto.getItems()) { Product product productService.getById(item.getProductId()); OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setSubtotal(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemService.save(orderItem); // 扣减库存 product.setStock(product.getStock() - item.getQuantity()); if (product.getStock() 0) { throw new RuntimeException(库存不足 product.getName()); } productService.updateById(product); } // 5. 占用桌台 table.setStatus(使用中); tableService.updateById(table); return Result.success(order.getId()); } }这段代码充分体现了事务的意义。如果第4步扣库存失败抛出异常第3步保存的订单主表必须回滚否则就会出现“有订单明细但没扣库存”的脏数据。Transactional(rollbackFor Exception.class)里rollbackForException.class很关键因为Spring默认只对运行时异常回滚如果你抛的是自定义的受检异常不加这个参数很可能不会回滚这就是一个典型的坑点。另外要注意一个工程量问题很多人喜欢把业务逻辑全写在Controller里Controller里直接调Mapper。小项目当时看着爽一旦订单、会员、库存之间有关联你就会陷入“一个方法里嵌套五个Mapper”的泥潭。我的建议是哪怕项目再小也要保持Controller薄、Service厚的习惯。Controller只做参数接收和结果返回Service里放业务规则这样维护和答辩讲逻辑都会清晰很多。2.3 登录鉴权与权限控制的落地细节登录鉴权我选的是JWT方案。为什么不用Session因为前后端分离之后前端可能部署在Nginx后端跑在8080端口Session依赖Cookie跨域环境下Cookie的维护是最麻烦的一环。JWT把用户身份信息比如用户ID、角色加密成一个token字符串前端放在请求头Authorization里后端拦截器每次校验token解密不依赖服务端存储天然适合这种架构。我的实现是这样组织的登录接口用户输入用户名密码我用BCrypt加密存储的password去校验比对成功生成token。全局过滤器写一个JwtAuthenticationFilter继承OncePerRequestFilter放行/login和静态资源其余请求都从Authorization头里取token并解析把用户信息放到ThreadLocal或Request域中。角色控制sys_user表里有role字段admin和staff。JWT里也会带role但真正做权限判断时要以数据库实时状态为准不能盲信token里的声明因为用户可能在登录后被禁用。密码加密一定要用BCrypt而不是MD5。MD5已经被彩虹表打烂了同样的密码加密结果永远相同毫无安全性可言。BCrypt每次加密结果都带随机盐同一个“123456”加密出来的字符串都不一样而且自带强度参数暴力破解成本极高。Spring Security里自带BCryptPasswordEncoder但如果你不想引入整个Spring Security单独引一个spring-security-crypto依赖也能用这个工具类。还有一个容易忽视的权限问题前端菜单要按角色显示但后端接口必须也做校验。比如店员角色不应该能调用员工管理的接口。如果只藏前端按钮那你就是给黑客留了一扇门。最简单的做法是在后端的员工管理Controller上加角色校验所有需要店长权限的接口统一走一个注解或拦截器判断避免每个方法都写一遍if。3. 前端页面搭建与接口联调3.1 Vue项目初始化与工程化配置前端我用的是Vue 2 Element UI脚手架创建项目很简单。装依赖的时候有个小建议npm install如果特别慢或者报错可以切换淘宝镜像源但不能只图快注意要看版本兼容性。Element UI要装2.x版本配Vue 2如果你装了Element Plus那就要Vue 3混用基本跑不起来。工程目录我做了这样的划分src/ ├── api/ // 每个模块的接口请求封装 │ ├── auth.js │ ├── product.js │ └── order.js ├── assets/ ├── components/ // 公共组件 ├── router/ // 路由配置 ├── store/ // Vuex状态管理 ├── utils/ │ └── request.js // axios封装 ├── views/ │ ├── Login.vue │ ├── Dashboard.vue │ ├── ProductList.vue │ ├── OrderPage.vue │ └── Statistics.vue └── main.js这里核心是把axios封装好。我在utils/request.js里做了两件事请求拦截器自动加上Authorization头从本地存储里取出token注入进去响应拦截器统一处理返回结构。代码大致这样import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error Promise.reject(error)) service.interceptors.response.use( response { const res response.data // 后端统一返回 { code, msg, data } if (res.code ! 200) { Message.error(res.msg || 请求错误) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.msg)) } return res }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) } ) export default service为什么一定要统一封装因为如果不封装每个页面都要自己判断接口返回状态、自己处理401跳登录代码写到最后全是重复的逻辑而且一旦后端改了提示语格式前端几十个文件你要逐个改。封装之后业务页面只需要拿到res.data直接渲染就行。3.2 核心页面实现思路与关键代码整个系统里我花时间最多的页面其实是点单页。咖啡厅使用频率最高的就是它柜台点单、服务员下单、确认结账都要在同一个页面完成。页面的布局是左右结构左侧按分类展示商品卡片点击或点击“”加入购物车右侧是当前订单区展示已选商品、数量、小计底部是桌台选择和结账按钮。点单页的购物车状态管理要重点设计。我用Vuex存了一个cart数组每个元素是{productId, name, price, quantity}对应后端订单明细DTO。加入商品时判断是否已存在存在就加数量不存在就push一条。这个逻辑看起来简单但涉及到对象引用和响应式更新如果直接用this.cart.push可能没问题但如果直接修改数组里某个对象的属性Vue 2的响应式系统可能检测不到这时候需要用Vue.set或者整体替换数组。这是Vue 2一个非常经典的坑。结账逻辑的前端代码如下methods: { async submitOrder() { if (!this.currentTableId) { this.$message.warning(请先选择桌台) return } if (this.cart.length 0) { this.$message.warning(购物车不能为空) return } const payload { tableId: this.currentTableId, memberId: this.currentMemberId || null, items: this.cart.map(item ({ productId: item.productId, quantity: item.quantity })) } const res await createOrder(payload) if (res.code 200) { this.$message.success(下单成功) this.cart [] this.loadTables() this.loadTodayOrders() } } }商品管理页相对简单就是Element UI的el-table展示商品列表配合el-dialog弹窗表单做新增和编辑。如果商品有图片上传图片这块要注意图片不能传给后端把二进制内容存进MySQL那会把数据库撑爆。正确做法是把图片上传到服务器指定的upload目录数据库只存文件路径比如/upload/coffee1.jpg前端直接用这个相对路径拼接完整URL展示。统计报表页是店长角色最看重的页面。我用ECharts做了一个近7天营业额折线图和一个品类销售占比饼图。后端提供一个统计接口返回最近7天的日期和营业额前端拿到数据塞进ECharts配置。这里有一个数据处理技巧后端返回的日期格式是2024-05-20T10:30:00这种带T的ISO格式前端要展示成“05/20”需要在JavaScript里做格式化否则界面会很难看。3.3 前后端联调与跨域处理开发阶段最烦的问题就是跨域。前端跑在本地8080Vue CLI默认端口后端跑在9090不同端口就算跨域。我的解决办法是在vue.config.js里配置代理module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样做的好处是开发时前端页面请求的地址是/api/login代理服务器会把请求转发到后端的/login浏览器看到的始终是同源的8080端口自然没有跨域问题。生产环境怎么办用Nginx做反向代理。前端打包后的dist目录放到Nginx的html目录后端jar包跑在9090Nginx配置里把/api开头的请求转发到后端。核心配置如下server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files那一行非常重要。Vue Router默认是history模式路由路径比如/admin/product刷新页面时Nginx会去服务器找这个路径对应的文件找不到就404。try_files会把请求回退到index.html由前端路由重新解析问题才能解决。如果你不想配Nginx也行把Vue Router改成hash模式URL里带个#号刷新也没问题但界面不太好看。联调过程中还有一个容易被忽略的坑日期格式。后端返回的LocalDateTime默认序列化成“2024-05-20T14:30:00”中间有个T。前端要么手动replace掉写正则replace(T, )要么后端加一个Jackson配置全局把LocalDateTime序列化成“yyyy-MM-dd HH:mm:ss”。我建议在application.yml里配置spring.jackson.date-format和time-zone一劳永逸。4. 系统安全、性能优化与常见问题排查4.1 安全防护与基础性能优化一个管理系统挂在公网上最基本的安全措施不能省。我的做法是三层防护。第一层SQL注入防护。MyBatis-Plus的Wrapper在绝大多数情况下都用参数绑定方式生成SQL基本免疫SQL注入。但如果你写了原生XML的SQL一定要用#{}而不是${}${}是字符串拼接用户输入拼进SQL就直接完蛋。比如按关键字搜索商品order by这种动态字段确实只能用${}这时要校验字段名是否在白名单里比如只允许列表里的固定字段名传入。第二层XSS过滤。用户输入的商品名称、备注都可能携带
企业数字化 ERP 产品动态
相关推荐
SSM航班订票系统开发全解析:数据库设计与并发控制实战 每年到这个时间点,我的私信里就会出现同一个问题:“航班订票系统用SSM怎么写?”或者更直白一点:“能不能给我一套完整能跑的SSM航班订票系统?”说实话,这个题目确实是高校课设和毕设里的常青树,… · 2026/9/26 16:50:02
Unity2D射击游戏开发:雷霆战机项目结构、对象池与碰撞检测全解析 简介:这是一份基于 Unity 引擎制作的 2D 游戏「雷霆战机」演示案例工程,面向 Unity 初学者、独立游戏开发者及休闲射击游戏爱好者,能够帮助读者快速了解横版/纵版战机射击类游戏的项目组织方式,理解场景搭建、战机操控、敌机生成、… · 2026/9/26 16:49:49
学生报名系统v2012:PHP老源码复现与二次开发全攻略 简介:学生报名系统源码 v2012 是一套基于 PHP 的报名管理解决方案,主要面向学校、培训机构等需要批量处理报名数据的场景。系统核心功能涵盖分区管理、学生信息登记(报名号、姓名、性别、出生年月、毕业学校、身份证号、家庭住址、父母姓名、… · 2026/9/26 16:49:49
Python3 提取 MySQL 数据并转字典数组:TaoToken 统一 Key 配置与验证 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 17:23:11
豆包AI做视频:不会剪辑也能出成片,开源skills安装与实操教程 前几天有位朋友来找我,开口就问:“我完全不会剪辑,真的能用豆包做视频吗?”我说这个问题问反了——你不会剪辑,恰恰是豆包这类AI工作流最有价值的场景。传统视频制作卡在操作门槛上:剪辑软件的时间线、轨道… · 2026/9/26 17:23:11
AI-Agents实战指南:从任务拆解到工具调用与记忆管理 1. 从"能聊"到"能干":AI-Agents到底在解决什么问题这两年大家跟大模型打交道的方式,基本还停留在"你问我答"的阶段——我敲一段话,它回一段话,聊得挺热闹,但聊完就散了,活儿… · 2026/9/26 17:23:11
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46