ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot2+Vue3画师约稿平台:订单状态机与钱包流水全解析

SpringBoot2+Vue3画师约稿平台:订单状态机与钱包流水全解析 我最早接到画师约稿平台这类需求时第一反应是又一个作品展示站。但真正把需求捋清楚后才发现作品展示只是最外面那层皮真正麻烦的是订单状态机、交付确认、资金流水这些看不见的东西。这篇文章围绕的是一套完整的 Java Web 项目——画师约稿平台系统源码技术栈是SpringBoot2 Vue3 MyBatis-Plus MySQL8.0带完整文档。我结合自己实际做过类似委托创作类项目的经验把这类平台从业务分析、数据库设计、核心流程到落地部署、踩坑复盘一次性讲透。不管是拿来做毕设、课设还是想接手真实约稿平台开发这篇都可以当参考手册用。1. 约稿平台真正要解决的问题是什么1.1 约稿场景和普通商城交易的差异画师约稿不是标准商品交易。买家买的不是一个现成的物品而是画师按需求定制创作的一个过程。这个过程天然有几个特点周期长、主观性强、交付物是数字文件、中间存在多次修改和确认。所以约稿交易里最核心的信任问题有三个买家怕什么怕画师收钱后拖稿、跑路、画出来的东西货不对板。画师怕什么怕白嫖。草稿发过去了买家拿去用然后以我不满意为由拒绝付尾款。平台方怕什么怕双方绕过平台私下交易怕订单状态难以取证仲裁没有依据。传统做法是微信或者 QQ 聊需求用 Excel 排单定金靠转账记录。一旦发生纠纷聊天记录找不齐、交易凭证对不上平台根本没有办法仲裁。所以这类系统真正的核心不是展示作品而是把委托创作流程数字化需求怎么发布、画师怎么接单、定金怎么锁定、过程怎么留痕、交付怎么确认、尾款怎么结算、纠纷怎么介入。这些才是平台存在的价值。1.2 从需求清单反推功能边界基于上面的核心问题我们把功能模块拆出来。一个基础可用的画师约稿平台需要这些模块模块解决的问题用户注册登录区分约稿方和画师两种身份画师信息维护画风标签、个人简介、擅长领域、是否接单作品画廊展示历史作品给买家做风格参考约稿需求发布买家填写预算、尺寸、用途、风格要求、参考图接单与报价画师查看需求池报价留言或直接接单订单管理整个委托流程的状态流转交付与验收画师上传稿件买家确认或退回修改钱包与流水虚拟账户余额定金、尾款、退款流转有记录消息通知接单、交稿、验收、催稿等事件通知申诉与仲裁出现纠纷时提交平台处理订单冻结这个功能清单看起来简单但如果把每一步都做出可用的状态流转和前后端交互工作量并不小。后面我会按这套边界逐个展开。2. 技术栈四件套的选型逻辑和组合红利2.1 后端为什么是 SpringBoot2 而不是别的这个项目用SpringBoot2准确说一般是 2.7.x 系列。原因很实际SpringBoot2 的技术生态已经非常成熟网上资料多、踩坑案例全配套的 Spring Security、Redis 客户端、各种 starter 都稳定。而且 2.7 这个版本对 JDK8 的用户最友好大多数高校和企业服务器现在还跑在 JDK8你按 JDK8 SpringBoot2 写出来的东西拿过去不需要改环境。对比 SpringBoot33.x 强制要求 JDK17虽然说新特性不少但对很多部署环境来说升级成本高于收益。选 SpringBoot2 不是因为它最新而是因为它最不容易出问题。另外项目还会用到 Spring MVC、Spring Security 或者简单的拦截器。如果你只是做毕设或者小规模商用可以用拦截器 JWT 的方式处理登录态不需要把 Spring Security 的过滤器链玩到多深够用就行。2.2 Vue3 与 Vue2 之间为什么直接选 Vue3Vue3现在已经是绝对主流。Vue2 虽然还有大量老项目在跑但已经停止维护新项目再选 Vue2 等于一开始就背着一个技术债。Vue3 带来的核心改变是 Composition API。以前 Vue2 用 Options API一个组件里 data、methods、computed 分开写逻辑一多容易散。Vue3 用 setup 组合式函数把某个业务的所有相关状态和操作聚在一起。比如订单操作这个逻辑可以抽成一个 useOrder()在多个页面里复用体验比 mixin 好太多。配合构建工具 Vite开发冷启动快、热更新快尤其在前端工程规模变大之后这种效率差距很明显。UI 组件库用 Element Plus表单、表格、弹窗、上传组件这些开箱即用非常契合后台管理系统和业务平台类项目。2.3 MyBatis-Plus 比原生 MyBatis 强在哪原生 MyBatis 有一个很大的痛点你要花大量时间写单表 CRUD。就一个 user 表的增删改查就得写 mapper 接口、mapper XML、resultMap大概几十行样板代码。MyBatis-Plus把这个问题解决了。它内置了 BaseMapper提供 insert、deleteById、updateById、selectById、selectList 这些通用方法单表操作基本零 SQL。再加上条件构造器复杂查询也不用写 XMLLambdaQueryWrapperCommissionOrder wrapper new LambdaQueryWrapper(); wrapper.eq(CommissionOrder::getCreatorId, userId) .eq(CommissionOrder::getStatus, OrderStatusEnum.WAIT_PAY.getCode()) .orderByDesc(CommissionOrder::getCreateTime); ListCommissionOrder list orderMapper.selectList(wrapper);更关键的是它提供了一组实用插件乐观锁插件、逻辑删除、自动填充、分页插件。这套组合正好踩在业务系统的通用需求上后面我会详细说。2.4 MySQL8.0 给项目带来的实际便利MySQL8.0相比 5.7 有几个点在这类项目里很实用默认字符集是 utf8mb4emoji 和生僻字都能直接存约稿平台的稿件描述里经常出现特殊符号这个很重要。支持窗口函数比如统计画师接单榜、作品排行用 ROW_NUMBER() 一行 SQL 就能搞定。支持 CTE公共表表达式复杂查询的写法更清晰。JSON 字段类型可以把画师标签、约稿需求里的参考图 URL 列表直接存成 JSON不用拆一堆关联表。我在做数据库设计时会刻意用掉这些特性让 SQL 更简洁。后面表结构里会体现。3. 工程骨架与数据库设计先把地基打牢3.1 前后端分离的工程结构项目是前后端分离结构通常会分成 commission-server 和 commission-web 两个工程目录。后端工程按常见的 controller / service / mapper / entity / dto 分层com.example.commission ├── controller # 接收请求返回统一 Result ├── service # 业务逻辑订单状态流转都在这一层 ├── mapper # MyBatis-Plus 的 BaseMapper 接口 ├── entity # 数据库表对应的实体类 ├── dto # 请求/响应对象避免直接暴露实体 ├── config # 跨域、分页插件、自动填充等配置 ├── enums # 订单状态、审核状态等枚举 ├── utils # JWT、文件存储等工具类 └── common # 统一返回体、全局异常处理前端按 Vue3 常规方式组织src ├── api # axios 接口封装统一管理请求地址 ├── views # 页面级组件 ├── components # 通用组件比如上传、订单卡片 ├── store # Pinia 状态管理 ├── router # 路由配置和守卫 ├── utils # 请求实例、token 管理等 └── styles # 全局样式分层原则很简单Controller 只做参数接收和结果封装业务逻辑全部下沉到 ServiceMapper 只做数据访问。不要出现 Controller 里直接查库的写法不然项目越改越乱。3.2 核心表结构设计画师约稿平台的核心表不会太多但每一张都承担具体职责。我把最关键的几张贴出来user用户表字段类型说明idBIGINT主键usernameVARCHAR(50)登录账号passwordVARCHAR(100)BCrypt 加密后的密码nicknameVARCHAR(50)昵称roleTINYINT1-约稿方2-画师画师可同时是约稿方avatarVARCHAR(255)头像地址statusTINYINT0-禁用1-正常balanceDECIMAL(10,2)钱包余额artist_profile画师信息表字段类型说明idBIGINT主键user_idBIGINT关联用户表bioTEXT个人简介tagsJSON绘画风格标签画师自选shop_statusTINYINT是否接单中audit_statusTINYINT画师认证状态commission_order约稿订单表这里是最核心的一张表字段类型说明idBIGINT主键order_noVARCHAR(32)订单号业务上展示用creator_idBIGINT约稿方用户IDartist_idBIGINT画师用户IDtitleVARCHAR(100)约稿标题descriptionTEXT需求描述reference_imgsJSON参考图URL列表style_tagVARCHAR(50)需求风格sizeVARCHAR(50)画布尺寸budgetDECIMAL(10,2)预算/成交价depositDECIMAL(10,2)定金金额statusTINYINT订单状态见下方状态机modify_countINT已修改次数modify_limitINT最大修改次数默认3deadlineDATETIME约定交付日期create_timeDATETIME创建时间update_timeDATETIME更新时间versionINT乐观锁版本号order_delivery稿件交付表字段类型说明idBIGINT主键order_idBIGINT订单IDdeliver_typeTINYINT1-草稿2-精修图3-最终稿file_urlVARCHAR(255)稿件文件地址noteVARCHAR(255)交付说明create_timeDATETIME交付时间wallet_flow钱包流水表字段类型说明idBIGINT主键user_idBIGINT用户IDorder_idBIGINT关联订单可为空amountDECIMAL(10,2)变动金额正负号表示增减balance_afterDECIMAL(10,2)变动后余额flow_typeTINYINT1-充值2-支付定金3-收入4-退款create_timeDATETIME流水时间3.3 订单状态字段的设计思路订单状态我不会只用一个数字字段还会配一张 order_log 状态变更日志表。每变一次状态都记录从哪个状态变到哪个状态、操作人是谁、操作时间、原因说明。这样做的价值在纠纷场景里才体现得出来当买家投诉画师没有按时交稿仲裁员查订单日志就能看到完整轨迹而不是两个人各执一词。订单状态的设计也不是越细越好太细了用户操作成本高太粗了纠纷说不清。我倾向于这组状态0 待接单 1 待付定金 2 草稿绘制中 3 草稿待确认 4 精修中 5 待交付确认 6 已完成 7 已取消 8 退款中止中间有修改需求时从精修中退回草稿绘制中再走一遍日志里会留下修改原因方便统计画师的返工情况。4. 约稿订单的完整生命周期状态机是系统的灵魂4.1 从发布需求到交易完结的完整链条我按实际业务顺序走一遍这个平台最核心的交互流程第一步买家发布约稿需求填标题、需求描述、参考图、尺寸、预算、风格标签。此时还没有生成订单只是进入需求池所有画师可见。第二步画师接单/报价画师看到需求池里的订单可以点击接单按预算生成订单。如果画师觉得预算不对可以留言报价买家再决定是否接受。第三步买家支付定金订单被画师接单后状态变为待付定金。买家需要从虚拟钱包里支付约定定金通常是总价的一半画师才开始动笔。这一步在系统里就是一次钱包扣款 写入流水 订单状态变更事务性很强。第四步画师交付草稿画师上传草稿图订单状态变成草稿待确认。买家查看草稿选择没问题继续或者退回修改。退回修改时系统扣一次 modify_count。第五步交付最终稿并结算尾款画师交付最终稿后买家确认收货剩余尾款从买家钱包转入画师钱包订单状态变成已完成。如果买家不点确认系统会在 N 天后自动确认。这个流程把约稿场景里的先铺一半、验货后付尾款模式跑通了。最重要的是每一步操作都必须有状态校验不能出现买家已支付定金、画师草稿没交付、订单却显示已完成这种诡异情况。4.2 状态机的落地实现状态机听起来高深实际落地可以很简单。我的做法是在 service 层写一个统一的流转方法每次状态变更都走同一个入口public void changeOrderStatus(CommissionOrder order, int targetStatus, Long operatorId, String reason) { // 校验当前状态下是否允许流转到目标状态 if (!canTransit(order.getStatus(), targetStatus)) { throw new BusinessException(当前订单状态不支持该操作); } // 乐观锁更新防止并发把状态改成互相矛盾的值 Integer oldStatus order.getStatus(); order.setStatus(targetStatus); orderMapper.updateById(order); // 记录状态变更日志 OrderLog log new OrderLog(); log.setOrderId(order.getId()); log.setFromStatus(oldStatus); log.setToStatus(targetStatus); log.setOperatorId(operatorId); log.setReason(reason); orderLogMapper.insert(log); }canTransit里维护一张允许流转的状态对照表比如待付定金只能流向草稿绘制中和已取消不能突然变成已完成。这样把非法流转挡在入口处前端就算绕过页面直接调接口也改不了状态。4.3 定时任务处理超时场景纯靠用户手动操作是跑不完整个订单生命周期的必须有定时任务兜底。我用 Spring 自带的Scheduled做简单任务就够超时未支付订单下单后 24 小时未支付定金的订单自动取消。超时未确认收货画师交付最终稿后 7 天买家未确认系统自动确认并结算尾款。超时未交付画师在 deadline 之前未交付草稿系统自动发送催稿通知。定时任务要注意幂等性任务执行前先查状态只处理当前处于特定状态的订单同时用乐观锁保证不会重复处理。4.4 钱包与流水的一致性设计钱包这块是平台最容易出 bug 的地方。核心原则就一条任何余额变动都必须写流水余额字段和流水必须在同一个本地事务里提交。比如支付定金这个操作伪代码如下Transactional public void payDeposit(Long orderId, Long buyerId) { User buyer userMapper.selectById(buyerId); // 校验余额充足 if (buyer.getBalance().compareTo(order.getDeposit()) 0) { throw new BusinessException(余额不足); } // 扣买家余额 buyer.setBalance(buyer.getBalance().subtract(order.getDeposit())); userMapper.updateById(buyer); // 写入买家流水 walletFlowMapper.insert(buildFlow(buyerId, orderId, order.getDeposit().negate(), 支付定金)); // 锁定画师待结算金额先不直接入账等最终交付后再入账 // 更新订单状态 changeOrderStatus(order, OrderStatusEnum.DRAFTING.getCode(), buyerId, 支付定金); }这里有一点很多人会踩坑定金支付成功后钱不应该直接进入画师余额而是进入一个待结算的概念。因为如果最终交易没完成、需要退款画师已经把钱花掉了平台就得垫钱。用待结算金额字段暂存订单完成后才真正入账这是从资金安全角度考虑的必要设计。5. MyBatis-Plus 在使用中的一些关键细节5.1 自动填充与逻辑删除的正确用法MyBatis-Plus 的自动填充很实用创建时间和更新时间不用每个业务逻辑里手动 setComponent public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }实体类字段上加TableField(fill FieldFill.INSERT)就能生效。逻辑删除则是用TableLogic注解删除时自动改成UPDATE ... SET deleted 1。好处是数据还在库里方便追溯坏处是有个经典坑我放到最后一部分单独讲。5.2 乐观锁防止并发覆盖约稿平台里有两类典型的并发场景买家支付定金时两个请求同时扣同一个余额没有锁就会扣两次。状态流转时画师确认交付和买家取消订单同时发起状态被后写入的覆盖。MyBatis-Plus 的乐观锁解决方式是加 version 字段更新时自动带上WHERE version ?版本不对则更新失败。配置一个插件就行Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }实体里的 version 字段用Version标注每次 updateById 时它会自己把 version 1。5.3 分页插件与联表查询的取舍MyBatis-Plus 的分页插件也是通过 MybatisPlusInterceptor 配置interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));然后分页查询PageOrderVO page new Page(current, size); IPageOrderVO result orderMapper.selectOrderPage(page, queryWrapper);这里有个经验联表查询不要一上来就写 LEFT JOIN。画师约稿平台的多数页面是按条件查订单列表这种场景用 MyBatis-Plus 的 in 子查询 DTO 聚合往往比 join 更高效。比如查我接的订单列表先根据 artist_id 查出订单主表分页结果再一次性查用户昵称、最新交付信息按 id 聚合填充。避免大字段表 join 后产生性能问题。5.4 mapper 接口与 XML 放同一个包下的配置很多人习惯把 mapper 接口放 src/main/javaXML 放 resources/mapper分开管理。但实际更稳妥的做法是把 XML 和 mapper 接口放在同一个目录下用classpath*:com/example/commission/mapper/*.xml这种方式扫描。在 SpringBoot2 里配置方式如下在 pom 里把 mapper 目录配置为资源目录确保打包时 XML 会被输出到 classpath。application.yml 里配置mybatis-plus: mapper-locations: classpath*:com/example/commission/mapper/**/*.xml这样 XML 和接口同包IDE 里能直接跳转维护起来比分两处舒服得多。缺点是很多人第一次配会踩坑打包后发现 XML 没打进去一条 SQL 报Invalid bound statement。6. Vue3 前端的几个核心模块拆解6.1 登录态与请求层封装前端所有请求都走 axios 实例统一处理 token 注入和错误提示const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { // 跳转登录页清理本地登录态 localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )路由守卫负责拦截未登录访问router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这个组合是所有 Vue3 业务系统的通用地基约稿平台也不例外。有一点要注意不要把 token 存到 localStorage 以外的复杂方案里小项目用 localStorage 足够cookie 反而要处理 CSRF 问题。6.2 稿件上传与画稿预览约稿平台里最不能省的是文件上传组件。参考图、草稿、最终稿都需要传图。Element Plus 的 upload 组件可以包一层加上图片压缩逻辑async function beforeUpload(file) { // 超过 1MB 的图片先压缩再上传 if (file.size 1024 * 1024) { return await compressImage(file) } return file }压缩图片用 Canvas 重绘实现读取文件到 img然后按最长边缩放再导出为 blob。参考图和草稿都是看个大概的用途压缩到宽度 1920 以内足够清晰上传速度还能快很多。画稿预览我建议用简单的灯箱组件不要一开始就上各种 3D 或者大图浏览库。约稿平台的核心场景是看风格、看细节一个支持放大缩小的简易查看器就够了。6.3 订单状态与 Tab 页签的交互细节订单管理页按状态分 Tab 是约稿平台的标配全部、待付定金、绘制中、待确认、已完成、退款中止。这里有两个前端容易出问题的地方。一个是 Tab 切换时的数据加载。用 Vue3 的 watch 当前 Tab 状态去重新请求不要把数据一股脑全拉回来再在前端过滤。订单列表后端已经分页了前端过滤会出现总共 20 条但当前 Tab 只有 3 条的问题。另一个是 Element Plus 的 tabs 样式覆盖。自定义 Tab 高度、字体、选中态颜色时需要看一眼组件内部 DOM 结构。如果是::deep穿透写法注意 scoped 样式下要加:deep()前缀才能生效否则样式怎么都改不动浏览器 F12 一看才发现是 scoped 属性导致的选择器不匹配。6.4 Pinia 做全局状态管理用户信息、未读消息数、当前钱包余额这些跨页面共享的状态我推荐用 Pinia 管理。相比 VuexPinia 的 API 更简洁类型提示更好export const useUserStore defineStore(user, { state: () ({ userInfo: null, unreadCount: 0 }), actions: { setUserInfo(info) { this.userInfo info } } })登录成功后调一次接口把用户信息塞进 store后续所有页面都能直接取不用每个页面重复拉取个人信息。7. 从环境到运行画师约稿平台本地跑通实录7.1 环境清单与安装假设你要在自己电脑上把项目跑起来先确认环境。这套项目对版本要求不算苛刻软件版本要求说明JDK1.8SpringBoot2 的标配Maven3.6 及以上后端依赖管理MySQL8.0.x项目指定版本Node.js16 或 18Vue3 Vite 的要求前端包管理器npm / pnpm推荐 pnpm安装更快IDEIDEA / VSCode后端推荐 IDEA前端用 VSCode 也行MySQL8.0 安装有多种方式本地开发图省事可以直接用 Dockerdocker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot \ -e MYSQL_DATABASEcommission \ mysql:8.0如果不用 Docker直接去官网下载对应系统的 installer 也一样。安装时注意端口别被占用以及 root 密码记清楚。7.2 初始化数据库数据库脚本一般会在项目文档的 doc/sql 目录下。执行方式mysql -uroot -p commission.sql或者用 Navicat 新建数据库后运行 SQL 文件。初始化完成后重点检查几张核心表是否创建成功用户表里是否有一条初始管理员数据。这里提醒一下导入 SQL 前先确认数据库字符集是 utf8mb4。命令行导入时如果字符集不对中文注释或者中文数据全变成乱码。7.3 后端启动步骤后端启动前要改两处配置。第一处是数据库连接在src/main/resources/application.yml里spring: datasource: url: jdbc:mysql://localhost:3306/commission?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root第二处是上传目录文件上传后存本地的路径要改成你的实际路径。启动后端mvn spring-boot:run或者打包后启动mvn clean package -DskipTests java -jar target/commission-server.jar能看到Started CommissionApplication字样就说明启动成功。7.4 前端启动与联调前端启动更简单cd commission-web pnpm install pnpm devVite 默认端口是 5173浏览器打开就能看到登录页。联调阶段要确认前端能请求到后端接口。如果前端把请求通过 vite proxy 转发在 vite.config.js 里配置server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }7.5 跑通一条完整业务路径项目能登录后建议按这条路径验证系统是否正常注册一个普通用户账号发布一条约稿需求。注册画师账号申请画师认证管理员审核通过。画师在需求池接单生成订单。买家支付定金。画师上传草稿买家确认。画师上传最终稿买家确认收货。查看双方钱包余额和流水记录。按这条链路走一遍基本能把所有核心模块都覆盖到。如果中途某一环卡住优先看后端日志和订单状态日志往往能快速定位问题。8. 开发过程中踩过的坑与完整排查复盘8.1 MySQL8.0 的时区、驱动、字符集三连坑这三个坑几乎所有人都踩过我按排查顺序说。第一个是驱动名。MySQL8.0 的 JDBC 驱动必须是com.mysql.cj.jdbc.Driver老项目里写com.mysql.jdbc.Driver会直接报 ClassNotFoundException。第二个是时区。URL 里不配serverTimezone启动会报The server time zone value Öйú±ê׼ʱ¼ä这种奇怪的错。这是 MySQL 返回的 CMT 时区信息和 JDK 不匹配导致的。解决方案就是 URL 里加serverTimezoneAsia/Shanghai。第三个是字符集。建库的时候没有指定 utf8mb4后期插入 emoji 或者中文特殊符号会报Incorrect string value。解决方案是建库语句里带上CREATE DATABASE commission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;8.2 逻辑删除与唯一索引的经典冲突这是我在一个画师标签表上踩的坑排查过程比较典型完整复盘一下。背景画师标签表里我想让(user_id, tag_name)唯一所以在数据库里建了唯一索引。同时表里用了 MyBatis-Plus 的逻辑删除deleted 字段默认 0删除后变成 1。现象第一次删除某条标签后再插入同名的标签会报Duplicate entry错误。原因很清晰逻辑删除后数据还在表里deleted1但唯一索引判断的是 user_id tag_name和 deleted 无关导致数据库认为那条数据仍然存在。排查链路先复现删除标签后再插同一条控制台直接报唯一索引冲突。查数据库SELECT * FROM artist_tag 发现已删除记录还在deleted1。分析根因唯一索引没包含 deleted 字段逻辑删除数据仍然占着唯一索引的位置。验证修复方案把唯一索引从(user_id, tag_name)改成(user_id, tag_name, deleted)。但这里还有一个隐患如果同一 user 删除又新增两次deleted 一直是 1还是会冲突。最终方案放弃对这个表用逻辑删除改成物理删除。因为标签这类数据没有追溯价值物理删除反而最省心。这个教训是不是所有表都适合逻辑删除。订单、钱包流水这类需要留痕的表用逻辑删除没问题标签、临时上传文件这类纯辅助数据物理删除更干净。8.3 后端跨域与字段命名不一致前后端联调时会遇到两种常见报错。跨域前端请求后端控制台报Access-Control-Allow-Origin。如果用了 Vite 代理同源问题通常不存在如果前端直接请求后端地址就需要后端加跨域配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }字段命名前端拿到的 JSON 字段是orderNo后端实体里是order_no导致页面显示 undefined。MyBatis-Plus 默认开启驼峰映射实体字段写成驼峰orderNoJava 里 setter/getter 对得上就行。前端如果用的字段名还是后端 DTO 里定义的保持一致就不会出问题。8.4 前端组件兼容性小坑Vue3 项目在部分浏览器上偶尔会出现一些组件样式或者交互不生效的情况。我遇到过两个一个是 Element Plus 的 tabs 在 Edge 浏览器下选中态样式异常。排查发现是浏览器版本太旧对 CSS 变量和color-scheme的支持不完整。方案升级浏览器或者在全局样式里干脆对 tabs 的选中态写死颜色覆盖。另一个是上传组件的on-success钩子有时候监听不到。这是异步时序问题上传成功回调里如果还调用了其他接口比如刷新文件列表两个请求竞争会导致回调不触发。解决方案是不要只依赖 on-success在on-change里拿响应结果做后续处理或者加个 debounce。这些前端问题看起来小但排查起来很耗时间。我的经验是遇到类似问题先确认浏览器版本、再确认组件版本、最后才怀疑自己的代码顺序反了会走很多弯路。最后再说点实际体会做完这个画师约稿平台我最大的感受是这类项目技术本身不复杂复杂的是把交易流程设计得严谨。接口写得再漂亮订单状态不对、流水对不上账上线就是事故。我建议准备做类似系统的人开工前先别急着写代码把订单状态图、钱包流水关系、用户角色权限这三件事想清楚。画一张状态流转表、列清楚每个操作的前置条件和后置动作后面写代码会顺利很多。这套 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 的组合作为画师约稿平台的底座完全够用。真到了要上生产的阶段再去考虑 Redis 缓存、消息队列、对象存储、那么问题不大。前期重要的是把核心业务跑顺把状态和资金的一致性管住平台才算真的立住了。
返回列表