news 2026/9/11 12:40:57

基于Spring Boot的画师约稿平台:订单状态机与权限控制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot的画师约稿平台:订单状态机与权限控制实战

简介:这份基于 Spring Boot 的画师约稿平台毕业设计项目,主要面向计算机相关专业学生,尤其适合需要完成毕业设计或课程设计的开发者。项目以画师约稿为核心场景,围绕用户、画师、作品、约稿、稿件五大主体搭建,涵盖用户注册登录、个人信息维护、作品类型分类、画师认证与等级评定、作品审核展示、约稿需求发布与匹配、稿件提交与确认交付、系统公告维护以及首页轮播图配置等完整功能模块,业务逻辑覆盖从需求发起到作品交付的全流程,能够体现较为完整的工程实践能力。资源压缩包约 23MB,以基于 Spring Boot 的 Java 后端工程代码和部署教程文档为主,代码结构清晰,模块边界明确,便于学习者对照运行和二次开发。目前该资源已有 67 人学习使用,对于想要快速上手真实业务项目、理解前后端交互并完成毕业设计答辩的读者,是一份具备较强参考价值的实战资料。

1. 画师约稿平台这道毕业设计,重点不在“画图”而在“约稿状态机”

如果你的题目里带了“画师约稿平台”,大概率课程的结课设计或毕业设计选型表里有这么一项。很多同学拿到题目第一反应是“约稿不就是发帖+聊天吗”,等真正写了才发现:发钱、交稿、验收这三个环节,每一步都有状态变化,画师约稿平台的技术难点全在订单流转上,而不是图片上传。Spring Boot 在这里承担的是接口、权限和业务规则,前端可以用 Vue 或别的模板,题目既然强调“代码+部署教程”,说明交付目标不只是能在 IDE 里跑通,还得能在服务器上稳定运行。这篇我会用一套常见做法把整个项目拆开讲:领域建模怎么做,Spring Boot 的工程怎么搭,约稿单的状态机怎么设计,最后给出可直接照用的部署步骤。

2. 用 Spring Boot 搭画师约稿平台骨架:依赖、目录与核心表

画师约稿平台的业务主体是“约稿单”,围绕它展开的用户、稿件、结算和站内信,实际上就是一个典型的订单系统加内容上传。工程搭建阶段就把实体关系理清楚,后面写接口会顺畅很多。

2.1 Maven 依赖选到什么程度:JPA 还是 MyBatis-Plus

我一般会先看团队熟悉的持久层。Spring Boot 官方推荐的 Spring Data JPA 适合实体关联多、继承关系复杂的场景;但毕业设计里,约稿单状态查询、按画师统计订单这类 SQL 通常写起来更快,MyBatis-Plus 反而更省事。依赖只加需要的,避免把整个 spring-boot-starter-webflux 或安全插件的全家桶都塞进来。以下是一个偏保守的 baseline,JDK 用 17,Spring Boot 用 2.7.x 或 3.2.x,避开版本太新的激进选择,因为 Spring Boot 3.x 下部分 starter 包名从 javax 换成 jakarta,网上很多旧教程会因此跑不通。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

核心依赖就这六个。加了 spring-boot-starter-security 之后,所有接口默认被拦截,我习惯在后续配置里放行登录、注册和约稿检索接口,其他接口统一走 JWT 认证。这里的版本号是当时验证过的组合,不追求最新,因为部署教程要面对的环境很可能还是 OpenJDK 8 或 11。

2.2 约稿领域模型:用户、画师资料、约稿单、稿件、结算

画师约稿平台最少需要五张表:用户表、画师信息表、约稿单表、稿件表、结算流水表。约稿单表是关键,它把客户、画师、价格、截至时间和当前状态绑在一个聚合里。建议约稿单 ID 用雪花算法生成,方便之后做分库或同步到消息队列,而不用依赖数据库自增。

