ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

基于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 或安全插件的全家桶都塞进来。以下是一个偏保守的 baselineJDK 用 17Spring Boot 用 2.7.x 或 3.2.x避开版本太新的激进选择因为 Spring Boot 3.x 下部分 starter 包名从 javax 换成 jakarta网上很多旧教程会因此跑不通。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 ListOrderStatus 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 ResultLong 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 转换直接抛 400referenceImage 不做必填但前端要在发单按钮上限制图片大小否则大图直接把 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}?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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 里配了finalNamejar 包名称会固定如果是默认的 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 直接写容器名 mysqlDocker 的内置 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 有说服力。做完这三个点整个画师约稿平台从代码结构到部署交付就都符合“代码部署教程”的标题预设了。本文还有配套的精品资源点击获取
返回列表