恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot+Vue+MySQL画师约稿平台:状态机与幂等设计实战
首页
资讯中心
/
SpringBoot+Vue+MySQL画师约稿平台:状态机与幂等设计实战
SpringBoot+Vue+MySQL画师约稿平台:状态机与幂等设计实战
发布时间:2026/10/7 13:49:58
简介这是一套面向计算机专业毕业设计、课程设计场景的完整画师约稿平台项目基于Java、SpringBoot、Vue与MySQL技术栈实现解决了画师展示作品、客户发布约稿需求以及订单管理全流程问题。项目为导师指导并通过的高分毕设前后端代码完整包含数据库脚本、Maven配置及Navicat等辅助工具说明下载后无需修改即可运行。压缩包共619个文件大小22.63MB以Java后端源码、Vue前端组件、SVG图标与CSS样式为主并附有SQL脚本、项目配置文件、启动脚本及说明文档目录结构清晰便于直接导入IDEA开发调试。系统功能完善、界面简洁支持作品展示、约稿发布、订单跟踪等核心环节既适合学习前后端分离开发与数据库设计也可作为可复用的业务平台继续迭代。目前已有80人学习下载对于希望快速搭建完整Web项目或完成高质量毕业设计的同学是一份性价比很高的参考资料。1. 画师约稿平台一个 Java 全栈毕设到底能干什么「你这约稿平台跟一个普通购物网站到底有什么区别」这是我当时准备答辩时室友随口问的一句却直接点醒了整个项目的核心。画师约稿和买现货完全不是一回事下单时要预付定金创作周期按天计算交付物是数字文件而不是物流包裹中途还要分阶段确认草稿。这个基于 java springboot vue mysql 的画师约稿平台源码方案恰好把这条「约稿链路」完整做成了可演示的前后端分离项目——画师发布档位、甲方发起约稿、模拟支付定金、画师接单、创作交付、甲方验收确认全程由一张约稿单的状态机驱动。源码自带数据库初始化脚本导入即可运行适合拿来做毕设底子也适合想快速搞懂 SpringBoot Vue MySQL 三者如何协作的开发者。2. 拆开源码包项目骨架、数据库落点与选型理由2.1 后端分包与前端目录先看懂结构再动手拿到 zip 解压后结构和一个正规的前后端分离仓库很像SpringBoot 工程单独一个目录Vue 工程单独一个目录sql 脚本和一个说明文档放在外面。我先从后端讲起。controller 层只干两件事——接收参数、包装返回值service 层写真正的业务规则比如约稿单能不能接、状态能不能往下推mapper 层基于 MyBatis-Plus 做数据访问entity 目录下是对应数据库表的实体类。common 包里放统一返回体、JWT 工具类、登录拦截器改一处就能全局生效。后端 controller 的典型写法是这样的RestController RequestMapping(/api/order) public class CommissionOrderController { Autowired private CommissionOrderService orderService; /** * 画师接单 * param orderId 约稿单ID * param currentUser 当前登录用户由拦截器注入 */ PostMapping(/accept/{orderId}) public Result accept(PathVariable Long orderId, RequestAttribute(currentUser) User currentUser) { // 核心校验都放在 service 里controller 保持轻薄 orderService.acceptOrder(orderId, currentUser.getId()); return Result.success(); } }这段代码一眼能看懂但有几个细节值得注意RequestAttribute(currentUser)是登录拦截器里预处理好的当前用户对象后续业务方法里不需要再查一遍用户表Result.success()是统一返回体格式一般是{ code: 200, message: ok, data: ... }前端 axios 拦截器统一判断 code不需要在每个页面里处理错误分支。前端这边src/views放页面组件比如画师工作台、甲方下单页、订单详情页src/api统一封装 axios 请求每个接口一个函数src/router里维护路由表登录拦截用路由守卫配合 token 判断。注意前端目录里没有把环境地址写死而是走VUE_APP_BASE_URL这个环境变量后面启动章节我会专门说这个配置。2.2 数据库设计用户、作品、约稿单三张核心表约稿平台的数据模型简单说就是围绕三张表展开用户表、作品表、约稿订单表。用户表sys_user通过role字段区分三种身份管理员、画师、甲方。作品表artwork挂在画师名下是画师展示自己风格用的画廊。commission_order是整张订单流转的核心表字段覆盖了业务的关键信息业务单号order_no、甲方requester_id、画师artist_id、关联作品artwork_id、总金额total_amount、定金deposit_amount、当前状态status、约定完成时间deadline_at以及最终交付物地址deliver_url。约稿订单表的部分建表 SQLCREATE TABLE commission_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务单号唯一索引, requester_id bigint(20) NOT NULL COMMENT 甲方用户ID, artist_id bigint(20) NOT NULL COMMENT 画师用户ID, artwork_id bigint(20) DEFAULT NULL COMMENT 关联作品ID可为空, total_amount decimal(10,2) NOT NULL COMMENT 约稿总价, deposit_amount decimal(10,2) NOT NULL COMMENT 定金额度, status varchar(20) NOT NULL DEFAULT CREATED COMMENT 状态: CREATED/PENDING_ACCEPT/ACCEPTED/REVIEW/COMPLETED/CANCELLED, deadline_at datetime DEFAULT NULL COMMENT 约定完成时间, deliver_url varchar(255) DEFAULT NULL COMMENT 交付文件地址, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT约稿订单表;这里我特意选了varchar而不是tinyint存状态理由是让代码里直接可读答辩时被问到「这笔单现在什么状态」可以直接对着字段念不用再去翻字典表。uk_order_no唯一索引很重要它是后续保证支付回调幂等的一道底牌第四章会展开讲。2.3 为什么这套技术栈适合做约稿平台SpringBoot 做这种带状态流转的业务优势是事务和声明式管理太方便了。一个Transactional注解接单、改状态、记操作日志就绑在同一个事务里中途报错整体回滚不用手写一堆事务模板。Vue 则把画师端、甲方端、管理后台拆成独立视图各自维护自己的页面状态组件复用度高。MySQL 的关系模型压住订单这种多维度查询很稳按画师查、按甲方查、按状态查都好写 SQL不需要引入额外的中间件对毕设来说是最省心的组合。3. 把平台跑起来环境搭配、数据库初始化与双端启动3.1 环境版本搭配先说结论再讲理由这种 SpringBoot 2.x Vue 2/3 的毕设项目对 JDK 版本极其敏感。我的建议是 JDK 1.8 Maven 3.6 MySQL 5.7或 8.0 Node 14/16这套组合踩坑最少网上资料也最全。工具推荐版本为什么是这个版本JDK1.8与 SpringBoot 2.x 完全兼容Lombok 无兼容问题Maven3.6.3稳定拉依赖速度可接受MySQL5.7 / 8.05.7 最稳8.0 能用但要注意认证插件Node14 / 16Vue CLI 项目对高版本 Node 有兼容问题node-sass 尤其挑版本后端的application.yml中数据库连接配置是关键入口我第一次搭这个项目时就是在这里翻的车spring: datasource: url: jdbc:mysql://localhost:3306/artist_commission ?useUnicodetrue characterEncodingutf8 useSSLfalse serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: map-underscore-to-camel-case: trueuseSSLfalse关掉 SSL 警告serverTimezoneAsia/Shanghai防止时间差八小时的问题map-underscore-to-camel-case打开后数据库里的create_time字段能自动映射到实体的createTime属性不用每个字段都加TableField注解。3.2 初始化数据库一条 source 命令和三处编码细节数据库导入本身不复杂但「看起来不复杂的事最容易翻车」。我的操作习惯是先建库再导入避免 sql 脚本里如果有CREATE DATABASE语句和现有库冲突。mysql -u root -p CREATE DATABASE IF NOT EXISTS artist_commission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE artist_commission; SET NAMES utf8mb4; SOURCE /your/path/artist_commission.sql;三条建议都是我踩过的第一sql 文件的路径不要包含中文和空格MySQL 终端对这类路径解析很容易抽风第二导入完成后执行SHOW TABLES;确认表数量重点看commission_order这种核心表有没有完整创建出来第三如果 sql 脚本开头包含DROP TABLE IF EXISTS确认脚本里没有漏掉外键关系导致的数据不一致约稿单表引用了用户表导入顺序不对会导致外键报错。3.3 启动后端与前端两套进程、一个代理后端启动最简单的方式是在 IDEA 里直接跑主类的 main 方法或者用命令行 Mavencd back-end mvn spring-boot:run首次执行会拉很多依赖建议在settings.xml里配阿里云镜像否则等十分钟还没拉完特别消磨耐心。后端启动成功的标志是控制台出现 SpringBoot 的 banner并且监听 8080 端口。前端在另一个终端窗口启动cd front-end npm install npm run serve这里的关键配置是vue.config.js里的 devServer 代理。前端开发服务器跑在 8081 端口所有/api开头的请求要代理到后端 8080module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };配置好之后前端页面里的 axios 请求直接写/api/order/list浏览器访问 8081 端口时Vue 的 devServer 会把这请求转发给 8080 后端就不会有跨域问题。验证方法是先curl http://localhost:8080/api/order/list看看能不能拿到 JSON再开浏览器访问前端页面登录。4. 核心业务链路约稿单状态机与支付回调幂等4.1 约稿单状态机六种状态、一次完整流转整套业务的核心是一张约稿单从创建到完成的状态流转。状态定义如下状态含义谁触发前置条件CREATED已创建待付定金甲方发起约稿无PENDING_ACCEPT定金已付待画师接单支付回调成功状态为 CREATEDACCEPTED画师已接单创作中画师接单状态为 PENDING_ACCEPTREVIEW待甲方验收画师上传交付物状态为 ACCEPTEDCOMPLETED甲方确认订单完成甲方验收状态为 REVIEWCANCELLED已取消甲方/画师取消状态为 CREATED 或 PENDING_ACCEPT状态更新不是简单的 update业务上要防住「旧状态覆盖新状态」的并发问题。画师接单的核心 service 代码如下Transactional(rollbackFor Exception.class) public void acceptOrder(Long orderId, Long artistId) { // 查询时用悲观锁? 这里用乐观锁状态条件更新 CommissionOrder order getById(orderId); if (order null) { throw new BizException(订单不存在); } // 校验画师身份与订单归属 if (!order.getArtistId().equals(artistId)) { throw new BizException(无权限操作该订单); } // 状态机校验: 只有 PENDING_ACCEPT 才能接单 if (!PENDING_ACCEPT.equals(order.getStatus())) { throw new BizException(当前订单状态不可接单); } // 条件更新: where status PENDING_ACCEPT, 防止重复接单 boolean updated lambdaUpdate() .eq(CommissionOrder::getId, orderId) .eq(CommissionOrder::getStatus, PENDING_ACCEPT) .set(CommissionOrder::getStatus, ACCEPTED) .update(); if (!updated) { throw new BizException(订单状态已变更请刷新后重试); } }核心手段有两个。一是Transactional保证状态更新和后续操作整体提交或回滚二是条件更新把「修改状态」和「校验旧状态」合并成一条 update 语句数据库层面天然挡住并发场景下两个画师同时接同一单的问题。如果updated为 false说明订单已经不处于待接单状态直接抛异常让前端刷新。4.2 支付环节模拟回调、唯一索引防重复毕设项目一般不会真的接入微信支付或支付宝常见做法是做一个模拟支付页面点击「确认支付」后调用支付回调接口。这里有个关键点支付回调天然可能重复触发网络超时重试、用户反复点击都会让同一个支付单被回调多次。如果不做幂等处理订单状态会被推成脏数据。设计思路是支付消息表payment_message记录每一次回调。这张表里pay_no建唯一索引每次收到回调先尝试插入Transactional public void receivePayCallback(String payNo, Long orderId, BigDecimal amount) { // 唯一索引保证同一支付单只插入一次 PaymentMessage msg new PaymentMessage(); msg.setPayNo(payNo); msg.setOrderId(orderId); msg.setAmount(amount); try { paymentMapper.insert(msg); } catch (DuplicateKeyException e) { // 已处理过, 直接返回, 防止状态被二次推进 return; } // 首次处理: 推进订单状态 lambdaUpdate() .eq(CommissionOrder::getId, orderId) .eq(CommissionOrder::getStatus, CREATED) .set(CommissionOrder::getStatus, PENDING_ACCEPT) .update(); }pay_no的唯一索引就是幂等的底牌重复回调时insert会触发主键冲突异常catch 住直接返回状态不会被二次推进。这里把「幂等判断」和「业务执行」放在同一个事务里即使后面状态更新失败支付记录也会一起回滚不会出现「钱付了单子还是待支付」的奇怪状态。4.3 前端状态渲染一种状态一个标签一种操作前端订单列表页状态字段本身只是字符串直接展示「PENDING_ACCEPT」没人看得懂。常见做法是维护一个状态映射用 el-tag 渲染不同颜色的标签并按状态控制按钮显示el-tag :typestatusMap[order.status].type {{ statusMap[order.status].label }} /el-tag el-button v-iforder.status PENDING_ACCEPT currentUser.role ARTIST clickacceptOrder(order.id) 接单 /el-buttonconst statusMap { CREATED: { label: 待付定金, type: warning }, PENDING_ACCEPT: { label: 待接单, type: primary }, ACCEPTED: { label: 创作中, type: }, REVIEW: { label: 待验收, type: warning }, COMPLETED: { label: 已完成, type: success }, CANCELLED: { label: 已取消, type: info } };业务规则用两个维度控制状态和角色。状态是「这笔单子现在能不能做这个动作」角色是「你有权做这个动作吗」两个条件都得满足才渲染按钮这样后端即使漏了校验前端也不会把按钮露给不该看的人。5. 避坑排查环境、依赖、跨域与数据约束的实战现场5.1 MySQL 8.0 认证插件导致后端连不上现象后端启动时日志报Access denied for user rootlocalhost或者直接出现Public Key Retrieval is not allowed。原因MySQL 8.0 默认认证插件是caching_sha2_password老版本的 JDBC 驱动不认识这个插件或者连接串里没有允许获取公钥。解决两种办法任选其一。最省事的是在连接串上加参数url: jdbc:mysql://localhost:3306/artist_commission?allowPublicKeyRetrievaltrueuseSSLfalse如果还不行就把账号认证方式改回兼容模式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;5.2 前端页面能开后接口全报跨域现象浏览器控制台出现CORS policy: No Access-Control-Allow-Origin页面白屏登录按钮点了没反应。原因我在 3.3 里讲的 devServer 代理没生效。最常见的是vue.config.js写错位置或者代理配置的路径前缀和 axios 请求的路径对不上。解决先确认 axios 请求是不是以/api开头再看vue.config.js里的 proxy 是否挂在devServer下。改完配置必须重启npm run servedevServer 的配置不会热更新。后端不要加一堆CrossOrigin注解去救火那只是治标根本解法是让浏览器同源。5.3 npm install 卡住不动的三类原因现象npm install进度条长时间不动或者报node-sass相关的ERR!红字。原因默认 registry 下载慢node-sass 需要本地编译Node 版本和它要求的版本对不上。解决先用镜像源再处理 sassnpm config set registry https://registry.npmmirror.com npm install如果是 node-sass 报错把package.json里的 node-sass 换成 sass或者升级到与当前 Node 匹配的版本。最稳妥的是直接用 Node 14 配 sassdart-sass基本没有编译兼容的烦恼。5.4 Lombok 注解不生效编译报「找不到 getter/setter」现象IDE 里 entity 类上的Data注解标红编译报错说找不到getStatus()之类的方法。原因IDE 没有启用注解处理annotation processing或者 Lombok 版本和 JDK 版本不兼容。JDK 9 对 Lombok 特别挑剔JDK 1.8 Lombok 1.18.x 是稳的组合。解决IDEA 里打开Settings → Build → Compiler → Annotation Processors勾上Enable annotation processing然后重启项目。如果还不行检查 pom.xml 里的 Lombok 版本统一用 1.18.20 以上。5.5 删除用户时外键约束拦路现象管理员在后台删除一个画师账号报错Cannot delete or update a parent row: a foreign key constraint fails。原因sys_user被commission_order引用了项目约稿单里还挂着这个画师的历史记录。关系型数据库的外键保护不允许物理删除。解决不要在界面上提供物理删除用户的功能改用逻辑删除。sys_user表加一个deleted字段查询时全局过滤器自动带上deleted 0条件约稿单留下的历史记录仍然能查到画师信息数据完整性和可追溯性都不受影响。6. 进阶用法一条验收链路、一个角色菜单、一次部署6.1 拿到项目后的第一件事完整走一遍验收链路拿到这套源码后建议别急着改代码先按下面的流程走一遍确认所有功能是通的。这一步能帮你发现环境差异导致的问题也让你对整套业务有体感答辩演示时不会被问住。步骤操作预期结果1注册甲方账号 A 和画师账号 B两个账号都能正常登录2画师 B 发布一条约稿档位甲方 A 在浏览页能看到3甲方 A 发起约稿并模拟支付订单状态变为待接单4画师 B 接单状态变为创作中5画师 B 上传交付物填交付链接状态变为待验收6甲方 A 确认验收状态变为已完成7甲方 A 发起一个新约稿然后取消状态变为已取消我自己第一次做这个项目验收时翻了两个车一是没有先清理数据库里的测试数据列表页全是状态错乱的脏记录二是没验证取消流程导致演示时中间状态卡住。从那以后我每次拿到新环境第一步永远是重置数据库并删掉测试账号再按这张表的顺序逐条走查确认无误后才开始碰代码。希望帮到你。本文还有配套的精品资源点击获取