CREATE TABLE `commission_order` ( `id` bigint NOT NULL COMMENT '约稿单ID', `order_no` varchar(32) NOT NULL COMMENT '业务单号', `client_id` bigint NOT NULL COMMENT '约稿方用户ID', `artist_id` bigint NOT NULL COMMENT '画师用户ID', `title` varchar(100) NOT NULL COMMENT '约稿需求标题', `description` text COMMENT '需求描述', `price` decimal(10,2) NOT NULL COMMENT '约稿价格', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0待付款 1进行中 2待验收 3已完成 4已取消', `deadline` datetime DEFAULT NULL COMMENT '交稿截止时间', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_client_id` (`client_id`), KEY `idx_artist_id` (`artist_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='约稿单表';

字段设计上把 client_id 和 artist_id 都落到订单表,查询“我约的稿”和“我接的稿”就不需要 join 用户表。status 用 tinyint 存储,注释里标明每个数字的意义,代码里再用枚举对应,而不是在业务逻辑里散落魔法数字。

实体类按 MyBatis-Plus 的规范写,主键用 ASSIGN_ID 让框架自动填充雪花 ID,状态字段映射成枚举后,代码里可以直接比较。这里要特别注意 Jackson 序列化枚举时默认输出名称,前端拿到的可能是 “WAIT_PAYMENT” 而不是数字 0,要么在枚举上加 @JsonValue 返回数字,要么干脆保留 Integer 类型、在 service 层做状态判断。一般我选后者,因为调试接口时数字更好理解。

3. 画师约稿平台的权限控制与约稿单状态机

创新点往往藏在这里:怎么保证约稿方不能替画师确认验收?怎么防止画师在未付款时就开始交稿?这两件事落到代码上,一是角色权限,二是状态机。用 Spring Security 加一个简单枚举就能覆盖大部分交付要求。

3.1 三种角色一张表还是三张表:用 Spring Security 实现角色与权限

画师约稿平台的用户画像天然分成:约稿方、画师、管理员。我见过一些项目把画师信息单独拆表,再跟用户表关联,这样导致一个问题:用户登录后查“我是不是画师”要额外查一次画师表,而且画师还能补充自我介绍、例图链接这些冗余字段。更省心的做法是在用户表上加一个 user_type 字段,0 表示普通用户,1 表示画师,画师资料单独存到 artist_profile 表,用 user_id 关联。角色权限用 Spring Security 的 GrantedAuthority 表达,登录成功后在 JWT 里写入角色列表。

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = resolveToken(request); if (token != null && jwtUtil.validateToken(token)) { Claims claims = jwtUtil.getClaims(token); String username = claims.get("username", String.class); String role = claims.get("role", String.class); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( username, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role)) ); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } }

参数说明:resolveToken 从请求头 Authorization 里取出 Bearer 开头的字符串,validateToken 校验签名和过期时间;role 放在 claims 里而不是每次查库,减少 Redis 或数据库压力。注意 SecurityContextHolder 是线程本地变量,异步线程池里执行任务时拿不到认证信息,如果后续要异步发送通知,要把用户 ID 作为参数显式传递,而不是依赖上下文。

3.2 从“待付款”到“已完结”:用枚举管好约稿状态流转

画师约稿平台编译期就防呆的做法,是把状态流转写进枚举:

public enum OrderStatus { WAIT_PAYMENT(0, "待付款", Arrays.asList(PAY_SUCCESS, CANCELED)), IN_PROGRESS(1, "进行中", Arrays.asList(SUBMIT_FOR_REVIEW, CANCELED, DISPUTE)), WAIT_ACCEPT(2, "待验收", Arrays.asList(COMPLETED, REJECTED, DISPUTE)), COMPLETED(3, "已完成", Collections.emptyList()), CANCELED(4, "已取消", Collections.emptyList()), DISPUTE(5, "争议中", Arrays.asList(COMPLETED, CANCELED, REFUNDED)); private final Integer code; private final String desc; private final List<OrderStatus> allowedNext; }

每个状态只保留它能跳转到的目标状态,比“在 if 里判断当前状态是否合法”更直观。审核代码的人只要看枚举,就知道业务路径。这里额外加了一个 DISPUTE 状态,因为约稿很容易因“改稿次数”或“风格不符”产生纠纷,毕业设计里加上它,答辩时能主动讲清楚状态机的可扩展性。注意把 COMPLETED 和 CANCELED 设计为终态,新需求要加“重开订单”时再扩展 allowedNext 即可,不用改动旧逻辑。

