
简介基于JavaSpringBootVue的摄影约拍管理系统源码面向计算机专业毕业生与课程设计者提供前后端分离的完整项目方案。后台支持管理员登录、用户与摄影师管理、拍摄地点/风格/套餐配置、约拍订单跟踪、服务评价、商品及论坛管理前台提供用户/摄影师注册登录、首页套餐推荐、私聊评论、收藏预约、购买支付、帖子发布与个人中心等功能覆盖实际业务中的核心流程。资源包共1916个文件包含316个Vue前端页面、227个Java源文件及对应class二进制文件、126个JavaScript脚本、64个XML配置另有HTML/CSS样式、图片素材、SQL数据库脚本、bat快捷启动脚本及MP4演示视频等整体压缩包约131.65MB目录结构清晰、便于按模块查阅。已有79人浏览学习项目已测试可正常运行可直接导入IDE运行调试也可作为二次开发的基础脚手架帮助理解SpringBootVue前后端交互、MyBatis数据持久化及Maven工程管理等关键知识点。1. 约拍系统先想清楚的三条链路摄影约拍这类管理系统真正难的不是 CRUD而是把“人、单、钱”三条链路在同一套代码里跑通。人指的是用户、摄影师、管理员三种角色怎么在同一个登录入口下区分权限单指的是从用户提交拍摄预约、摄影师接单、到拍摄完成、双方评价的整条流程钱则落在套餐定价、订单金额、充值余额和商品交易上。这次拿到的项目是基于 Java SpringBoot Vue 的摄影约拍管理系统后台覆盖用户管理、约拍管理、商品管理、论坛管理和系统管理前台把摄影师展示、套餐预约、商品购买、公告论坛和个人中心全部串了起来。对做毕业设计、课程设计或者想快速跑通一个前后端分离完整项目的开发者来说这套代码最大的价值是帮你省掉从零搭权限和订单状态机的体力活。2. SpringBoot 后端分层与用户角色权限设计2.1 Controller、Service、Mapper 三层边界怎么划这个项目后端用的是 SpringBoot MyBatis Maven 的经典组合包结构基本是controller、service、mapper、entity四层。很多人在课程设计里容易犯的错是把业务逻辑写在 Controller 里而这个约拍系统的划分方式是Controller 只做参数接收、调用 Service、返回统一结果集Service 做业务编排比如预约下单时先查套餐库存、再校验用户余额或积分、最后插入订单Mapper 只负责 SQL 和实体映射。记住这个边界能帮你省大量改 bug 的时间。以用户登录为例流程是前端把用户名密码传到UserControllerController 不做密码校验直接调用UserService.login(request)Service 里用selectOne查库比对密码后生成 token 返回。从代码阅读顺序上只要沿着 Controller 入口往下看很快能定位到每个功能的实现位置。RestController RequestMapping(/api/user) public class UserController { Resource private UserService userService; PostMapping(/login) public Result login(RequestBody LoginRequest req) { // 只做参数接收不写业务逻辑 return userService.login(req); } }这里Result是统一的响应封装一般包含code、msg、data三个字段。RequestMapping(/api/user)是路由前缀约拍相关的接口按业务模块拆成/api/appointment、/api/order、/api/product等这种命名方式在前端 axios 请求里非常好对应。2.2 三种角色的权限控制前端路由守卫 后端注解用户、摄影师、管理员三种角色在这个系统里共用一个登录入口区分方式是在用户表里加一个角色字段一般用整数role表示管理员、摄影师、普通用户。前端根据登录后拿到的角色值决定显示哪些菜单和按钮比如普通用户看不到后台管理入口摄影师能看到“接单管理”却看不到“用户列表”。后端权限则是靠拦截器实现不是每个接口写死 if 判断。做法是自定义一个RequireRole注解标注在 Controller 方法上再用 Spring 的HandlerInterceptor统一拦截请求从 token 里解析出当前用户角色做比较不匹配直接返回 401。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int value(); // 1管理员2摄影师3用户 }Component public class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (handler instanceof HandlerMethod) { HandlerMethod hm (HandlerMethod) handler; RequireRole rr hm.getMethodAnnotation(RequireRole.class); if (rr ! null !checkRole(request, rr.value())) { response.setStatus(401); return false; } } return true; } }preHandle在请求进入 Controller 之前执行getMethodAnnotation判断当前接口是否需要特定角色checkRole内部从请求头里的 token 解析当前登录人的角色字段。这种设计的好处是权限校验和业务逻辑完全解耦后面想加“摄影师只能改自己的套餐”这种细粒度控制不需要动业务代码单独加注解就行。2.3 用户管理与摄影师列表的表结构设计思路用户管理在后台分了用户列表和摄影师列表说明库表设计上是同一张用户表通过角色字段区分展示而不是摄影师单独建一张表。摄影师的额外信息是通过关联字段挂在用户主键下面比如摄影师介绍、擅长风格、作品集字段可以放在用户表的扩展列也可以拆分photographer_info表用外键关联。我一般推荐第二种因为用户表太宽会影响查询性能而且摄影师信息和登录信息变更频率不在一个级别。角色权限矩阵可以这样整理写代码和做演示都清晰功能模块管理员摄影师普通用户用户管理增删改查无无约拍套餐管理增删改查查看查看并预约预约订单处理全部订单接单/完成拍摄发起预约服务评价管理删除查看回复提交评价商品购买管理商品购买购买论坛发布管理帖子发帖回帖发帖回帖MyBatis的 Mapper 层在查询用户列表时通常用where标签把角色作为动态条件传入页面上的角色筛选框直接映射这个字段。注意区分“角色筛选”和“权限控制”是两回事前者是查询条件后者是请求拦截别混在一处写。3. 约拍订单状态机设计从预约到评价的数据流转3.1 订单状态字段与迁移路径约拍系统的业务闭环核心是拍摄预约的完整生命周期。这个项目里订单的状态流转依次是用户提交拍摄预约、摄影师确认接单、拍摄执行完成、双方服务评价。数据库订单表用一个整型状态字段就够了常见取值对应关系是 0 待确认、1 已确认待拍摄、2 拍摄完成待评价、3 已完成。有些实现还会加 4 已取消本项目不必额外扩展。订单表字段设计要和前台页面对齐至少包含这些列订单号、下单用户 ID、摄影师 ID、套餐 ID、预约拍摄时间、拍摄地点、订单金额、状态、下单时间、完成时间。把订单号独立出来而不是直接用自增主键是为了前端列表展示、后台检索、以及和支付回调对接时更好追踪。状态迁移中比较关键的是不允许跳变比如用户提交的预约必须先经过摄影师确认不能直接把状态置为拍摄完成。这块用 service 层方法控制而非数据库触发器主要是为了把状态流转的业务校验写在代码里方便加日志和后续功能扩展。下面是核心的表结构语句CREATE TABLE appointment_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 下单用户, photographer_id BIGINT NOT NULL COMMENT 摄影师ID, package_id BIGINT NOT NULL COMMENT 拍摄套餐ID, shoot_time DATETIME COMMENT 预约拍摄时间, address VARCHAR(255) COMMENT 拍摄地点, amount DECIMAL(10,2) COMMENT 订单金额, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2拍摄完成 3已完成, create_time DATETIME, update_time DATETIME );这里的status注释最好用纯数字对应状态含义后端用枚举类做语义映射前端页面展示时通过字典翻译不要把中文状态直接落库。原因很简单中文值一旦存入数据库后续状态改名时你得写一大堆 update 语句而用数字只需改前端翻译映射。3.2 Service 层状态流转的代码控制状态变更不能散落在各个 Controller 里必须收拢到订单 Service 的统一入口方法中。比如用户取消预约、摄影师确认接单、拍摄完成各自封装成独立方法每个方法内部先查当前状态再校验当前状态是否允许迁移到目标状态不满足就抛出业务异常满足才执行 update。这样可以避免多人开发时互相覆盖状态判断逻辑。public void confirmOrder(Long orderId, Integer operatorRole) { AppointmentOrder order orderMapper.selectById(orderId); // 参数说明0待确认 1已确认 2拍摄完成 3已完成 if (order.getStatus() ! 0) { throw new BizException(当前状态不可确认); } if (operatorRole ! 2) { throw new BizException(仅摄影师可确认接单); } order.setStatus(1); orderMapper.updateById(order); }operatorRole从当前登录 token 中解析得出orderMapper.selectById查出订单后先读状态再判断角色。BizException是自定义的业务异常最终由全局异常处理器捕获返回给前端提示信息。这种写法的目的就是让任何人阅读流程代码时都能一眼看到状态跳转的约束条件不依赖隐性约定。3.3 拍摄套餐与商品订单的多表关联除了约拍主流程系统还有商品购买模块。商品表和商品分类表用分类 ID 关联商品订单表和约拍订单表分离各自维护自己的订单号、金额和状态。注意一点商品订单不会走到摄影师接单环节它的状态是待付款、已付款、已发货、已完成和约拍订单的状态语义完全不同放在同一个表里只会让查询和统计异常绕路。前台用户下单商品时product_order表要冗余一份商品名称和商品快照价格不能只存商品 ID。原因是商品信息是可变的如果商家改了价格或下架了商品历史订单查询时关联出来的数据就全错位了。类似的做法是配一张订单-商品明细表存快照演示级项目直接在订单表冗余关键字段也能接受。购物车、收藏、地址这类附属功能本质上都是围绕 user_id 做维度表。购物车表存用户 ID、商品 ID、数量、选中状态收藏表存收藏类型因为用户既可能收藏套餐也可能收藏商品用 type 字段区分比拆两张表更合适地址表则是标准的省市区联动三字段加详细地址和手机号。4. Vue 前端接口封装与页面联动实现4.1 接口请求封装和登录 token 处理前端技术栈包括 Vue、Element UI、Axios 和 Maven 管理依赖的后端接口。页面上所有请求都需要经过 axios 实例统一管理我一般会在src/utils/request.js里创建 axios 实例配置baseURL、超时时间再用请求拦截器附带 token、响应拦截器统一处理错误码。项目根目录下的main.js.bak是入口文件的备份版本用它可以对比当前main.js被改动过什么本地调试时如果入口配置改乱了直接复制这个备份文件还原即可。import axios from axios const request axios.create({ baseURL: process.env.VUE_APP_BASE_URL || /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) request.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } )baseURL用环境变量控制开发环境走 Vite 或 Vue CLI 的代理生产环境直接指向后端域名。请求拦截器把 token 塞到请求头里后端过滤器统一从 Header 取。响应拦截器里对 401 做全局跳转是因为登录过期时任何接口都可能返回未授权在每个页面单独处理会漏写。这个设计在前后端分离项目里是通用做法。4.2 路由守卫控制页面访问权限前端路由需要做到未登录用户只能访问首页、套餐列表和公告这类公共信息涉及个人中心和订单操作必须先跳到登录页。Vue Router 的beforeEach全局守卫可以统一完成这个判断每次路由跳转前检查本地是否存有 token再检查目标路由是否需要登录权限。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })to.meta.requiresAuth是在路由表定义时写在 meta 字段里的标记例如此约拍下单页面、个人中心、后台管理全部打标首页和套餐列表不设置。跳转登录页时将当前地址放入 query登录成功后用redirect参数跳回原页面用户操作路径不会被截断。后端接口也要做同样的权限校验前端守卫只是优化体验不能作为安全边界。4.3 摄影师私聊、套餐收藏与轮播图的数据交互摄影师列表中可以直接点“私聊”这个功能本质是站内信或简单聊天记录在数据表设计上建议用 user_id 和 photographer_id 做一对一会话会话表加 last_message 和 update_time 排序。实现时新建聊天记录表存消息内容和发送时间前端用定时轮询或者 WebSocket 推送新消息课程设计通常用轮询就够了。套餐收藏使用的表和商品收藏共用一张收藏表type 字段区分是套餐还是商品。轮播图数据从后台 SystemController 加载首页通过getBannerList接口拿数据也许你注意到main.js.bak里引用了原生 DOM 操作——如果后续你引入 Swiper 或 Element UI 的走马灯组件这个备份文件能让你对比原来的全局配置有没有冲突。前端的关键交互模块可以按下表拆解页面操作调用的接口模块关键参数提交拍摄预约/appointment/submitpackageId、shootTime、address摄影师接单/appointment/confirmorderId用户评价服务/comment/submitorderId、rating、content购买商品/productOrder/createproductId、quantity、addressId发布论坛帖子/forum/posttitle、content、images这个表格能帮你在接到源码后快速建立测试用例的路径图每个功能模块点一遍跟着接口文档校验参数是否完整。5. 本地跑通与源码定位的排错技巧5.1 环境准备与启动脚本说明项目里提供了run.bat、2-run.bat、3-build.bat、build.bat这几个脚本它们做的事情分别是run.bat直接启动后端 SpringBoot2-run.bat启动前端 Vue 开发服务3-build.bat是前端打包build.bat应该是后端打包或构建脚本的组合。如果双击run.bat发现窗口一闪而过多半是环境变量没配好或 Maven 依赖没下载成功建议先手动执行mvn spring-boot:run看报错信息。环境依赖的具体版本可以在pom.xml和package.json中确认重点检查 JDK 版本与 SpringBoot 版本是否匹配以及前端 Node 版本是否满足 Vite 或 Vue CLI 的要求。启动顺序是先启动后端再启动前端因为前端页面的数据全部来自后端接口单独开前端页面只能看到空壳。5.2 常见启动失败快速定位数据库连接失败排在所有启动报错的第一位。后端日志出现Access denied for user说明数据库密码没改成你自己的Unknown database说明还没创建对应名称的数据库或者在application.yml里写错了库名。解决方法是先用 Navicat 手动连接一次 MySQL再把连接参数复制到配置文件spring.datasource.url中。前端启动后接口请求 404优先检查vue.config.js里的代理配置和 axios 的baseURL是否匹配许多项目的代理只配了/api前缀而某个接口不小心写成了/admin开头导致绕过代理直接请求了不存在的后端地址。用浏览器 F12 看 Network 面板请求地址如果是http://localhost:8080/xxx而不是http://localhost:8080/api/xxx那就是 baseURL 或代理冲突了。5.3 两个快速定位业务 bug 的通用手段第一拿到源码先跑通流程再改代码。不要上来就研究登录逻辑先用管理员账号登录后台创建摄影师账号再用摄影师账号接单最后用用户账号评价把整个闭环走一遍期间用浏览器开发者工具记录每个接口的请求参数和返回结果。第二再遇到下单后状态没跳变用 IDEA 在 Service 层updateById和confirmOrder方法打上断点调试查看代码中的order.getStatus()与传入的状态值是否匹配是状态值传错还是校验逻辑把合理路径拦掉。本文还有配套的精品资源点击获取