3.3 状态校验不放在 Controller 而是放到 Service

有些入门项目会把状态判断写在 Controller 里,导致同样的校验在更新订单、支付回调、人工处理三个入口各写一遍。画师约稿平台里同样的约稿单会同时被用户端、支付回调和管理后台触发,所以校验动作要下沉到 Service 层:

public void completeOrder(Long orderId, Long operatorId) { CommissionOrder order = getOrderById(orderId); if (!order.getClientId().equals(operatorId)) { throw new BusinessException("只有约稿方才能确认验收"); } OrderStatus current = OrderStatus.fromCode(order.getStatus()); if (!current.getAllowedNext().contains(OrderStatus.COMPLETED)) { throw new BusinessException("当前状态不能直接完结"); } order.setStatus(OrderStatus.COMPLETED.getCode()); updateById(order); }

这样做的好处是,约稿方确认验收、管理员强制完结、支付回调触发下一步,全部走同一个守卫逻辑。参数说明:operatorId 从当前登录用户解析出来,不能用前端传来的 userId,否则抓包改参数就能以别人的身份操作订单。注意并发场景下两个请求同时读到“待验收”,分别执行了完结操作,这里要在 UPDATE 语句里带上前状态条件,影响行数为 0 时再抛异常,避免重复触发资金结算。

4. 画师约稿平台核心接口是怎么写的:发布约稿、接单、交稿与验收

代码提示里的“代码+部署教程”通常要求能跑通一整个闭环。约稿平台的闭环是:约稿方发单、画师接单、画师交稿、约稿方验收、结算完成。这里我拆出三个最容易写歪的接口,把参数设计和幂等控制一起说明白。

4.1 发布约稿:把需求以 form 和文件一起接收

约稿需求通常要带参考图。常见的做法是图片单独走文件上传接口,返回 URL 之后再随表单提交,这样约稿单表只存 reference_url,避免大字段影响查询性能。前端如果一次性 multipart 提交,后端可以这样接收:

@PostMapping("/commission") public Result<Long> createCommission(@RequestParam("title") String title, @RequestParam("description") String description, @RequestParam("price") BigDecimal price, @RequestParam("deadline") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime deadline, @RequestParam(value = "referenceImage", required = false) MultipartFile referenceImage) { Long clientId = SecurityUtils.getCurrentUserId(); CommissionOrder order = orderService.createDraft(clientId, title, description, price, deadline); if (referenceImage != null && !referenceImage.isEmpty()) { String url = fileStorageService.store(referenceImage); order.setReferenceUrl(url); orderService.updateById(order); } return Result.ok(order.getId()); }

参数说明:deadline 用 LocalDateTime 接收,前端必须传标准格式,否则 Spring 转换直接抛 400;referenceImage 不做必填,但前端要在发单按钮上限制图片大小,否则大图直接把 Tomcat 默认的 max-post-size 塞满,导致整个接口不可用。fileStorageService 可以把文件写到本地目录,也可以接对象存储,这取决于你的部署架构。

4.2 接单与锁定:用数据库行锁和版本号防并发

热门画师的单子可能同时被多个人点击“接单”,如果用“先查状态再更新”的逻辑,两个请求都会认为订单还是待接单。应对方案是使用乐观锁版本号:

@Update("UPDATE commission_order SET status = #{newStatus}, artist_id = #{artistId}, version = version + 1 " + "WHERE id = #{orderId} AND status = #{oldStatus} AND version = #{expectVersion}") int lockOrder(@Param("orderId") Long orderId, @Param("artistId") Long artistId, @Param("oldStatus") Integer oldStatus, @Param("newStatus") Integer newStatus, @Param("expectVersion") Integer expectVersion);

调用后检查返回值,影响行数为 1 则抢单成功,为 0 则说明状态已经被别人改过,返回“手慢了”提示。参数说明:oldStatus 用状态枚举里的 IN_PROGRESS 前置码,expectVersion 从查询结果里带出来,SQL 里同时限定 status 和 version 能保证并发安全。画师约稿平台里这个操作也可以直接依赖数据库的行锁,加上SELECT ... FOR UPDATE,但对于毕业设计体量的并发,乐观锁更简洁,不需要额外处理事务超时。

4.3 交稿与验收:上传参考图、留痕、改状态

画师交稿要保留历史版本,因为约稿方可能要对比初稿和修改稿。建设稿表时,用 parent_id 字段实现父子版本,而不是覆盖同名文件。验收通过时写一条结算流水并更新订单状态:

@Transactional(rollbackFor = Exception.class) public void acceptAndSettle(Long orderId, Long clientId, Integer artworkId) { CommissionOrder order = getById(orderId); if (!order.getClientId().equals(clientId) || !OrderStatus.WAIT_ACCEPT.match(order.getStatus())) { throw new BusinessException("订单不在可验收状态"); } artworkService.updateStatus(artworkId, ArtworkStatus.ACCEPTED); order.setStatus(OrderStatus.COMPLETED.getCode()); orderService.updateById(order); settlementService.createSettlement(order.getId(), order.getPrice(), order.getArtistId()); }

注意事务里先改稿件状态再改订单状态,最后写结算,任何一个步骤失败都会回滚,避免出现“稿件已验收但画师没收到钱”的脏数据。参数说明:settlementService.createSettlement 里生成的流水号要带订单号前缀,方便对账排查。如果你在答辩时想突出业务完整性,可以在这个方法里加一个“分成交付平台手续费”的计算逻辑,把画师所得和平台所得拆成两条流水,这个细节比堆接口更显真实经验。

5. 画师约稿平台打包部署:从本地 IDE 到服务器

“代码+部署教程”这个标题已经暴露了需求:代码能跑只完成一半,另一半是部署。这一章我按 Linux 服务器加 Docker Compose 的方式展开,这套方案既适合毕设演示,也适合后续放进简历写成“完成项目的容器化部署”。

5.1 打包前先检查:配置文件按环境拆分

本地跑通不代表服务器能跑,很多报错都出在配置上。我习惯把配置拆成 application.yml、application-dev.yml、application-prod.yml,主配置文件只放公共部分。生产环境配置里数据库和 Redis 地址用环境变量引用,敏感字段不写明文:

spring: datasource: url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:art_commission}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: ${MYSQL_USER} password: ${MYSQL_PASSWORD} redis: host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379}

注意 serverTimezone 一定要显式指定,否则数据库时区和你本机时区不一致时,日期字段会差 8 小时。打包命令使用跳过测试的方式,避免本地测试代码在构建机上执行失败:

mvn clean package -DskipTests

打包完成后 target 目录会生成 jar 包。这里提醒一点:如果你的 pom 里配了<finalName>,jar 包名称会固定;如果是默认的 artifactId-version.jar,部署脚本的路径要跟着版本号调整,我一般会在 pom 里固定 finalName 为art-commission,省去改脚本的麻烦。

5.2 用 Docker Compose 把 MySQL、Redis 和应用一起拉起来

服务器上装 Docker 之后,直接暴露应用端口,数据库只走容器内部网络,不映射端口到宿主机,这样别人无法从外部连你的数据库。以下是我的 docker-compose.yml 精简版:

version: "3.8" services: mysql: image: mysql:8.0 container_name: art-mysql environment: MYSQL_ROOT_PASSWORD: rootPass MYSQL_DATABASE: art_commission volumes: - ./mysql/data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d networks: - art-net redis: image: redis:7-alpine container_name: art-redis networks: - art-net app: build: . container_name: art-app environment: MYSQL_HOST: mysql MYSQL_PORT: 3306 MYSQL_DATABASE: art_commission MYSQL_USER: root MYSQL_PASSWORD: rootPass REDIS_HOST: redis ports: - "8080:8080" depends_on: - mysql - redis networks: - art-net networks: art-net:

环境变量里 MYSQL_HOST 直接写容器名 mysql,Docker 的内置 DNS 会自动解析到对应容器 IP。init 目录里的 SQL 文件会在首次启动时自动执行,方便把建表脚本和测试数据一次性灌进去。部署时要注意 MySQL 容器首次启动需要初始化数据,如果应用容器先启动并执行初始化 SQL,会连不上数据库报 Connection refused,所以我在 compose 里只用了 depends_on,更严谨的写法是加 healthcheck,等 MySQL 真正 ready 再启动 app。

5.3 部署完成后怎么验证:日志、接口、压测

启动完成后的第一步不是直接测业务,而是看启动日志。docker compose logs -f app会输出 Spring Boot 的启动过程,看到 “Started Application in xx seconds” 才算真正起来。之后用 curl 测试接口连通性:

curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"123456"}'

返回结果里带上 JWT token 后,再拿着 token 去请求约稿单列表接口,验证拦截器是否生效。如果登录接口返回 401 或 403,优先检查 Spring Security 的放行配置,而不是看业务代码。压测推荐用 wrk 或 locust 这类轻量工具,在服务器本机跑wrk -t4 -c50 -d30s http://localhost:8080/api/commission/page,观察吞吐量和错误率,如果错误率偏高,先看 MySQL 慢查询日志,再看应用线程数瓶颈。

6. 让画师约稿平台更像“真实生产项目”的三个加固点

部署跑通之后,大多数毕业设计就收尾了。如果你想让答辩老师觉得“这人不只是复制粘贴”,或者想把项目写进简历,我建议花半天时间做三件事。

第一件,给文件上传加磁盘配额和类型白名单。很多画师约稿平台的参考图、成稿都是图片,如果你直接用 MultipartFile 接收文件并原样存盘,等于让用户随意传 executable 文件到服务器,部署后极易被拿来做跳板。我一般会在 FileStorageService 里用文件头判断真实类型,而不是只看扩展名。判断逻辑很短,读取文件前几个字节比对 JPEG、PNG 的 magic number,不符合的直接抛异常。给每个订单可积累的附件大小设上限,超过 200MB 就不再接收,这些限制写在配置里,方便后续调整。

第二件,把 JWT 的过期时间和 Redis 的会话状态连起来用。现在很多项目只发 JWT,不存状态,导致“退出登录”功能形同虚设,因为 token 在过期前始终有效。我建议登录时把 token 的 jti 存进 Redis,过期时间跟 JWT 对齐,加一个拦截器检查 Redis 里是否存在该 jti,退出时删掉对应 key 实现强制失效。这个改动能在答辩时讲出一个完整的“会话管理”设计,而不只是“前端把 token 清掉”。

第三件,把一个接口的响应包装改成统一的返回结构。你可以在项目里维护一个 Result 类,包含 code、message、data 三个字段。所有接口返回这个结构,前端只需要处理这一个 JSON 格式,配合一个全局异常处理器,把业务异常和系统异常映射成不同 code。这样面试聊到“如何配合前端联调”时,你能直接说出这套约定,比零散返回 Map 有说服力。做完这三个点,整个画师约稿平台从代码结构到部署交付就都符合“代码+部署教程”的标题预设了。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:40:07

浮点数打印避坑指南:从IEEE 754到格式化输出

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:38:51

5 分钟给 Milvus 装上国内镜像加速:DaoCloud 方案实战

5 分钟给 Milvus 装上国内镜像加速&#xff1a;DaoCloud 方案实战 【免费下载链接】public-image-mirror 很多镜像都在国外。比如 gcr 。国内下载很慢&#xff0c;需要加速。致力于提供连接全世界的稳定可靠安全的容器镜像服务。 项目地址: https://gitcode.com/GitHub_Trend…

作者头像 李华
网站建设 2026/9/11 12:38:33

从零搭建个人Agent应用:WorkBuddy开放平台实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:38:06

从原理到实战:格式化字符串漏洞利用与GOT表覆写全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:37:23

camofox-browser:基于Firefox的浏览器指纹伪装与反追踪实战

作为一个常年折腾浏览器、把隐私保护当成日常习惯的技术爱好者&#xff0c;我最近在自己的主力机上深度体验了一个叫camofox-browser的项目。这名字起得挺直白&#xff1a;camo 是迷彩&#xff0c;fox 是火狐&#xff0c;合起来就是用火狐的底子做一套"迷彩伪装"&…

作者头像 李华